detached e9c6360

#29 feat(web): merge merge requests asynchronously with htmx

state
merged · cf26365 · squash
branch
feat/mr-async-merge → main
opened
bnema ·
updated

The merge request Merge action now runs through htmx for every mode (fast-forward, squash, merge) instead of a full-page POST and 303 reload.

  • The form posts asynchronously, drops double submits and shows a pending indicator; without JS it still posts normally.
  • htmx merges get OOB fragments (state badge + action area). Refusals, failures, expired sessions and rejected origins render a status in the merge box.
  • Other open tabs of the same MR receive the merged state through the live stream.
  • The checks-label observer watches the stable MR bar so it survives swaps.

Deferred: the fragment path still builds the full page view (diff, comments) before rendering only state/actions; a dedicated state view builder is a follow-up.

Checks: go test ./..., go vet, node jstest suite (198 pass). Not manually tested in a browser.

CommitSubjectAuthorDateSignatureStatus
e9c6360fix(web): show session refusals in the async merge box and keep a single merge error alertbriceGPG Verified
5fea81ffeat(web): merge merge requests asynchronously with htmx and live state updatesbriceGPG Verified

Checks 4

64 files · +1924 -2040
4/4
M docs/issues.md +1 -3
M docs/releases.md +5 -44
M go.mod +5 -5
M go.sum +8 -8
M internal/adapters/web/fragments.go +15 -0
M internal/adapters/web/inbox.go +3 -25
M internal/adapters/web/inbox.templ +1 -34
M internal/adapters/web/inbox_templ.go +173 -451
M internal/adapters/web/inbox_test.go +4 -6
D internal/adapters/web/jstest/keys.test.mjs +0 -29
M internal/adapters/web/jstest/mr.test.mjs +12 -4
M internal/adapters/web/keys.templ +0 -1
M internal/adapters/web/keys_templ.go +1 -1
M internal/adapters/web/mr.go +7 -0
M internal/adapters/web/mr.templ +62 -29
A internal/adapters/web/mr_async_test.go +166 -0
M internal/adapters/web/mr_post.go +33 -1
M internal/adapters/web/mr_templ.go +1226 -1034
M internal/adapters/web/overlays.templ +0 -3
M internal/adapters/web/overlays_templ.go +39 -49
M internal/adapters/web/session.go +12 -2
M internal/adapters/web/shell.templ +1 -5
M internal/adapters/web/shell_templ.go +133 -152
M internal/adapters/web/static/css/60-mr.css +2 -0
M internal/adapters/web/static/js/ermite.js +2 -2
M internal/adapters/web/static/js/mr.js +3 -3
@@ -293,8 +293,8 @@ window.ermiteMR = (() => {
293293 let observer;
294294 const summary = $('#mr-ck');
295295 if (summary) {
296- // The live stream replaces #mr-ck as a whole: watch only the children of
297- // its parent, and write the label only when it changes. hx-live rewrites
296+ // Both #mr-ck and #mr-actions can be replaced: observe the stable bar,
297+ // and write the label only when it changes. hx-live rewrites
298298 // other text in the same bar on every mutation of the document; a write
299299 // here on each of them would wake hx-live again, a loop that never yields.
300300 const sync = () => {
@@ -303,7 +303,7 @@ window.ermiteMR = (() => {
303303 if (label && label.textContent !== text) label.textContent = text;
304304 };
305305 observer = new MutationObserver(sync);
306- observer.observe(summary.parentElement, { childList: true });
306+ observer.observe(summary.closest('.mr-bar'), { childList: true, subtree: true });
307307 sync();
308308 }
309309 show(initial);
@@ -293,8 +293,8 @@ window.ermiteMR = (() => {
293 let observer;293 let observer;
294 const summary = $('#mr-ck');294 const summary = $('#mr-ck');
295 if (summary) {295 if (summary) {
296- // The live stream replaces #mr-ck as a whole: watch only the children of296+ // Both #mr-ck and #mr-actions can be replaced: observe the stable bar,
297- // its parent, and write the label only when it changes. hx-live rewrites297+ // and write the label only when it changes. hx-live rewrites
298 // other text in the same bar on every mutation of the document; a write298 // other text in the same bar on every mutation of the document; a write
299 // here on each of them would wake hx-live again, a loop that never yields.299 // here on each of them would wake hx-live again, a loop that never yields.
300 const sync = () => {300 const sync = () => {
@@ -303,7 +303,7 @@ window.ermiteMR = (() => {
303 if (label && label.textContent !== text) label.textContent = text;303 if (label && label.textContent !== text) label.textContent = text;
304 };304 };
305 observer = new MutationObserver(sync);305 observer = new MutationObserver(sync);
306- observer.observe(summary.parentElement, { childList: true });306+ observer.observe(summary.closest('.mr-bar'), { childList: true, subtree: true });
307 sync();307 sync();
308 }308 }
309 show(initial);309 show(initial);
M internal/app/forge.go +1 -1
@@ -412,7 +412,7 @@ func newForge(ctx context.Context, cfg config.Config, opts options) (_ *forge, e
412412 BaseDifficulty: opts.issuePoW, MaxDifficulty: opts.issuePoW, MinFillTime: fastFill(opts.issuePoW),
413413 })
414414 f.Inbox = usecase.NewInbox(usecase.InboxConfig{
415- Meta: store, MergeRequests: store.MergeRequests(), Issues: store.Issues(), Pending: store.PendingIssues(), CI: store.CI(),
415+ Meta: store, MergeRequests: store.MergeRequests(), Issues: store.Issues(), Pending: store.PendingIssues(),
416416 })
417417 f.CI.SetPullGate(f.MergeRequests)
418418 f.AutoMerge.Start()
@@ -412,7 +412,7 @@ func newForge(ctx context.Context, cfg config.Config, opts options) (_ *forge, e
412 BaseDifficulty: opts.issuePoW, MaxDifficulty: opts.issuePoW, MinFillTime: fastFill(opts.issuePoW),412 BaseDifficulty: opts.issuePoW, MaxDifficulty: opts.issuePoW, MinFillTime: fastFill(opts.issuePoW),
413 })413 })
414 f.Inbox = usecase.NewInbox(usecase.InboxConfig{414 f.Inbox = usecase.NewInbox(usecase.InboxConfig{
415- Meta: store, MergeRequests: store.MergeRequests(), Issues: store.Issues(), Pending: store.PendingIssues(), CI: store.CI(),415+ Meta: store, MergeRequests: store.MergeRequests(), Issues: store.Issues(), Pending: store.PendingIssues(),
416 })416 })
417 f.CI.SetPullGate(f.MergeRequests)417 f.CI.SetPullGate(f.MergeRequests)
418 f.AutoMerge.Start()418 f.AutoMerge.Start()
M internal/app/testdata/web/blob-binary.html +0 -1
@@ -428,7 +428,6 @@
428428 k-r from:body -> ermite.tab('r');
429429 k-m from:body -> ermite.tab('m');
430430 k-i from:body -> ermite.tab('i');
431- k-inbox from:body -> ermite.go('[data-inbox]');
432431 k-next from:body -> ermite.nextNote();
433432 k-split from:body -> ermite.layout('split');
434433 k-unified from:body -> ermite.layout('unified');
@@ -428,7 +428,6 @@
428 k-r from:body -> ermite.tab('r');428 k-r from:body -> ermite.tab('r');
429 k-m from:body -> ermite.tab('m');429 k-m from:body -> ermite.tab('m');
430 k-i from:body -> ermite.tab('i');430 k-i from:body -> ermite.tab('i');
431- k-inbox from:body -> ermite.go('[data-inbox]');
432 k-next from:body -> ermite.nextNote();431 k-next from:body -> ermite.nextNote();
433 k-split from:body -> ermite.layout('split');432 k-split from:body -> ermite.layout('split');
434 k-unified from:body -> ermite.layout('unified');433 k-unified from:body -> ermite.layout('unified');
M internal/app/testdata/web/blob-code.html +0 -1
@@ -498,7 +498,6 @@
498498 k-r from:body -> ermite.tab('r');
499499 k-m from:body -> ermite.tab('m');
500500 k-i from:body -> ermite.tab('i');
501- k-inbox from:body -> ermite.go('[data-inbox]');
502501 k-next from:body -> ermite.nextNote();
503502 k-split from:body -> ermite.layout('split');
504503 k-unified from:body -> ermite.layout('unified');
@@ -498,7 +498,6 @@
498 k-r from:body -> ermite.tab('r');498 k-r from:body -> ermite.tab('r');
499 k-m from:body -> ermite.tab('m');499 k-m from:body -> ermite.tab('m');
500 k-i from:body -> ermite.tab('i');500 k-i from:body -> ermite.tab('i');
501- k-inbox from:body -> ermite.go('[data-inbox]');
502 k-next from:body -> ermite.nextNote();501 k-next from:body -> ermite.nextNote();
503 k-split from:body -> ermite.layout('split');502 k-split from:body -> ermite.layout('split');
504 k-unified from:body -> ermite.layout('unified');503 k-unified from:body -> ermite.layout('unified');
M internal/app/testdata/web/blob-empty.html +0 -1
@@ -428,7 +428,6 @@
428428 k-r from:body -> ermite.tab('r');
429429 k-m from:body -> ermite.tab('m');
430430 k-i from:body -> ermite.tab('i');
431- k-inbox from:body -> ermite.go('[data-inbox]');
432431 k-next from:body -> ermite.nextNote();
433432 k-split from:body -> ermite.layout('split');
434433 k-unified from:body -> ermite.layout('unified');
@@ -428,7 +428,6 @@
428 k-r from:body -> ermite.tab('r');428 k-r from:body -> ermite.tab('r');
429 k-m from:body -> ermite.tab('m');429 k-m from:body -> ermite.tab('m');
430 k-i from:body -> ermite.tab('i');430 k-i from:body -> ermite.tab('i');
431- k-inbox from:body -> ermite.go('[data-inbox]');
432 k-next from:body -> ermite.nextNote();431 k-next from:body -> ermite.nextNote();
433 k-split from:body -> ermite.layout('split');432 k-split from:body -> ermite.layout('split');
434 k-unified from:body -> ermite.layout('unified');433 k-unified from:body -> ermite.layout('unified');
M internal/app/testdata/web/blob-image.html +0 -1
@@ -430,7 +430,6 @@
430430 k-r from:body -> ermite.tab('r');
431431 k-m from:body -> ermite.tab('m');
432432 k-i from:body -> ermite.tab('i');
433- k-inbox from:body -> ermite.go('[data-inbox]');
434433 k-next from:body -> ermite.nextNote();
435434 k-split from:body -> ermite.layout('split');
436435 k-unified from:body -> ermite.layout('unified');
@@ -430,7 +430,6 @@
430 k-r from:body -> ermite.tab('r');430 k-r from:body -> ermite.tab('r');
431 k-m from:body -> ermite.tab('m');431 k-m from:body -> ermite.tab('m');
432 k-i from:body -> ermite.tab('i');432 k-i from:body -> ermite.tab('i');
433- k-inbox from:body -> ermite.go('[data-inbox]');
434 k-next from:body -> ermite.nextNote();433 k-next from:body -> ermite.nextNote();
435 k-split from:body -> ermite.layout('split');434 k-split from:body -> ermite.layout('split');
436 k-unified from:body -> ermite.layout('unified');435 k-unified from:body -> ermite.layout('unified');
M internal/app/testdata/web/blob-markdown.html +0 -1
@@ -495,7 +495,6 @@
495495 k-r from:body -> ermite.tab('r');
496496 k-m from:body -> ermite.tab('m');
497497 k-i from:body -> ermite.tab('i');
498- k-inbox from:body -> ermite.go('[data-inbox]');
499498 k-next from:body -> ermite.nextNote();
500499 k-split from:body -> ermite.layout('split');
501500 k-unified from:body -> ermite.layout('unified');
@@ -495,7 +495,6 @@
495 k-r from:body -> ermite.tab('r');495 k-r from:body -> ermite.tab('r');
496 k-m from:body -> ermite.tab('m');496 k-m from:body -> ermite.tab('m');
497 k-i from:body -> ermite.tab('i');497 k-i from:body -> ermite.tab('i');
498- k-inbox from:body -> ermite.go('[data-inbox]');
499 k-next from:body -> ermite.nextNote();498 k-next from:body -> ermite.nextNote();
500 k-split from:body -> ermite.layout('split');499 k-split from:body -> ermite.layout('split');
501 k-unified from:body -> ermite.layout('unified');500 k-unified from:body -> ermite.layout('unified');
M internal/app/testdata/web/blob-svg.html +0 -1
@@ -453,7 +453,6 @@
453453 k-r from:body -> ermite.tab('r');
454454 k-m from:body -> ermite.tab('m');
455455 k-i from:body -> ermite.tab('i');
456- k-inbox from:body -> ermite.go('[data-inbox]');
457456 k-next from:body -> ermite.nextNote();
458457 k-split from:body -> ermite.layout('split');
459458 k-unified from:body -> ermite.layout('unified');
@@ -453,7 +453,6 @@
453 k-r from:body -> ermite.tab('r');453 k-r from:body -> ermite.tab('r');
454 k-m from:body -> ermite.tab('m');454 k-m from:body -> ermite.tab('m');
455 k-i from:body -> ermite.tab('i');455 k-i from:body -> ermite.tab('i');
456- k-inbox from:body -> ermite.go('[data-inbox]');
457 k-next from:body -> ermite.nextNote();456 k-next from:body -> ermite.nextNote();
458 k-split from:body -> ermite.layout('split');457 k-split from:body -> ermite.layout('split');
459 k-unified from:body -> ermite.layout('unified');458 k-unified from:body -> ermite.layout('unified');
M internal/app/testdata/web/blob-text.html +0 -1
@@ -448,7 +448,6 @@
448448 k-r from:body -> ermite.tab('r');
449449 k-m from:body -> ermite.tab('m');
450450 k-i from:body -> ermite.tab('i');
451- k-inbox from:body -> ermite.go('[data-inbox]');
452451 k-next from:body -> ermite.nextNote();
453452 k-split from:body -> ermite.layout('split');
454453 k-unified from:body -> ermite.layout('unified');
@@ -448,7 +448,6 @@
448 k-r from:body -> ermite.tab('r');448 k-r from:body -> ermite.tab('r');
449 k-m from:body -> ermite.tab('m');449 k-m from:body -> ermite.tab('m');
450 k-i from:body -> ermite.tab('i');450 k-i from:body -> ermite.tab('i');
451- k-inbox from:body -> ermite.go('[data-inbox]');
452 k-next from:body -> ermite.nextNote();451 k-next from:body -> ermite.nextNote();
453 k-split from:body -> ermite.layout('split');452 k-split from:body -> ermite.layout('split');
454 k-unified from:body -> ermite.layout('unified');453 k-unified from:body -> ermite.layout('unified');
M internal/app/testdata/web/blob-too-large.html +0 -1
@@ -428,7 +428,6 @@
428428 k-r from:body -> ermite.tab('r');
429429 k-m from:body -> ermite.tab('m');
430430 k-i from:body -> ermite.tab('i');
431- k-inbox from:body -> ermite.go('[data-inbox]');
432431 k-next from:body -> ermite.nextNote();
433432 k-split from:body -> ermite.layout('split');
434433 k-unified from:body -> ermite.layout('unified');
@@ -428,7 +428,6 @@
428 k-r from:body -> ermite.tab('r');428 k-r from:body -> ermite.tab('r');
429 k-m from:body -> ermite.tab('m');429 k-m from:body -> ermite.tab('m');
430 k-i from:body -> ermite.tab('i');430 k-i from:body -> ermite.tab('i');
431- k-inbox from:body -> ermite.go('[data-inbox]');
432 k-next from:body -> ermite.nextNote();431 k-next from:body -> ermite.nextNote();
433 k-split from:body -> ermite.layout('split');432 k-split from:body -> ermite.layout('split');
434 k-unified from:body -> ermite.layout('unified');433 k-unified from:body -> ermite.layout('unified');
M internal/app/testdata/web/blob-trojan.html +0 -1
@@ -495,7 +495,6 @@
495495 k-r from:body -> ermite.tab('r');
496496 k-m from:body -> ermite.tab('m');
497497 k-i from:body -> ermite.tab('i');
498- k-inbox from:body -> ermite.go('[data-inbox]');
499498 k-next from:body -> ermite.nextNote();
500499 k-split from:body -> ermite.layout('split');
501500 k-unified from:body -> ermite.layout('unified');
@@ -495,7 +495,6 @@
495 k-r from:body -> ermite.tab('r');495 k-r from:body -> ermite.tab('r');
496 k-m from:body -> ermite.tab('m');496 k-m from:body -> ermite.tab('m');
497 k-i from:body -> ermite.tab('i');497 k-i from:body -> ermite.tab('i');
498- k-inbox from:body -> ermite.go('[data-inbox]');
499 k-next from:body -> ermite.nextNote();498 k-next from:body -> ermite.nextNote();
500 k-split from:body -> ermite.layout('split');499 k-split from:body -> ermite.layout('split');
501 k-unified from:body -> ermite.layout('unified');500 k-unified from:body -> ermite.layout('unified');
M internal/app/testdata/web/commit-big.html +0 -1
@@ -480,7 +480,6 @@
480480 k-r from:body -> ermite.tab('r');
481481 k-m from:body -> ermite.tab('m');
482482 k-i from:body -> ermite.tab('i');
483- k-inbox from:body -> ermite.go('[data-inbox]');
484483 k-next from:body -> ermite.nextNote();
485484 k-split from:body -> ermite.layout('split');
486485 k-unified from:body -> ermite.layout('unified');
@@ -480,7 +480,6 @@
480 k-r from:body -> ermite.tab('r');480 k-r from:body -> ermite.tab('r');
481 k-m from:body -> ermite.tab('m');481 k-m from:body -> ermite.tab('m');
482 k-i from:body -> ermite.tab('i');482 k-i from:body -> ermite.tab('i');
483- k-inbox from:body -> ermite.go('[data-inbox]');
484 k-next from:body -> ermite.nextNote();483 k-next from:body -> ermite.nextNote();
485 k-split from:body -> ermite.layout('split');484 k-split from:body -> ermite.layout('split');
486 k-unified from:body -> ermite.layout('unified');485 k-unified from:body -> ermite.layout('unified');
M internal/app/testdata/web/commit-binary.html +0 -1
@@ -471,7 +471,6 @@
471471 k-r from:body -> ermite.tab('r');
472472 k-m from:body -> ermite.tab('m');
473473 k-i from:body -> ermite.tab('i');
474- k-inbox from:body -> ermite.go('[data-inbox]');
475474 k-next from:body -> ermite.nextNote();
476475 k-split from:body -> ermite.layout('split');
477476 k-unified from:body -> ermite.layout('unified');
@@ -471,7 +471,6 @@
471 k-r from:body -> ermite.tab('r');471 k-r from:body -> ermite.tab('r');
472 k-m from:body -> ermite.tab('m');472 k-m from:body -> ermite.tab('m');
473 k-i from:body -> ermite.tab('i');473 k-i from:body -> ermite.tab('i');
474- k-inbox from:body -> ermite.go('[data-inbox]');
475 k-next from:body -> ermite.nextNote();474 k-next from:body -> ermite.nextNote();
476 k-split from:body -> ermite.layout('split');475 k-split from:body -> ermite.layout('split');
477 k-unified from:body -> ermite.layout('unified');476 k-unified from:body -> ermite.layout('unified');
M internal/app/testdata/web/commit-merge.html +0 -1
@@ -513,7 +513,6 @@
513513 k-r from:body -> ermite.tab('r');
514514 k-m from:body -> ermite.tab('m');
515515 k-i from:body -> ermite.tab('i');
516- k-inbox from:body -> ermite.go('[data-inbox]');
517516 k-next from:body -> ermite.nextNote();
518517 k-split from:body -> ermite.layout('split');
519518 k-unified from:body -> ermite.layout('unified');
@@ -513,7 +513,6 @@
513 k-r from:body -> ermite.tab('r');513 k-r from:body -> ermite.tab('r');
514 k-m from:body -> ermite.tab('m');514 k-m from:body -> ermite.tab('m');
515 k-i from:body -> ermite.tab('i');515 k-i from:body -> ermite.tab('i');
516- k-inbox from:body -> ermite.go('[data-inbox]');
517 k-next from:body -> ermite.nextNote();516 k-next from:body -> ermite.nextNote();
518 k-split from:body -> ermite.layout('split');517 k-split from:body -> ermite.layout('split');
519 k-unified from:body -> ermite.layout('unified');518 k-unified from:body -> ermite.layout('unified');
M internal/app/testdata/web/commit-modify-split.html +0 -1
@@ -636,7 +636,6 @@ And &lt;b&gt;html&lt;/b&gt; &amp; &#34;quotes&#34;.</pre>
636636 k-r from:body -> ermite.tab('r');
637637 k-m from:body -> ermite.tab('m');
638638 k-i from:body -> ermite.tab('i');
639- k-inbox from:body -> ermite.go('[data-inbox]');
640639 k-next from:body -> ermite.nextNote();
641640 k-split from:body -> ermite.layout('split');
642641 k-unified from:body -> ermite.layout('unified');
@@ -636,7 +636,6 @@ And &lt;b&gt;html&lt;/b&gt; &amp; &#34;quotes&#34;.</pre>
636 k-r from:body -> ermite.tab('r');636 k-r from:body -> ermite.tab('r');
637 k-m from:body -> ermite.tab('m');637 k-m from:body -> ermite.tab('m');
638 k-i from:body -> ermite.tab('i');638 k-i from:body -> ermite.tab('i');
639- k-inbox from:body -> ermite.go('[data-inbox]');
640 k-next from:body -> ermite.nextNote();639 k-next from:body -> ermite.nextNote();
641 k-split from:body -> ermite.layout('split');640 k-split from:body -> ermite.layout('split');
642 k-unified from:body -> ermite.layout('unified');641 k-unified from:body -> ermite.layout('unified');
M internal/app/testdata/web/commit-modify.html +0 -1
@@ -636,7 +636,6 @@ And &lt;b&gt;html&lt;/b&gt; &amp; &#34;quotes&#34;.</pre>
636636 k-r from:body -> ermite.tab('r');
637637 k-m from:body -> ermite.tab('m');
638638 k-i from:body -> ermite.tab('i');
639- k-inbox from:body -> ermite.go('[data-inbox]');
640639 k-next from:body -> ermite.nextNote();
641640 k-split from:body -> ermite.layout('split');
642641 k-unified from:body -> ermite.layout('unified');
@@ -636,7 +636,6 @@ And &lt;b&gt;html&lt;/b&gt; &amp; &#34;quotes&#34;.</pre>
636 k-r from:body -> ermite.tab('r');636 k-r from:body -> ermite.tab('r');
637 k-m from:body -> ermite.tab('m');637 k-m from:body -> ermite.tab('m');
638 k-i from:body -> ermite.tab('i');638 k-i from:body -> ermite.tab('i');
639- k-inbox from:body -> ermite.go('[data-inbox]');
640 k-next from:body -> ermite.nextNote();639 k-next from:body -> ermite.nextNote();
641 k-split from:body -> ermite.layout('split');640 k-split from:body -> ermite.layout('split');
642 k-unified from:body -> ermite.layout('unified');641 k-unified from:body -> ermite.layout('unified');
M internal/app/testdata/web/commit-rename.html +0 -1
@@ -540,7 +540,6 @@
540540 k-r from:body -> ermite.tab('r');
541541 k-m from:body -> ermite.tab('m');
542542 k-i from:body -> ermite.tab('i');
543- k-inbox from:body -> ermite.go('[data-inbox]');
544543 k-next from:body -> ermite.nextNote();
545544 k-split from:body -> ermite.layout('split');
546545 k-unified from:body -> ermite.layout('unified');
@@ -540,7 +540,6 @@
540 k-r from:body -> ermite.tab('r');540 k-r from:body -> ermite.tab('r');
541 k-m from:body -> ermite.tab('m');541 k-m from:body -> ermite.tab('m');
542 k-i from:body -> ermite.tab('i');542 k-i from:body -> ermite.tab('i');
543- k-inbox from:body -> ermite.go('[data-inbox]');
544 k-next from:body -> ermite.nextNote();543 k-next from:body -> ermite.nextNote();
545 k-split from:body -> ermite.layout('split');544 k-split from:body -> ermite.layout('split');
546 k-unified from:body -> ermite.layout('unified');545 k-unified from:body -> ermite.layout('unified');
M internal/app/testdata/web/commit-root.html +0 -1
@@ -859,7 +859,6 @@
859859 k-r from:body -> ermite.tab('r');
860860 k-m from:body -> ermite.tab('m');
861861 k-i from:body -> ermite.tab('i');
862- k-inbox from:body -> ermite.go('[data-inbox]');
863862 k-next from:body -> ermite.nextNote();
864863 k-split from:body -> ermite.layout('split');
865864 k-unified from:body -> ermite.layout('unified');
@@ -859,7 +859,6 @@
859 k-r from:body -> ermite.tab('r');859 k-r from:body -> ermite.tab('r');
860 k-m from:body -> ermite.tab('m');860 k-m from:body -> ermite.tab('m');
861 k-i from:body -> ermite.tab('i');861 k-i from:body -> ermite.tab('i');
862- k-inbox from:body -> ermite.go('[data-inbox]');
863 k-next from:body -> ermite.nextNote();862 k-next from:body -> ermite.nextNote();
864 k-split from:body -> ermite.layout('split');863 k-split from:body -> ermite.layout('split');
865 k-unified from:body -> ermite.layout('unified');864 k-unified from:body -> ermite.layout('unified');
M internal/app/testdata/web/commit-trojan.html +0 -1
@@ -688,7 +688,6 @@
688688 k-r from:body -> ermite.tab('r');
689689 k-m from:body -> ermite.tab('m');
690690 k-i from:body -> ermite.tab('i');
691- k-inbox from:body -> ermite.go('[data-inbox]');
692691 k-next from:body -> ermite.nextNote();
693692 k-split from:body -> ermite.layout('split');
694693 k-unified from:body -> ermite.layout('unified');
@@ -688,7 +688,6 @@
688 k-r from:body -> ermite.tab('r');688 k-r from:body -> ermite.tab('r');
689 k-m from:body -> ermite.tab('m');689 k-m from:body -> ermite.tab('m');
690 k-i from:body -> ermite.tab('i');690 k-i from:body -> ermite.tab('i');
691- k-inbox from:body -> ermite.go('[data-inbox]');
692 k-next from:body -> ermite.nextNote();691 k-next from:body -> ermite.nextNote();
693 k-split from:body -> ermite.layout('split');692 k-split from:body -> ermite.layout('split');
694 k-unified from:body -> ermite.layout('unified');693 k-unified from:body -> ermite.layout('unified');
M internal/app/testdata/web/commits-page2.html +0 -1
@@ -441,7 +441,6 @@
441441 k-r from:body -> ermite.tab('r');
442442 k-m from:body -> ermite.tab('m');
443443 k-i from:body -> ermite.tab('i');
444- k-inbox from:body -> ermite.go('[data-inbox]');
445444 k-next from:body -> ermite.nextNote();
446445 k-split from:body -> ermite.layout('split');
447446 k-unified from:body -> ermite.layout('unified');
@@ -441,7 +441,6 @@
441 k-r from:body -> ermite.tab('r');441 k-r from:body -> ermite.tab('r');
442 k-m from:body -> ermite.tab('m');442 k-m from:body -> ermite.tab('m');
443 k-i from:body -> ermite.tab('i');443 k-i from:body -> ermite.tab('i');
444- k-inbox from:body -> ermite.go('[data-inbox]');
445 k-next from:body -> ermite.nextNote();444 k-next from:body -> ermite.nextNote();
446 k-split from:body -> ermite.layout('split');445 k-split from:body -> ermite.layout('split');
447 k-unified from:body -> ermite.layout('unified');446 k-unified from:body -> ermite.layout('unified');
M internal/app/testdata/web/commits-path.html +0 -1
@@ -456,7 +456,6 @@
456456 k-r from:body -> ermite.tab('r');
457457 k-m from:body -> ermite.tab('m');
458458 k-i from:body -> ermite.tab('i');
459- k-inbox from:body -> ermite.go('[data-inbox]');
460459 k-next from:body -> ermite.nextNote();
461460 k-split from:body -> ermite.layout('split');
462461 k-unified from:body -> ermite.layout('unified');
@@ -456,7 +456,6 @@
456 k-r from:body -> ermite.tab('r');456 k-r from:body -> ermite.tab('r');
457 k-m from:body -> ermite.tab('m');457 k-m from:body -> ermite.tab('m');
458 k-i from:body -> ermite.tab('i');458 k-i from:body -> ermite.tab('i');
459- k-inbox from:body -> ermite.go('[data-inbox]');
460 k-next from:body -> ermite.nextNote();459 k-next from:body -> ermite.nextNote();
461 k-split from:body -> ermite.layout('split');460 k-split from:body -> ermite.layout('split');
462 k-unified from:body -> ermite.layout('unified');461 k-unified from:body -> ermite.layout('unified');
M internal/app/testdata/web/commits-root.html +0 -1
@@ -1246,7 +1246,6 @@
12461246 k-r from:body -> ermite.tab('r');
12471247 k-m from:body -> ermite.tab('m');
12481248 k-i from:body -> ermite.tab('i');
1249- k-inbox from:body -> ermite.go('[data-inbox]');
12501249 k-next from:body -> ermite.nextNote();
12511250 k-split from:body -> ermite.layout('split');
12521251 k-unified from:body -> ermite.layout('unified');
@@ -1246,7 +1246,6 @@
1246 k-r from:body -> ermite.tab('r');1246 k-r from:body -> ermite.tab('r');
1247 k-m from:body -> ermite.tab('m');1247 k-m from:body -> ermite.tab('m');
1248 k-i from:body -> ermite.tab('i');1248 k-i from:body -> ermite.tab('i');
1249- k-inbox from:body -> ermite.go('[data-inbox]');
1250 k-next from:body -> ermite.nextNote();1249 k-next from:body -> ermite.nextNote();
1251 k-split from:body -> ermite.layout('split');1250 k-split from:body -> ermite.layout('split');
1252 k-unified from:body -> ermite.layout('unified');1251 k-unified from:body -> ermite.layout('unified');
M internal/app/testdata/web/commits-small.html +0 -1
@@ -611,7 +611,6 @@
611611 k-r from:body -> ermite.tab('r');
612612 k-m from:body -> ermite.tab('m');
613613 k-i from:body -> ermite.tab('i');
614- k-inbox from:body -> ermite.go('[data-inbox]');
615614 k-next from:body -> ermite.nextNote();
616615 k-split from:body -> ermite.layout('split');
617616 k-unified from:body -> ermite.layout('unified');
@@ -611,7 +611,6 @@
611 k-r from:body -> ermite.tab('r');611 k-r from:body -> ermite.tab('r');
612 k-m from:body -> ermite.tab('m');612 k-m from:body -> ermite.tab('m');
613 k-i from:body -> ermite.tab('i');613 k-i from:body -> ermite.tab('i');
614- k-inbox from:body -> ermite.go('[data-inbox]');
615 k-next from:body -> ermite.nextNote();614 k-next from:body -> ermite.nextNote();
616 k-split from:body -> ermite.layout('split');615 k-split from:body -> ermite.layout('split');
617 k-unified from:body -> ermite.layout('unified');616 k-unified from:body -> ermite.layout('unified');
M internal/app/testdata/web/empty.html +0 -1
@@ -284,7 +284,6 @@
284284 k-r from:body -> ermite.tab('r');
285285 k-m from:body -> ermite.tab('m');
286286 k-i from:body -> ermite.tab('i');
287- k-inbox from:body -> ermite.go('[data-inbox]');
288287 k-next from:body -> ermite.nextNote();
289288 k-split from:body -> ermite.layout('split');
290289 k-unified from:body -> ermite.layout('unified');
@@ -284,7 +284,6 @@
284 k-r from:body -> ermite.tab('r');284 k-r from:body -> ermite.tab('r');
285 k-m from:body -> ermite.tab('m');285 k-m from:body -> ermite.tab('m');
286 k-i from:body -> ermite.tab('i');286 k-i from:body -> ermite.tab('i');
287- k-inbox from:body -> ermite.go('[data-inbox]');
288 k-next from:body -> ermite.nextNote();287 k-next from:body -> ermite.nextNote();
289 k-split from:body -> ermite.layout('split');288 k-split from:body -> ermite.layout('split');
290 k-unified from:body -> ermite.layout('unified');289 k-unified from:body -> ermite.layout('unified');
M internal/app/testdata/web/home.html +0 -1
@@ -394,7 +394,6 @@
394394 k-r from:body -> ermite.tab('r');
395395 k-m from:body -> ermite.tab('m');
396396 k-i from:body -> ermite.tab('i');
397- k-inbox from:body -> ermite.go('[data-inbox]');
398397 k-next from:body -> ermite.nextNote();
399398 k-split from:body -> ermite.layout('split');
400399 k-unified from:body -> ermite.layout('unified');
@@ -394,7 +394,6 @@
394 k-r from:body -> ermite.tab('r');394 k-r from:body -> ermite.tab('r');
395 k-m from:body -> ermite.tab('m');395 k-m from:body -> ermite.tab('m');
396 k-i from:body -> ermite.tab('i');396 k-i from:body -> ermite.tab('i');
397- k-inbox from:body -> ermite.go('[data-inbox]');
398 k-next from:body -> ermite.nextNote();397 k-next from:body -> ermite.nextNote();
399 k-split from:body -> ermite.layout('split');398 k-split from:body -> ermite.layout('split');
400 k-unified from:body -> ermite.layout('unified');399 k-unified from:body -> ermite.layout('unified');
M internal/app/testdata/web/not-found-page.html +0 -1
M internal/app/testdata/web/not-found.html +0 -1
M internal/app/testdata/web/refs.html +0 -1
M internal/app/testdata/web/summary.html +0 -1
M internal/app/testdata/web/tree-nested.html +0 -1
M internal/app/testdata/web/tree-root.html +0 -1
M internal/app/testdata/web/tree-slash-ref.html +0 -1
M internal/app/testdata/web/tree-subdir.html +0 -1
M internal/app/web_inbox_test.go +2 -32
M internal/ports/dto.go +3 -6
M internal/usecase/events.go +2 -2
M internal/usecase/inbox.go +2 -31
M internal/usecase/inbox_test.go +0 -33
M skills/ermite/SKILL.md +0 -13

Comments0 open / 0

no comments yet.

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
l
merge request: the list of files (j k move, enter opens, esc back)
M
merge request: the merge menu; there 1–3 pick the mode, D deletes the branch, enter merges
R
merge request: mark the head read
esc
close a dialog, leave a field

Branches 2

Tags 42

↑↓ moveenter openesc close