@oddessentials/agent-guild 0.31.0 → 0.33.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 +42 -4
- package/bin/agent-guild.mjs +8 -3
- package/package.json +1 -1
- package/src/manager/autostart.mjs +492 -0
- package/src/manager/github-views.mjs +37 -1
- package/src/manager/main.mjs +15 -2
- package/src/manager/server.mjs +13 -1
- package/src/manager/session.mjs +27 -2
- package/src/manager/shell-env.mjs +53 -13
- package/src/manager/uninstall.mjs +7 -2
- package/src/manager/usage.mjs +116 -10
- package/web/app.js +278 -6
- package/web/environment.js +2 -0
- package/web/index.html +28 -0
- package/web/layout.js +36 -0
- package/web/styles.css +25 -3
- package/web/terminal-controls.js +7 -25
package/web/app.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { SOUNDS, MAX_ALERT_AGE_MS, playOnce, rearmSound, managerLossWatcher, stopWatcher, updateWatcher } from './alerts.js';
|
|
5
5
|
import { TerminalCopy } from './terminal-copy.js';
|
|
6
6
|
import { TerminalControls, bindTerminalViewport } from './terminal-controls.js';
|
|
7
|
-
import { topbarInline, dockMode, clampDockWidth, stageBesideDock, splitMode, clampRatio, bindSplitter, DOCK_MIN, SPLIT_RATIO_MIN } from './layout.js';
|
|
7
|
+
import { topbarInline, dockMode, clampDockWidth, stageBesideDock, splitMode, clampRatio, bindSplitter, bindVisibleViewport, DOCK_MIN, SPLIT_RATIO_MIN } from './layout.js';
|
|
8
8
|
import { highlightParts, rankRepos, recentFirst, remember, repoForOrigin, repoKey } from './repo-search.js';
|
|
9
9
|
import { createActivityFavicon, isSessionWorking } from './activity-favicon.js';
|
|
10
10
|
import { createRemoteAccessUI } from './remote-access.js';
|
|
@@ -299,6 +299,69 @@ function alertSound(name, key, at = Date.now(), fresh = () => true, related = []
|
|
|
299
299
|
});
|
|
300
300
|
}
|
|
301
301
|
|
|
302
|
+
const AUTOSTART_NOTE = 'Starts the session manager in the background when you sign in to the computer running Agent Guild. The page does not open.';
|
|
303
|
+
let autostartRequest = 0;
|
|
304
|
+
let autostartChanging = false;
|
|
305
|
+
|
|
306
|
+
/** What the entry did at the last sign-in it ran at, or '' while it is off. */
|
|
307
|
+
function autostartRun(autostart) {
|
|
308
|
+
if (!autostart?.available || !autostart.enabled) return '';
|
|
309
|
+
const run = autostart.lastRun;
|
|
310
|
+
if (!run) return 'Has not run at a sign-in yet.';
|
|
311
|
+
const at = new Date(run.at).toLocaleString([], { dateStyle: 'medium', timeStyle: 'short' });
|
|
312
|
+
if (run.outcome === 'started') return `Last ran at sign-in on ${at} and started the session manager.`;
|
|
313
|
+
if (run.outcome === 'running') return `Last ran at sign-in on ${at}; the session manager was already running.`;
|
|
314
|
+
if (run.outcome === 'starting') return `Starting the session manager for the sign-in on ${at}…`;
|
|
315
|
+
return `Last ran at sign-in on ${at}, but the session manager did not start.${autostart.log ? ` See ${autostart.log}.` : ''}`;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** The manager's sign-in setting; hidden when the manager has none. */
|
|
319
|
+
function renderAutostart(autostart) {
|
|
320
|
+
$('autostart-choice').hidden = !autostart;
|
|
321
|
+
$('autostart').checked = Boolean(autostart?.enabled);
|
|
322
|
+
$('autostart').disabled = !autostart?.available;
|
|
323
|
+
$('autostart-note').textContent = autostart?.reason || autostart?.note || AUTOSTART_NOTE;
|
|
324
|
+
const run = autostartRun(autostart);
|
|
325
|
+
$('autostart-run').textContent = run;
|
|
326
|
+
$('autostart-run').hidden = !run;
|
|
327
|
+
$('autostart-run').dataset.outcome = autostart?.enabled ? autostart.lastRun?.outcome ?? 'none' : '';
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async function loadAutostart({ afterChange = false } = {}) {
|
|
331
|
+
if (!state.connected || (autostartChanging && !afterChange)) return;
|
|
332
|
+
const request = ++autostartRequest;
|
|
333
|
+
$('autostart').disabled = true;
|
|
334
|
+
try {
|
|
335
|
+
const { autostart } = await api('GET', '/autostart');
|
|
336
|
+
if (request === autostartRequest) renderAutostart(autostart);
|
|
337
|
+
} catch (err) {
|
|
338
|
+
if (request !== autostartRequest) return;
|
|
339
|
+
if (err instanceof AuthError) showAuth(err.message);
|
|
340
|
+
else if (err.code === 'not_found') renderAutostart(null);
|
|
341
|
+
else renderAutostart({ available: false, enabled: false, reason: `Could not read the startup setting: ${err.message}` });
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async function changeAutostart(input) {
|
|
346
|
+
if (autostartChanging) return;
|
|
347
|
+
autostartChanging = true;
|
|
348
|
+
++autostartRequest;
|
|
349
|
+
const enabled = input.checked;
|
|
350
|
+
input.disabled = true;
|
|
351
|
+
try {
|
|
352
|
+
renderAutostart((await api('PUT', '/autostart', { enabled })).autostart);
|
|
353
|
+
} catch (err) {
|
|
354
|
+
if (err instanceof AuthError) showAuth(err.message);
|
|
355
|
+
else {
|
|
356
|
+
toast(err.message);
|
|
357
|
+
// An OS operation can partly succeed. Read back what actually happened.
|
|
358
|
+
await loadAutostart({ afterChange: true });
|
|
359
|
+
}
|
|
360
|
+
} finally {
|
|
361
|
+
autostartChanging = false;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
302
365
|
function changeSound(input) {
|
|
303
366
|
save(SOUND_KEY, input.checked ? 'on' : null);
|
|
304
367
|
prepareSounds();
|
|
@@ -2967,6 +3030,13 @@ function loadGitHub() {
|
|
|
2967
3030
|
function setGitHub(github) {
|
|
2968
3031
|
const before = state.github?.signIn;
|
|
2969
3032
|
state.github = github;
|
|
3033
|
+
const branches = githubPick.data.branches;
|
|
3034
|
+
if (branches && !github.accounts.some((a) => a.id === branches.repo.accountId && !a.needsSignIn)) {
|
|
3035
|
+
// Replace the generation even while the dock is closed; signing in again must
|
|
3036
|
+
// never make an earlier account's pending answer current.
|
|
3037
|
+
githubPick.data.branches = { ...branches, value: null, loading: false, nextPage: null,
|
|
3038
|
+
error: `Sign in as @${branches.repo.login} in Repositories to see its branches.` };
|
|
3039
|
+
}
|
|
2970
3040
|
const done = github.signIn?.status === 'done' && before?.status === 'pending' ? github.signIn : null;
|
|
2971
3041
|
if (done) {
|
|
2972
3042
|
selectGitHubAccount(done.accountId);
|
|
@@ -3086,7 +3156,7 @@ function openGitHub({ focus = true } = {}) {
|
|
|
3086
3156
|
loadGitHub();
|
|
3087
3157
|
if (githubPick.repo) {
|
|
3088
3158
|
loadView('actions');
|
|
3089
|
-
if (
|
|
3159
|
+
if (['issues', 'pulls', 'branches'].includes(githubShownView())) loadView(githubShownView());
|
|
3090
3160
|
}
|
|
3091
3161
|
followTerminal();
|
|
3092
3162
|
if (focus) ($('github-card').querySelector('.btn.primary') ?? $('dock-close')).focus();
|
|
@@ -3497,7 +3567,7 @@ function useFolder(dir) {
|
|
|
3497
3567
|
|
|
3498
3568
|
// ---- GitHub repository views ------------------------------------------------
|
|
3499
3569
|
|
|
3500
|
-
const GITHUB_VIEWS = ['repos', 'issues', 'actions', 'pulls'];
|
|
3570
|
+
const GITHUB_VIEWS = ['repos', 'issues', 'actions', 'pulls', 'branches'];
|
|
3501
3571
|
const RUNS_POLL_MS = 6000;
|
|
3502
3572
|
const RUNS_IDLE_POLL_MS = 30000;
|
|
3503
3573
|
const BODY_LIMIT = 48000;
|
|
@@ -3592,7 +3662,11 @@ function pickRepo(repo, { chosen = false } = {}) {
|
|
|
3592
3662
|
githubPick.recent = remember(githubPick.recent, repo);
|
|
3593
3663
|
save(GITHUB_RECENT_KEY, JSON.stringify(githubPick.recent));
|
|
3594
3664
|
}
|
|
3595
|
-
if (!same)
|
|
3665
|
+
if (!same) {
|
|
3666
|
+
Object.assign(githubPick, { data: {}, editing: null });
|
|
3667
|
+
$('github-branches-filter').value = '';
|
|
3668
|
+
$('github-branches').scrollTop = 0;
|
|
3669
|
+
}
|
|
3596
3670
|
if ((!same || chosen) && githubView.accountId !== repo.accountId && state.github?.accounts.some((a) => a.id === repo.accountId)) {
|
|
3597
3671
|
selectGitHubAccount(repo.accountId);
|
|
3598
3672
|
githubView.card = null;
|
|
@@ -3605,7 +3679,7 @@ function pickRepo(repo, { chosen = false } = {}) {
|
|
|
3605
3679
|
renderGitHubViews();
|
|
3606
3680
|
if (!dockShows('github')) return;
|
|
3607
3681
|
loadView('actions');
|
|
3608
|
-
if (
|
|
3682
|
+
if (['issues', 'pulls', 'branches'].includes(githubShownView())) loadView(githubShownView());
|
|
3609
3683
|
}
|
|
3610
3684
|
|
|
3611
3685
|
/** Picks the repository whose clone holds the focused terminal's folder, once for each terminal focused. */
|
|
@@ -3628,6 +3702,7 @@ async function followTerminal() {
|
|
|
3628
3702
|
}
|
|
3629
3703
|
|
|
3630
3704
|
async function loadView(view) {
|
|
3705
|
+
if (view === 'branches') return loadBranches();
|
|
3631
3706
|
const repo = githubPick.repo;
|
|
3632
3707
|
if (!repo || view === 'repos') return;
|
|
3633
3708
|
const query = view === 'issues' ? `?state=${githubPick.issueState}` : '';
|
|
@@ -3652,6 +3727,175 @@ async function loadView(view) {
|
|
|
3652
3727
|
if (view === 'actions') scheduleRuns();
|
|
3653
3728
|
}
|
|
3654
3729
|
|
|
3730
|
+
// Each object is a listing generation, scoped to the selected account/repository.
|
|
3731
|
+
// A retry continues that generation; Refresh replaces it and starts at page one.
|
|
3732
|
+
function branchesCurrent(request) {
|
|
3733
|
+
return githubPick.data.branches === request && githubPick.repo && repoKey(githubPick.repo) === request.key
|
|
3734
|
+
&& state.github?.accounts.some((a) => a.id === request.repo.accountId && !a.needsSignIn);
|
|
3735
|
+
}
|
|
3736
|
+
|
|
3737
|
+
function branchesVisible() {
|
|
3738
|
+
return dockShows('github') && githubShownView() === 'branches' && document.visibilityState === 'visible';
|
|
3739
|
+
}
|
|
3740
|
+
|
|
3741
|
+
async function loadBranches({ resume = false } = {}) {
|
|
3742
|
+
const repo = githubPick.repo;
|
|
3743
|
+
if (!repo || !state.github?.accounts.some((a) => a.id === repo.accountId && !a.needsSignIn)) return;
|
|
3744
|
+
const before = viewData('branches');
|
|
3745
|
+
if (resume && (!before || before.loading || before.nextPage === null)) return;
|
|
3746
|
+
const request = resume ? before : {
|
|
3747
|
+
key: repoKey(repo), repo, value: before?.value ?? null, loading: false, error: null,
|
|
3748
|
+
nextPage: 1, received: false, rows: new Map(), defaultBranch: null, metadataError: null,
|
|
3749
|
+
};
|
|
3750
|
+
githubPick.data.branches = request;
|
|
3751
|
+
request.loading = true;
|
|
3752
|
+
request.error = null;
|
|
3753
|
+
renderGitHubViews();
|
|
3754
|
+
try {
|
|
3755
|
+
while (request.nextPage !== null && branchesVisible() && branchesCurrent(request)) {
|
|
3756
|
+
const page = request.nextPage;
|
|
3757
|
+
const result = await api('GET', `${repoPath(repo)}/branches?page=${page}`);
|
|
3758
|
+
if (!branchesCurrent(request)) return;
|
|
3759
|
+
// A malformed response must not masquerade as a complete, empty list.
|
|
3760
|
+
if (!Array.isArray(result.branches) || (result.nextPage !== null
|
|
3761
|
+
&& (!Number.isSafeInteger(result.nextPage) || result.nextPage <= page))) {
|
|
3762
|
+
throw new Error('Could not read the next page of branches. Try again.');
|
|
3763
|
+
}
|
|
3764
|
+
if (page === 1) {
|
|
3765
|
+
request.defaultBranch = result.defaultBranch;
|
|
3766
|
+
request.metadataError = result.metadataError;
|
|
3767
|
+
}
|
|
3768
|
+
for (const branch of result.branches) request.rows.set(branch.name, branch);
|
|
3769
|
+
request.nextPage = result.nextPage;
|
|
3770
|
+
// First loads display each page. A refresh keeps the previous list until
|
|
3771
|
+
// every page arrives, so later-page rows and the reading position survive.
|
|
3772
|
+
// `received` means the displayed value belongs to this listing generation.
|
|
3773
|
+
if (!request.value || request.received || request.nextPage === null) {
|
|
3774
|
+
request.received = true;
|
|
3775
|
+
request.value = { ...result, branches: [...request.rows.values()],
|
|
3776
|
+
defaultBranch: request.defaultBranch, metadataError: request.metadataError };
|
|
3777
|
+
}
|
|
3778
|
+
renderGitHubViews();
|
|
3779
|
+
}
|
|
3780
|
+
} catch (err) {
|
|
3781
|
+
if (!branchesCurrent(request)) return;
|
|
3782
|
+
if (err instanceof AuthError) { showAuth(err.message); return; }
|
|
3783
|
+
request.error = err.message;
|
|
3784
|
+
} finally {
|
|
3785
|
+
if (branchesCurrent(request)) {
|
|
3786
|
+
request.loading = false;
|
|
3787
|
+
renderGitHubViews();
|
|
3788
|
+
}
|
|
3789
|
+
}
|
|
3790
|
+
}
|
|
3791
|
+
|
|
3792
|
+
/** Keep the first visible branch at the same pixel offset when earlier rows arrive. */
|
|
3793
|
+
function branchAnchor(panel, list) {
|
|
3794
|
+
const { top, bottom } = panel.getBoundingClientRect();
|
|
3795
|
+
const row = [...list.children].find((node) => {
|
|
3796
|
+
const rect = node.getBoundingClientRect();
|
|
3797
|
+
return rect.bottom > top && rect.top < bottom;
|
|
3798
|
+
});
|
|
3799
|
+
return row ? { name: row.dataset.branch, offset: row.getBoundingClientRect().top - top } : null;
|
|
3800
|
+
}
|
|
3801
|
+
|
|
3802
|
+
async function copyBranch(name, control) {
|
|
3803
|
+
const request = viewData('branches');
|
|
3804
|
+
control.disabled = true;
|
|
3805
|
+
let message;
|
|
3806
|
+
try {
|
|
3807
|
+
if (!navigator.clipboard?.writeText) throw new Error('Clipboard unavailable');
|
|
3808
|
+
await navigator.clipboard.writeText(name);
|
|
3809
|
+
message = `Copied branch name: ${name}`;
|
|
3810
|
+
} catch {
|
|
3811
|
+
message = 'Could not copy. Select the branch name and copy it using your browser.';
|
|
3812
|
+
} finally {
|
|
3813
|
+
control.disabled = false;
|
|
3814
|
+
}
|
|
3815
|
+
if (request === viewData('branches') && branchesVisible() && control.isConnected) toast(message);
|
|
3816
|
+
}
|
|
3817
|
+
|
|
3818
|
+
function renderBranches({ preserveAnchor = true } = {}) {
|
|
3819
|
+
const panel = $('github-branches');
|
|
3820
|
+
const list = $('github-branches-list');
|
|
3821
|
+
const repo = githubPick.repo;
|
|
3822
|
+
const key = repo ? repoKey(repo) : '';
|
|
3823
|
+
const same = panel.dataset.repo === key;
|
|
3824
|
+
const anchor = same && preserveAnchor ? branchAnchor(panel, list) : null;
|
|
3825
|
+
const focused = list.contains(document.activeElement) ? document.activeElement : null;
|
|
3826
|
+
const oldScroll = same ? panel.scrollTop : 0;
|
|
3827
|
+
if (!same) { list.replaceChildren(); panel.dataset.repo = key; }
|
|
3828
|
+
$('github-branches-pick').hidden = Boolean(repo);
|
|
3829
|
+
$('github-branches-content').hidden = !repo;
|
|
3830
|
+
if (!repo) return;
|
|
3831
|
+
const slot = viewData('branches');
|
|
3832
|
+
const value = slot?.value;
|
|
3833
|
+
const query = $('github-branches-filter').value.trim().toLocaleLowerCase();
|
|
3834
|
+
$('github-branches-clear').disabled = !$('github-branches-filter').value;
|
|
3835
|
+
const refresh = $('github-branches-refresh');
|
|
3836
|
+
const usable = state.github?.accounts.some((a) => a.id === repo.accountId && !a.needsSignIn);
|
|
3837
|
+
refresh.disabled = Boolean(slot?.loading) || !usable;
|
|
3838
|
+
refresh.textContent = slot?.loading ? (value ? 'Refreshing…' : 'Loading…') : 'Refresh';
|
|
3839
|
+
const out = $('github-branches-out');
|
|
3840
|
+
out.href = value?.url ?? `https://github.com/${repo.fullName}/branches`;
|
|
3841
|
+
out.title = `Open ${repo.fullName} branches on GitHub`;
|
|
3842
|
+
const branches = (value?.branches ?? []).filter((b) => b.name.toLocaleLowerCase().includes(query)).sort((a, b) =>
|
|
3843
|
+
Number(b.name === value.defaultBranch) - Number(a.name === value.defaultBranch)
|
|
3844
|
+
|| a.name.localeCompare(b.name) || (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
3845
|
+
const complete = Boolean(slot?.received && value?.nextPage === null);
|
|
3846
|
+
const count = value?.branches.length ?? 0;
|
|
3847
|
+
let status = !value ? (slot?.error ? 'Branches could not be loaded.' : 'Loading branches…')
|
|
3848
|
+
: query ? `${branches.length} of ${count} loaded branches match.`
|
|
3849
|
+
: `${count} ${count === 1 ? 'branch' : 'branches'}${complete ? '' : ' loaded'}.`;
|
|
3850
|
+
if (value && !slot.received) status += slot.loading ? ' Refreshing…' : ' Showing previous results.';
|
|
3851
|
+
else if (!complete && value) status += slot.loading ? ' Loading more…' : ' Listing incomplete.';
|
|
3852
|
+
if (complete && !branches.length) status = query ? `No branches match “${$('github-branches-filter').value.trim()}”.` : 'No remote branches yet.';
|
|
3853
|
+
const statusNode = $('github-branches-status');
|
|
3854
|
+
if (statusNode.textContent !== status) statusNode.textContent = status;
|
|
3855
|
+
const failure = $('github-branches-error');
|
|
3856
|
+
failure.hidden = !slot?.error;
|
|
3857
|
+
failure.querySelector('span').textContent = slot?.error ?? '';
|
|
3858
|
+
$('github-branches-retry').disabled = Boolean(slot?.loading) || slot?.nextPage === null || !usable;
|
|
3859
|
+
const metadata = $('github-branches-metadata');
|
|
3860
|
+
metadata.hidden = !value?.metadataError;
|
|
3861
|
+
metadata.textContent = value?.metadataError ? 'Branches loaded, but the default branch could not be identified. Refresh to try again.' : '';
|
|
3862
|
+
const existing = new Map([...list.children].map((row) => [row.dataset.branch, row]));
|
|
3863
|
+
const wanted = new Set(branches.map((branch) => branch.name));
|
|
3864
|
+
let cursor = list.firstElementChild;
|
|
3865
|
+
for (const branch of branches) {
|
|
3866
|
+
let row = existing.get(branch.name);
|
|
3867
|
+
if (!row) {
|
|
3868
|
+
const copy = button('Copy name', () => copyBranch(branch.name, copy));
|
|
3869
|
+
copy.setAttribute('aria-label', `Copy branch name ${branch.name}`);
|
|
3870
|
+
row = recordRow(`branch:${branch.name}`, branch.name, '', copy, branch.url);
|
|
3871
|
+
row.classList.add('github-branch');
|
|
3872
|
+
row.dataset.branch = branch.name;
|
|
3873
|
+
row.setAttribute('role', 'listitem');
|
|
3874
|
+
row.querySelector('.history-title').append(el('span', 'github-branch-badges'));
|
|
3875
|
+
}
|
|
3876
|
+
const badges = row.querySelector('.github-branch-badges');
|
|
3877
|
+
const badgeState = `${branch.name === value.defaultBranch}:${branch.protected}`;
|
|
3878
|
+
if (badges.dataset.state !== badgeState) {
|
|
3879
|
+
badges.dataset.state = badgeState;
|
|
3880
|
+
badges.replaceChildren(...[
|
|
3881
|
+
branch.name === value.defaultBranch ? badge('', 'Default', 'Default branch') : null,
|
|
3882
|
+
branch.protected ? badge('', 'Protected', 'Protected by branch protection or rulesets') : null,
|
|
3883
|
+
].filter(Boolean));
|
|
3884
|
+
}
|
|
3885
|
+
row.querySelector('.history-meta').textContent = branch.sha ? branch.sha.slice(0, 7) : '';
|
|
3886
|
+
row.querySelector('.history-meta').title = branch.sha ?? '';
|
|
3887
|
+
if (row === cursor) cursor = cursor.nextElementSibling;
|
|
3888
|
+
else list.insertBefore(row, cursor);
|
|
3889
|
+
}
|
|
3890
|
+
for (const row of [...list.children]) if (!wanted.has(row.dataset.branch)) row.remove();
|
|
3891
|
+
if (focused?.isConnected && document.activeElement !== focused) focused.focus({ preventScroll: true });
|
|
3892
|
+
panel.scrollTop = oldScroll;
|
|
3893
|
+
if (anchor) {
|
|
3894
|
+
const row = [...list.children].find((node) => node.dataset.branch === anchor.name);
|
|
3895
|
+
if (row) panel.scrollTop += row.getBoundingClientRect().top - panel.getBoundingClientRect().top - anchor.offset;
|
|
3896
|
+
}
|
|
3897
|
+
}
|
|
3898
|
+
|
|
3655
3899
|
function viewData(view) {
|
|
3656
3900
|
const slot = githubPick.data[view];
|
|
3657
3901
|
const repo = githubPick.repo;
|
|
@@ -3703,7 +3947,17 @@ function setPickerOpen(open) {
|
|
|
3703
3947
|
input.setAttribute('aria-expanded', String(open));
|
|
3704
3948
|
$('github-repo-list').hidden = !open;
|
|
3705
3949
|
if (!open) input.removeAttribute('aria-activedescendant');
|
|
3706
|
-
else
|
|
3950
|
+
else {
|
|
3951
|
+
fitPickerList();
|
|
3952
|
+
renderPickerList();
|
|
3953
|
+
}
|
|
3954
|
+
}
|
|
3955
|
+
|
|
3956
|
+
// The list stays inside the visible part of the dock, so no option sits under an on-screen keyboard.
|
|
3957
|
+
function fitPickerList() {
|
|
3958
|
+
if (!githubPick.open) return;
|
|
3959
|
+
const room = $('dock').getBoundingClientRect().bottom - $('github-repo').getBoundingClientRect().bottom - 12;
|
|
3960
|
+
$('github-repo-list').style.setProperty('--picker-room', `${Math.max(120, Math.floor(room))}px`);
|
|
3707
3961
|
}
|
|
3708
3962
|
|
|
3709
3963
|
function highlighted(text, query) {
|
|
@@ -3824,6 +4078,7 @@ function renderGitHubViews() {
|
|
|
3824
4078
|
$('github-issues').hidden = shown !== 'issues';
|
|
3825
4079
|
$('github-runs').hidden = shown !== 'actions';
|
|
3826
4080
|
$('github-pulls').hidden = shown !== 'pulls';
|
|
4081
|
+
$('github-branches').hidden = shown !== 'branches';
|
|
3827
4082
|
const ready = Boolean(state.github && githubAccount() && !githubAccount().needsSignIn);
|
|
3828
4083
|
$('github-clone-into').hidden = !ready || shown !== 'repos';
|
|
3829
4084
|
const running = Boolean(viewData('actions')?.value?.running);
|
|
@@ -3833,6 +4088,7 @@ function renderGitHubViews() {
|
|
|
3833
4088
|
if (shown === 'issues') renderIssues();
|
|
3834
4089
|
else if (shown === 'actions') renderRuns();
|
|
3835
4090
|
else if (shown === 'pulls') renderPulls();
|
|
4091
|
+
else if (shown === 'branches') renderBranches();
|
|
3836
4092
|
}
|
|
3837
4093
|
|
|
3838
4094
|
/** Rebuilds a panel and puts the focus back on the control with the same `data-key`. */
|
|
@@ -4863,6 +5119,7 @@ const terminalControls = new TerminalControls({
|
|
|
4863
5119
|
},
|
|
4864
5120
|
});
|
|
4865
5121
|
bindTerminalViewport($('terminal-panel'), $('terminal-controls'));
|
|
5122
|
+
bindVisibleViewport($('dock'), 'dock', { fitted: fitPickerList });
|
|
4866
5123
|
|
|
4867
5124
|
const terminalCopy = new TerminalCopy({
|
|
4868
5125
|
opener: $('panel-copy'), dialog: $('terminal-copy'),
|
|
@@ -5650,6 +5907,18 @@ $('github-views').addEventListener('click', (e) => {
|
|
|
5650
5907
|
if (view) switchGitHubView(view);
|
|
5651
5908
|
});
|
|
5652
5909
|
$('github-views').addEventListener('keydown', moveGitHubView);
|
|
5910
|
+
$('github-branches-refresh').addEventListener('click', () => loadView('branches'));
|
|
5911
|
+
$('github-branches-retry').addEventListener('click', () => loadBranches({ resume: true }));
|
|
5912
|
+
$('github-branches-filter').addEventListener('input', () => {
|
|
5913
|
+
$('github-branches').scrollTop = 0;
|
|
5914
|
+
renderBranches({ preserveAnchor: false });
|
|
5915
|
+
});
|
|
5916
|
+
$('github-branches-clear').addEventListener('click', () => {
|
|
5917
|
+
$('github-branches-filter').value = '';
|
|
5918
|
+
$('github-branches').scrollTop = 0;
|
|
5919
|
+
renderBranches({ preserveAnchor: false });
|
|
5920
|
+
$('github-branches-filter').focus();
|
|
5921
|
+
});
|
|
5653
5922
|
$('github-parent').addEventListener('change', () => setCloneParent(cloneParent()));
|
|
5654
5923
|
$('github-parent-pick').addEventListener('click', firstClick(() => chooseFolder('clone')));
|
|
5655
5924
|
$('history-here').addEventListener('change', renderHistory);
|
|
@@ -5710,6 +5979,7 @@ document.addEventListener('visibilitychange', () => {
|
|
|
5710
5979
|
if (document.visibilityState === 'visible' && state.connected && Date.now() - newsLoadedAt > 60000) loadNews();
|
|
5711
5980
|
if (document.visibilityState === 'visible' && dockShows('github') && githubShownView() === 'actions') loadView('actions');
|
|
5712
5981
|
else scheduleRuns();
|
|
5982
|
+
if (branchesVisible() && !viewData('branches')?.error) loadBranches({ resume: true });
|
|
5713
5983
|
});
|
|
5714
5984
|
addEventListener('pagehide', () => {
|
|
5715
5985
|
flushNotes();
|
|
@@ -5793,6 +6063,7 @@ $('settings-menu').addEventListener('change', (e) => {
|
|
|
5793
6063
|
else if (e.target.name === 'theme') changeTheme(e.target);
|
|
5794
6064
|
else if (e.target.name === 'sound') changeSound(e.target);
|
|
5795
6065
|
else if (e.target.name === 'voice') changeVoice(e.target);
|
|
6066
|
+
else if (e.target.name === 'autostart') changeAutostart(e.target);
|
|
5796
6067
|
});
|
|
5797
6068
|
/**
|
|
5798
6069
|
* Runs `opened` once a menu shows and `closed` once it hides. Chrome skips the toggle event of a menu
|
|
@@ -5812,6 +6083,7 @@ function returnFocus(menu, invoker) {
|
|
|
5812
6083
|
if ((!at || at === document.body || menu.contains(at)) && invoker?.checkVisibility?.()) invoker.focus();
|
|
5813
6084
|
}
|
|
5814
6085
|
onMenu($('settings-menu'), (menu) => {
|
|
6086
|
+
loadAutostart();
|
|
5815
6087
|
placeMenu(menu, $('settings'));
|
|
5816
6088
|
menu.querySelector('input:checked')?.focus();
|
|
5817
6089
|
}, (menu) => returnFocus(menu, $('settings')));
|
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
|
@@ -46,6 +46,12 @@
|
|
|
46
46
|
<legend>Sound</legend>
|
|
47
47
|
<label class="choice" title="Plays when the session manager stops, remains unavailable after a brief recovery check, or a new Agent Guild update is discovered. Click or type on this page to allow playback."><input id="sound" type="checkbox" name="sound"><span class="sound-alerts">Alert sounds</span></label>
|
|
48
48
|
</fieldset>
|
|
49
|
+
<fieldset id="autostart-choice" hidden>
|
|
50
|
+
<legend>Startup</legend>
|
|
51
|
+
<label class="choice"><input id="autostart" type="checkbox" name="autostart"><span class="autostart">Launch at sign-in</span></label>
|
|
52
|
+
<p id="autostart-note" class="voice-note"></p>
|
|
53
|
+
<p id="autostart-run" class="voice-note autostart-run" role="status" hidden></p>
|
|
54
|
+
</fieldset>
|
|
49
55
|
<fieldset id="voice-choice" hidden>
|
|
50
56
|
<legend>Voice</legend>
|
|
51
57
|
<label class="choice"><input id="voice" type="checkbox" name="voice"><span class="voice-input">Voice input</span></label>
|
|
@@ -208,6 +214,7 @@
|
|
|
208
214
|
<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
215
|
<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
216
|
<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>
|
|
217
|
+
<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
218
|
</div>
|
|
212
219
|
<div id="github-repos" class="models-body github-body" role="tabpanel" aria-labelledby="github-view-repos">
|
|
213
220
|
<section id="github-card" class="github-card" aria-live="polite" hidden></section>
|
|
@@ -241,6 +248,27 @@
|
|
|
241
248
|
<div id="github-issues" class="models-body github-body" role="tabpanel" aria-labelledby="github-view-issues" hidden></div>
|
|
242
249
|
<div id="github-runs" class="models-body github-body" role="tabpanel" aria-labelledby="github-view-actions" hidden></div>
|
|
243
250
|
<div id="github-pulls" class="models-body github-body" role="tabpanel" aria-labelledby="github-view-pulls" hidden></div>
|
|
251
|
+
<div id="github-branches" class="models-body github-body" role="tabpanel" aria-labelledby="github-view-branches" hidden>
|
|
252
|
+
<p id="github-branches-pick" class="github-view-note">Pick a repository above to see its branches.</p>
|
|
253
|
+
<div id="github-branches-content" hidden>
|
|
254
|
+
<div class="github-view-tools">
|
|
255
|
+
<button id="github-branches-refresh" class="btn" type="button">Refresh</button>
|
|
256
|
+
<span class="github-spacer"></span>
|
|
257
|
+
<a id="github-branches-out" class="btn github-out" target="_blank" rel="noopener noreferrer" aria-description="Opens in a new tab">GitHub</a>
|
|
258
|
+
</div>
|
|
259
|
+
<div class="github-branch-filter">
|
|
260
|
+
<input id="github-branches-filter" type="search" placeholder="Filter branches" aria-label="Filter branches" spellcheck="false" autocomplete="off">
|
|
261
|
+
<button id="github-branches-clear" class="btn" type="button" disabled>Clear</button>
|
|
262
|
+
</div>
|
|
263
|
+
<p id="github-branches-status" class="github-view-note" role="status" aria-live="polite" aria-atomic="true"></p>
|
|
264
|
+
<div id="github-branches-error" class="github-error" hidden>
|
|
265
|
+
<span role="alert"></span>
|
|
266
|
+
<button id="github-branches-retry" class="btn" type="button">Retry</button>
|
|
267
|
+
</div>
|
|
268
|
+
<p id="github-branches-metadata" class="github-view-note" hidden></p>
|
|
269
|
+
<div id="github-branches-list" class="history-list" role="list" aria-label="Remote branches"></div>
|
|
270
|
+
</div>
|
|
271
|
+
</div>
|
|
244
272
|
<div id="github-clone-into" class="history-form github-form">
|
|
245
273
|
<label for="github-parent">Clone into</label>
|
|
246
274
|
<div class="cwd-field single">
|
package/web/layout.js
CHANGED
|
@@ -73,3 +73,39 @@ export function bindSplitter(handle, { axis = () => 'x', start, move, end, step
|
|
|
73
73
|
end();
|
|
74
74
|
});
|
|
75
75
|
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Publishes the part of the window an on-screen keyboard leaves visible as
|
|
79
|
+
* `--<name>-viewport-top`, `--<name>-viewport-bottom` and `--<name>-viewport-height`
|
|
80
|
+
* on `element` while `shown()` holds. iOS Safari keeps fixed elements at their full
|
|
81
|
+
* height under its keyboard and pans the page beneath them, so they must fit themselves.
|
|
82
|
+
* `fitted(height)` runs after each update, with null while the element is not shown.
|
|
83
|
+
*/
|
|
84
|
+
export function bindVisibleViewport(element, name, { shown = () => !element.hidden, watch = [element], fitted = () => {} } = {}) {
|
|
85
|
+
const viewport = window.visualViewport;
|
|
86
|
+
const props = ['top', 'bottom', 'height'].map((edge) => `--${name}-viewport-${edge}`);
|
|
87
|
+
let frame;
|
|
88
|
+
const update = () => {
|
|
89
|
+
if (!shown()) {
|
|
90
|
+
for (const prop of props) element.style.removeProperty(prop);
|
|
91
|
+
return fitted(null);
|
|
92
|
+
}
|
|
93
|
+
// Magnification belongs to the browser; it must not resize the layout.
|
|
94
|
+
if (viewport && Math.abs(viewport.scale - 1) > 0.01) return;
|
|
95
|
+
const top = viewport?.offsetTop || 0;
|
|
96
|
+
const height = viewport?.height ?? innerHeight;
|
|
97
|
+
element.style.setProperty(props[0], `${top}px`);
|
|
98
|
+
element.style.setProperty(props[1], `${Math.max(0, innerHeight - top - height)}px`);
|
|
99
|
+
element.style.setProperty(props[2], `${height}px`);
|
|
100
|
+
fitted(height);
|
|
101
|
+
};
|
|
102
|
+
const schedule = () => { cancelAnimationFrame(frame); frame = requestAnimationFrame(update); };
|
|
103
|
+
viewport?.addEventListener('resize', schedule);
|
|
104
|
+
viewport?.addEventListener('scroll', schedule);
|
|
105
|
+
addEventListener('resize', schedule);
|
|
106
|
+
addEventListener('pageshow', schedule);
|
|
107
|
+
const observer = new MutationObserver(schedule);
|
|
108
|
+
for (const watched of watch) observer.observe(watched, { attributes: true, attributeFilter: ['hidden'] });
|
|
109
|
+
schedule();
|
|
110
|
+
return schedule;
|
|
111
|
+
}
|
package/web/styles.css
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
--icon-sun: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round'%3E%3Ccircle cx='12' cy='12' r='4.2'/%3E%3Cpath d='M12 2.5v2.2M12 19.3v2.2M2.5 12h2.2M19.3 12h2.2M5.3 5.3l1.6 1.6M17.1 17.1l1.6 1.6M5.3 18.7l1.6-1.6M17.1 6.9l1.6-1.6'/%3E%3C/svg%3E");
|
|
24
24
|
--icon-moon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linejoin='round'%3E%3Cpath d='M19.5 14.6A8 8 0 0 1 9.4 4.5a8 8 0 1 0 10.1 10.1z'/%3E%3C/svg%3E");
|
|
25
25
|
--icon-mic: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='9' y='3' width='6' height='11' rx='3'/%3E%3Cpath d='M5.5 11a6.5 6.5 0 0 0 13 0M12 17.5V21'/%3E%3C/svg%3E");
|
|
26
|
+
--icon-power: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round'%3E%3Cpath d='M12 3v8.5'/%3E%3Cpath d='M7 6.2a7.5 7.5 0 1 0 10 0'/%3E%3C/svg%3E");
|
|
26
27
|
--icon-sound: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 9.5h3.5L12 5.5v13l-4.5-4H4z'/%3E%3Cpath d='M15.5 9a4.2 4.2 0 0 1 0 6M18.3 6.5a8 8 0 0 1 0 11'/%3E%3C/svg%3E");
|
|
27
28
|
--icon-external:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M7 17L17 7M9 7h8v8'/%3E%3C/svg%3E");
|
|
28
29
|
--icon-cloud: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linejoin='round'%3E%3Cpath d='M7 18.5h10.5a4 4 0 0 0 .6-7.96A6 6 0 0 0 6.5 9.6 4.5 4.5 0 0 0 7 18.5z'/%3E%3C/svg%3E");
|
|
@@ -392,7 +393,10 @@ main { max-width: calc(1200px + var(--dock-space, 0px)); margin: 0 auto; padding
|
|
|
392
393
|
.settings-menu .choice > .mode-dark::before { mask-image: var(--icon-moon); }
|
|
393
394
|
.settings-menu .choice > .sound-alerts::before { mask-image: var(--icon-sound); }
|
|
394
395
|
.settings-menu .choice > .voice-input::before { mask-image: var(--icon-mic); }
|
|
396
|
+
.settings-menu .choice > .autostart::before { mask-image: var(--icon-power); }
|
|
395
397
|
.voice-note { margin: 0 8px 6px; max-width: 26ch; color: var(--muted); font-size: 0.75rem; line-height: 1.35; }
|
|
398
|
+
.autostart-run { overflow-wrap: anywhere; }
|
|
399
|
+
.autostart-run[data-outcome="failed"] { color: var(--danger); }
|
|
396
400
|
.notes-open::before { mask-image: var(--icon-notes); }
|
|
397
401
|
.notes-open[aria-pressed="true"], .github-toggle[aria-pressed="true"] { border-color: var(--accent); color: var(--accent); }
|
|
398
402
|
.menu-button::after { margin-left: 0.45em; transform: rotate(90deg); transition: transform 0.15s var(--ease); }
|
|
@@ -872,7 +876,7 @@ a.news-link:visited { color: var(--muted); }
|
|
|
872
876
|
.github-combo input:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }
|
|
873
877
|
.github-combo input::placeholder { font-weight: 400; }
|
|
874
878
|
.github-repo-list {
|
|
875
|
-
position: absolute; z-index: 5; left: 0; right: 0; top: calc(100% + 4px); max-height: min(26rem, 60dvh); overflow: auto;
|
|
879
|
+
position: absolute; z-index: 5; left: 0; right: 0; top: calc(100% + 4px); max-height: min(26rem, 60dvh, var(--picker-room, 26rem)); overflow: auto;
|
|
876
880
|
margin: 0; padding: 4px; list-style: none; background: var(--surface); color: var(--text);
|
|
877
881
|
border: 1px solid var(--border); border-radius: var(--radius); box-shadow: 0 12px 32px rgba(0, 0, 0, 0.28);
|
|
878
882
|
}
|
|
@@ -898,10 +902,27 @@ a.news-link:visited { color: var(--muted); }
|
|
|
898
902
|
.github-view[aria-selected="true"] { color: var(--heading); background: var(--surface-2); border-color: var(--border); border-bottom-color: var(--surface-2); }
|
|
899
903
|
.github-view:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; }
|
|
900
904
|
.github-view .short { display: none; }
|
|
901
|
-
@container (max-width:
|
|
905
|
+
@container (max-width: 440px) {
|
|
902
906
|
.github-view .long { display: none; }
|
|
903
907
|
.github-view .short { display: inline; }
|
|
904
908
|
}
|
|
909
|
+
#github-branches { overflow-anchor: none; }
|
|
910
|
+
.github-branch-filter { display: flex; align-items: center; gap: 6px; }
|
|
911
|
+
.github-branch-filter input {
|
|
912
|
+
flex: 1; min-width: 0; padding: 7px 10px; border: 1px solid var(--border); border-radius: var(--radius);
|
|
913
|
+
background: var(--surface); color: var(--text); font: inherit; font-size: 0.85rem;
|
|
914
|
+
}
|
|
915
|
+
.github-branch-filter input:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }
|
|
916
|
+
.github-branch .history-title { display: flex; flex-wrap: wrap; gap: 4px 8px; align-items: baseline; }
|
|
917
|
+
.github-branch .github-record-title { min-width: 0; overflow-wrap: anywhere; }
|
|
918
|
+
.github-branch-badges { display: inline-flex; flex-wrap: wrap; gap: 4px; }
|
|
919
|
+
.github-branch .history-meta { font-family: var(--font-mono, monospace); }
|
|
920
|
+
.github-branch > .btn { align-self: start; }
|
|
921
|
+
#github-branches-error .btn { margin-left: 6px; }
|
|
922
|
+
@media (pointer: coarse) {
|
|
923
|
+
.github-branch-filter input { font-size: 16px; }
|
|
924
|
+
.github-branch > .btn, #github-branches .github-view-tools .btn, .github-branch-filter .btn { min-height: 36px; }
|
|
925
|
+
}
|
|
905
926
|
.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
927
|
.github-body[hidden] { display: none; }
|
|
907
928
|
.github-view-tools { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; padding-bottom: 6px; }
|
|
@@ -944,7 +965,8 @@ a.btn.github-out::after {
|
|
|
944
965
|
.toast .toast-action { margin-left: 12px; padding: 2px 10px; background: transparent; box-shadow: none; color: inherit; border-color: currentColor; }
|
|
945
966
|
|
|
946
967
|
.dock {
|
|
947
|
-
|
|
968
|
+
/* Above an on-screen keyboard, which the page publishes as --dock-viewport-*. */
|
|
969
|
+
position: fixed; top: max(var(--topbar-h, 56px), var(--dock-viewport-top, 0px)); right: 0; bottom: var(--dock-viewport-bottom, 0px); z-index: 12; width: var(--dock-w, 420px); max-width: 100vw;
|
|
948
970
|
display: flex; flex-direction: column; background: var(--surface); color: var(--text);
|
|
949
971
|
border-left: 1px solid var(--border); box-shadow: -16px 0 48px rgba(0, 0, 0, 0.25);
|
|
950
972
|
}
|
package/web/terminal-controls.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { bindVisibleViewport } from './layout.js';
|
|
2
|
+
|
|
1
3
|
const ARROWS = { ArrowUp: 'A', ArrowDown: 'B', ArrowRight: 'C', ArrowLeft: 'D' };
|
|
2
4
|
|
|
3
5
|
/** Use the same cursor mode as a physical keyboard, including inside full-screen tools. */
|
|
@@ -102,29 +104,9 @@ export class TerminalControls {
|
|
|
102
104
|
|
|
103
105
|
/** Fit the touch panel to a docked keyboard without changing global page layout. */
|
|
104
106
|
export function bindTerminalViewport(panel, controls) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
panel.style.removeProperty('--terminal-viewport-bottom');
|
|
111
|
-
delete panel.dataset.compact;
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
// Magnification belongs to the browser; it must not resize the shared PTY.
|
|
115
|
-
if (viewport && Math.abs(viewport.scale - 1) > 0.01) return;
|
|
116
|
-
const top = viewport?.offsetTop || 0;
|
|
117
|
-
const height = viewport?.height ?? innerHeight;
|
|
118
|
-
panel.style.setProperty('--terminal-viewport-top', `${top}px`);
|
|
119
|
-
panel.style.setProperty('--terminal-viewport-bottom', `${Math.max(0, innerHeight - top - height)}px`);
|
|
120
|
-
panel.toggleAttribute('data-compact', height < 420);
|
|
121
|
-
};
|
|
122
|
-
const schedule = () => { cancelAnimationFrame(frame); frame = requestAnimationFrame(update); };
|
|
123
|
-
viewport?.addEventListener('resize', schedule);
|
|
124
|
-
viewport?.addEventListener('scroll', schedule);
|
|
125
|
-
addEventListener('resize', schedule);
|
|
126
|
-
addEventListener('pageshow', schedule);
|
|
127
|
-
const observer = new MutationObserver(schedule);
|
|
128
|
-
for (const element of [panel, controls]) observer.observe(element, { attributes: true, attributeFilter: ['hidden'] });
|
|
129
|
-
schedule();
|
|
107
|
+
bindVisibleViewport(panel, 'terminal', {
|
|
108
|
+
shown: () => !panel.hidden && !controls.hidden,
|
|
109
|
+
watch: [panel, controls],
|
|
110
|
+
fitted: (height) => panel.toggleAttribute('data-compact', height !== null && height < 420),
|
|
111
|
+
});
|
|
130
112
|
}
|