detached 302b124

#28 feat(web): list running CI runs in the inbox and open it with Shift+I

state
merged · 801147f · squash
branch
feat/inbox-runs → main
opened
bnema ·
updated

The owner's inbox lists queued and in-progress CI runs in a Running section (after Awaiting moderation), with a running count, the ?repo= filter, and live refresh on run status changes. CI off: no section.

Shift+I opens the inbox from any page for the signed-in owner; lowercase i still opens the issues tab. The help dialog and the inbox button announce it.

Checks: go build, go vet, go test ./internal/..., node --test jstest (199 pass). Wrap-up review (go + js) done; one a11y finding fixed.

CommitSubjectAuthorDateSignatureStatus
302b124feat(web): announce the Shift+I shortcut on the inbox buttonbriceGPG Verified
0f18a24feat(web): list running CI runs in the inbox and open it with Shift+IbriceGPG Verified

Checks 4

52 files · +902 -425
4/4
M docs/issues.md +3 -1
@@ -50,7 +50,9 @@ Limits: the 100 most recent commits of the merge request, 16 KiB of each text, 1
5050
5151 The signed-in owner has an Inbox at `/-/inbox`, reached from the **Inbox** button at the top left of the home page, which shows the number of open items. It lists the open merge requests and the open issues of every repository, most recently updated first. The counters at the top (`N merge requests · M issues`) always count everything; `?repo=<name>` restricts the two lists to one repository, and the repository links under the counters set it. Repositories with issues off contribute no issues.
5252
53-The page updates live: merge request and issue events of any repository refresh the counters and the lists. Anyone who is not the signed-in owner, including a token, gets the same 404 as for an unknown page, and the same answer for the `owner` event stream it follows.
53+The **Running** section, after **Awaiting moderation**, lists queued and in-progress CI runs newest first, with links to each run, its repository, ref, and start time. The `N running` counter counts every repository; the repository filter also applies to this section. **Shift+I** (`I`) opens the inbox from any page for the signed-in owner, unless single-key shortcuts are off; lowercase `i` still opens the issues tab.
54+
55+The page updates live: run status changes, merge request and issue events of any repository refresh the counters and the lists. Anyone who is not the signed-in owner, including a token, gets the same 404 as for an unknown page, and the same answer for the `owner` event stream it follows.
5456
5557 ## Public form and moderation
5658
@@ -50,7 +50,9 @@ Limits: the 100 most recent commits of the merge request, 16 KiB of each text, 1
50 50
51 The signed-in owner has an Inbox at `/-/inbox`, reached from the **Inbox** button at the top left of the home page, which shows the number of open items. It lists the open merge requests and the open issues of every repository, most recently updated first. The counters at the top (`N merge requests · M issues`) always count everything; `?repo=<name>` restricts the two lists to one repository, and the repository links under the counters set it. Repositories with issues off contribute no issues.51 The signed-in owner has an Inbox at `/-/inbox`, reached from the **Inbox** button at the top left of the home page, which shows the number of open items. It lists the open merge requests and the open issues of every repository, most recently updated first. The counters at the top (`N merge requests · M issues`) always count everything; `?repo=<name>` restricts the two lists to one repository, and the repository links under the counters set it. Repositories with issues off contribute no issues.
52 52
53-The page updates live: merge request and issue events of any repository refresh the counters and the lists. Anyone who is not the signed-in owner, including a token, gets the same 404 as for an unknown page, and the same answer for the `owner` event stream it follows.53+The **Running** section, after **Awaiting moderation**, lists queued and in-progress CI runs newest first, with links to each run, its repository, ref, and start time. The `N running` counter counts every repository; the repository filter also applies to this section. **Shift+I** (`I`) opens the inbox from any page for the signed-in owner, unless single-key shortcuts are off; lowercase `i` still opens the issues tab.
54+
55+The page updates live: run status changes, merge request and issue events of any repository refresh the counters and the lists. Anyone who is not the signed-in owner, including a token, gets the same 404 as for an unknown page, and the same answer for the `owner` event stream it follows.
54 56
55 ## Public form and moderation57 ## Public form and moderation
56 58
M docs/releases.md +5 -44
@@ -57,60 +57,21 @@ the S3 endpoint. Stored-token publishers need `release:write` and, for private
5757 repository reads, `repo:read`. The owner component is `ERMITE_OWNER` (`owner` by
5858 default).
5959
60-GoReleaser does not read `GITHUB_API_URL` or `GITHUB_SERVER_URL` by itself, so
61-template `github_urls` from them (replace the fallback host and repository):
60+GoReleaser configuration (replace the host and repository):
6261
6362 ```yaml
6463 version: 2
65-# In CI the runner sets GITHUB_API_URL and GITHUB_SERVER_URL to the address the
66-# job can reach; the public URLs are the fallback for a local run.
6764 github_urls:
68- api: '{{ or (index .Env "GITHUB_API_URL") "https://git.example.org/-/github/api" }}/'
69- upload: '{{ or (index .Env "GITHUB_API_URL") "https://git.example.org/-/github/api" }}/uploads/'
70- download: '{{ or (index .Env "GITHUB_SERVER_URL") "https://git.example.org" }}/'
65+ api: https://git.example.org/-/github/api/
66+ upload: https://git.example.org/-/github/api/uploads/
67+ download: https://git.example.org/
7168 release:
7269 github:
7370 owner: owner
7471 name: app
7572 ```
7673
77-In a CI job the runner rewrites the server and API URLs to an origin the job
78-can reach, which may differ from the public URL (for example `http://ermite:7341`
79-with a relayed forge). Sandboxes block private and CGNAT ranges, so a job that
80-targets only the public URL times out (`dial tcp 100.x:443: i/o timeout`). A
81-plain-http relay needs no TLS option. The API base gets `api/v3/` appended by
82-the client; ermite accepts that alias. Locally, run
83-`GITHUB_TOKEN=<token> goreleaser release --clean` from a tagged checkout; the
84-fallbacks apply.
85-
86-Workflow:
87-
88-```yaml
89-name: release
90-on:
91- push:
92- tags: ['v*']
93-permissions:
94- contents: write
95-jobs:
96- release:
97- runs-on: ubuntu-latest
98- steps:
99- - uses: actions/checkout@v4
100- with:
101- fetch-depth: 0
102- - uses: actions/setup-go@v5
103- with:
104- go-version-file: go.mod
105- - uses: goreleaser/goreleaser-action@v6
106- with:
107- args: release --clean
108- env:
109- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
110-```
111-
112-An existing GitHub workflow must be enabled on ermite:
113-`ermite workflow enable <repo> .github/workflows/release.yml`.
74+Run `GITHUB_TOKEN=<token> goreleaser release --clean` from a tagged checkout.
11475
11576 On an ermite runner, the job supplies `GITHUB_API_URL`, `GITHUB_SERVER_URL` and
11677 `GITHUB_TOKEN`; no endpoint override is needed for action-gh-release:
@@ -57,60 +57,21 @@ the S3 endpoint. Stored-token publishers need `release:write` and, for private
57 repository reads, `repo:read`. The owner component is `ERMITE_OWNER` (`owner` by57 repository reads, `repo:read`. The owner component is `ERMITE_OWNER` (`owner` by
58 default).58 default).
59 59
60-GoReleaser does not read `GITHUB_API_URL` or `GITHUB_SERVER_URL` by itself, so60+GoReleaser configuration (replace the host and repository):
61-template `github_urls` from them (replace the fallback host and repository):
62 61
63 ```yaml62 ```yaml
64 version: 263 version: 2
65-# In CI the runner sets GITHUB_API_URL and GITHUB_SERVER_URL to the address the
66-# job can reach; the public URLs are the fallback for a local run.
67 github_urls:64 github_urls:
68- api: '{{ or (index .Env "GITHUB_API_URL") "https://git.example.org/-/github/api" }}/'65+ api: https://git.example.org/-/github/api/
69- upload: '{{ or (index .Env "GITHUB_API_URL") "https://git.example.org/-/github/api" }}/uploads/'66+ upload: https://git.example.org/-/github/api/uploads/
70- download: '{{ or (index .Env "GITHUB_SERVER_URL") "https://git.example.org" }}/'67+ download: https://git.example.org/
71 release:68 release:
72 github:69 github:
73 owner: owner70 owner: owner
74 name: app71 name: app
75 ```72 ```
76 73
77-In a CI job the runner rewrites the server and API URLs to an origin the job74+Run `GITHUB_TOKEN=<token> goreleaser release --clean` from a tagged checkout.
78-can reach, which may differ from the public URL (for example `http://ermite:7341`
79-with a relayed forge). Sandboxes block private and CGNAT ranges, so a job that
80-targets only the public URL times out (`dial tcp 100.x:443: i/o timeout`). A
81-plain-http relay needs no TLS option. The API base gets `api/v3/` appended by
82-the client; ermite accepts that alias. Locally, run
83-`GITHUB_TOKEN=<token> goreleaser release --clean` from a tagged checkout; the
84-fallbacks apply.
85-
86-Workflow:
87-
88-```yaml
89-name: release
90-on:
91- push:
92- tags: ['v*']
93-permissions:
94- contents: write
95-jobs:
96- release:
97- runs-on: ubuntu-latest
98- steps:
99- - uses: actions/checkout@v4
100- with:
101- fetch-depth: 0
102- - uses: actions/setup-go@v5
103- with:
104- go-version-file: go.mod
105- - uses: goreleaser/goreleaser-action@v6
106- with:
107- args: release --clean
108- env:
109- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
110-```
111-
112-An existing GitHub workflow must be enabled on ermite:
113-`ermite workflow enable <repo> .github/workflows/release.yml`.
114 75
115 On an ermite runner, the job supplies `GITHUB_API_URL`, `GITHUB_SERVER_URL` and76 On an ermite runner, the job supplies `GITHUB_API_URL`, `GITHUB_SERVER_URL` and
116 `GITHUB_TOKEN`; no endpoint override is needed for action-gh-release:77 `GITHUB_TOKEN`; no endpoint override is needed for action-gh-release:
M internal/adapters/web/inbox.go +25 -3
@@ -67,8 +67,17 @@ type InboxRepoLink struct {
6767 Current bool
6868 }
6969
70-// InboxPage is the inbox: the counters, the filter and the two lists.
70+// InboxRunRow adds repository and start time to the Actions run display.
71+type InboxRunRow struct {
72+ RunRow
73+ Repo, RepoHref, When, WhenAt string
74+}
75+
76+// InboxPage is the inbox: counters, repository filter and item lists.
7177 type InboxPage struct {
78+ Running int
79+ RunsEnabled bool
80+ Runs []InboxRunRow
7281 // MergeRequests and Issues count every open item, whatever the filter.
7382 MergeRequests, Issues int
7483 // PendingCount counts every issue awaiting moderation.
@@ -106,6 +115,19 @@ func inboxFilter(r *http.Request) (domain.RepoName, error) {
106115 func (s *Server) inboxPage(ov ports.InboxOverview, filter domain.RepoName) InboxPage {
107116 now := s.cfg.Now()
108117 p := InboxPage{MergeRequests: ov.Counts.MergeRequests, Issues: ov.Counts.Issues, PendingCount: ov.Counts.Pending, Filter: filter.String()}
118+ p.Running, p.RunsEnabled = ov.Counts.Running, ov.RunsEnabled
119+ for _, run := range ov.Runs {
120+ r := InboxRunRow{RunRow: s.runRow(run.Repo.String(), run, now), Repo: run.Repo.String(), RepoHref: repoHref(run.Repo.String())}
121+ if run.Status == domain.CIInProgress {
122+ r.State = "in progress"
123+ }
124+ ts := run.StartedAt
125+ if ts.IsZero() {
126+ ts = run.CreatedAt
127+ }
128+ r.When, r.WhenAt = whenOf(now, ts)
129+ p.Runs = append(p.Runs, r)
130+ }
109131 for _, pi := range ov.Pending {
110132 p.Pending = append(p.Pending, s.pendingRow(now, pi, filter))
111133 }
@@ -266,7 +288,7 @@ func (l *liveStream) inbox(ctx context.Context, we ports.WatchEvent) ([]fragment
266288 switch we.Event.(type) {
267289 case domain.MergeRequestOpened, domain.MergeRequestMerged, domain.MergeRequestClosed, domain.MergeRequestReady, domain.CommentAdded, domain.CommentEdited, domain.CommentDeleted, domain.CommentResolved,
268290 domain.IssueOpened, domain.IssueClosed, domain.IssueReopened, domain.IssueCommented, domain.IssueSubmitted, domain.IssuePendingRemoved,
269- domain.RepoUpdated, domain.RepoDeleted:
291+ domain.RepoUpdated, domain.RepoDeleted, domain.RunUpdated:
270292 default:
271293 return nil, nil
272294 }
@@ -283,7 +305,7 @@ func (l *liveStream) inbox(ctx context.Context, we ports.WatchEvent) ([]fragment
283305 }
284306 p := l.s.inboxPage(ov, filter)
285307 var out []fragment
286- for _, c := range []templ.Component{InboxCounters(p), InboxRepos(p), InboxPending(p), InboxMRs(p), InboxIssues(p)} {
308+ for _, c := range []templ.Component{InboxCounters(p), InboxRepos(p), InboxPending(p), InboxRuns(p), InboxMRs(p), InboxIssues(p)} {
287309 f, err := renderFragment(ctx, inboxEvent, c)
288310 if err != nil {
289311 return nil, err
@@ -67,8 +67,17 @@ type InboxRepoLink struct {
67 Current bool67 Current bool
68 }68 }
69 69
70-// InboxPage is the inbox: the counters, the filter and the two lists.70+// InboxRunRow adds repository and start time to the Actions run display.
71+type InboxRunRow struct {
72+ RunRow
73+ Repo, RepoHref, When, WhenAt string
74+}
75+
76+// InboxPage is the inbox: counters, repository filter and item lists.
71 type InboxPage struct {77 type InboxPage struct {
78+ Running int
79+ RunsEnabled bool
80+ Runs []InboxRunRow
72 // MergeRequests and Issues count every open item, whatever the filter.81 // MergeRequests and Issues count every open item, whatever the filter.
73 MergeRequests, Issues int82 MergeRequests, Issues int
74 // PendingCount counts every issue awaiting moderation.83 // PendingCount counts every issue awaiting moderation.
@@ -106,6 +115,19 @@ func inboxFilter(r *http.Request) (domain.RepoName, error) {
106 func (s *Server) inboxPage(ov ports.InboxOverview, filter domain.RepoName) InboxPage {115 func (s *Server) inboxPage(ov ports.InboxOverview, filter domain.RepoName) InboxPage {
107 now := s.cfg.Now()116 now := s.cfg.Now()
108 p := InboxPage{MergeRequests: ov.Counts.MergeRequests, Issues: ov.Counts.Issues, PendingCount: ov.Counts.Pending, Filter: filter.String()}117 p := InboxPage{MergeRequests: ov.Counts.MergeRequests, Issues: ov.Counts.Issues, PendingCount: ov.Counts.Pending, Filter: filter.String()}
118+ p.Running, p.RunsEnabled = ov.Counts.Running, ov.RunsEnabled
119+ for _, run := range ov.Runs {
120+ r := InboxRunRow{RunRow: s.runRow(run.Repo.String(), run, now), Repo: run.Repo.String(), RepoHref: repoHref(run.Repo.String())}
121+ if run.Status == domain.CIInProgress {
122+ r.State = "in progress"
123+ }
124+ ts := run.StartedAt
125+ if ts.IsZero() {
126+ ts = run.CreatedAt
127+ }
128+ r.When, r.WhenAt = whenOf(now, ts)
129+ p.Runs = append(p.Runs, r)
130+ }
109 for _, pi := range ov.Pending {131 for _, pi := range ov.Pending {
110 p.Pending = append(p.Pending, s.pendingRow(now, pi, filter))132 p.Pending = append(p.Pending, s.pendingRow(now, pi, filter))
111 }133 }
@@ -266,7 +288,7 @@ func (l *liveStream) inbox(ctx context.Context, we ports.WatchEvent) ([]fragment
266 switch we.Event.(type) {288 switch we.Event.(type) {
267 case domain.MergeRequestOpened, domain.MergeRequestMerged, domain.MergeRequestClosed, domain.MergeRequestReady, domain.CommentAdded, domain.CommentEdited, domain.CommentDeleted, domain.CommentResolved,289 case domain.MergeRequestOpened, domain.MergeRequestMerged, domain.MergeRequestClosed, domain.MergeRequestReady, domain.CommentAdded, domain.CommentEdited, domain.CommentDeleted, domain.CommentResolved,
268 domain.IssueOpened, domain.IssueClosed, domain.IssueReopened, domain.IssueCommented, domain.IssueSubmitted, domain.IssuePendingRemoved,290 domain.IssueOpened, domain.IssueClosed, domain.IssueReopened, domain.IssueCommented, domain.IssueSubmitted, domain.IssuePendingRemoved,
269- domain.RepoUpdated, domain.RepoDeleted:291+ domain.RepoUpdated, domain.RepoDeleted, domain.RunUpdated:
270 default:292 default:
271 return nil, nil293 return nil, nil
272 }294 }
@@ -283,7 +305,7 @@ func (l *liveStream) inbox(ctx context.Context, we ports.WatchEvent) ([]fragment
283 }305 }
284 p := l.s.inboxPage(ov, filter)306 p := l.s.inboxPage(ov, filter)
285 var out []fragment307 var out []fragment
286- for _, c := range []templ.Component{InboxCounters(p), InboxRepos(p), InboxPending(p), InboxMRs(p), InboxIssues(p)} {308+ for _, c := range []templ.Component{InboxCounters(p), InboxRepos(p), InboxPending(p), InboxRuns(p), InboxMRs(p), InboxIssues(p)} {
287 f, err := renderFragment(ctx, inboxEvent, c)309 f, err := renderFragment(ctx, inboxEvent, c)
288 if err != nil {310 if err != nil {
289 return nil, err311 return nil, err
M internal/adapters/web/inbox.templ +34 -1
@@ -12,6 +12,7 @@ templ Inbox(l Layout, p InboxPage) {
1212 @InboxCounters(p)
1313 @InboxRepos(p)
1414 @InboxPending(p)
15+ @InboxRuns(p)
1516 @InboxMRs(p)
1617 @InboxIssues(p)
1718 </div>
@@ -20,7 +21,7 @@ templ Inbox(l Layout, p InboxPage) {
2021
2122 templ InboxCounters(p InboxPage) {
2223 <p id="inbox-counts" class="inbox-counts">
23- <strong>{ itoa(p.MergeRequests) }</strong> merge requests · <strong>{ itoa(p.Issues) }</strong> issues · <strong>{ itoa(p.PendingCount) }</strong> awaiting moderation
24+ <strong>{ itoa(p.MergeRequests) }</strong> merge requests · <strong>{ itoa(p.Issues) }</strong> issues · <strong>{ itoa(p.PendingCount) }</strong> awaiting moderation · <strong>{ itoa(p.Running) }</strong> running
2425 </p>
2526 }
2627
@@ -113,3 +114,35 @@ templ inboxRows(rows []InboxRow, label string) {
113114 </table>
114115 }
115116 }
117+
118+// InboxRuns stays as an empty fragment when CI is disabled.
119+templ InboxRuns(p InboxPage) {
120+ <section id="inbox-runs" hidden?={ !p.RunsEnabled }
121+ if p.RunsEnabled {
122+ aria-labelledby="inbox-runs-h"
123+ }
124+ >
125+ if p.RunsEnabled {
126+ <h2 id="inbox-runs-h" class="rule-h"><span>Running</span><span class="count">{ itoa(len(p.Runs)) }</span></h2>
127+ if len(p.Runs) == 0 {
128+ <p class="lede dim">no runs in progress.</p>
129+ } else {
130+ <table class="list mrs" aria-label="Running">
131+ <tbody data-rows>
132+ for _, r := range p.Runs {
133+ <tr>
134+ <td class="c-st">@CheckIcon(r.Kind, r.State)<span class="dim">{ r.State }</span></td>
135+ <td class="c-subject"><a href={ templ.SafeURL(r.Href) }>{ r.Workflow }{ " " }{ r.Number }</a> <a class="mute" href={ templ.SafeURL(r.RepoHref) }>{ r.Repo }</a> <span class="dim">{ r.Ref }{ " " }{ r.Event }</span></td>
136+ <td class="c-when nowrap dim">
137+ if r.WhenAt != "" {
138+ <time datetime={ r.WhenAt } title={ r.WhenAt }>{ r.When }</time>
139+ }
140+ </td>
141+ </tr>
142+ }
143+ </tbody>
144+ </table>
145+ }
146+ }
147+ </section>
148+}
@@ -12,6 +12,7 @@ templ Inbox(l Layout, p InboxPage) {
12 @InboxCounters(p)12 @InboxCounters(p)
13 @InboxRepos(p)13 @InboxRepos(p)
14 @InboxPending(p)14 @InboxPending(p)
15+ @InboxRuns(p)
15 @InboxMRs(p)16 @InboxMRs(p)
16 @InboxIssues(p)17 @InboxIssues(p)
17 </div>18 </div>
@@ -20,7 +21,7 @@ templ Inbox(l Layout, p InboxPage) {
20 21
21 templ InboxCounters(p InboxPage) {22 templ InboxCounters(p InboxPage) {
22 <p id="inbox-counts" class="inbox-counts">23 <p id="inbox-counts" class="inbox-counts">
23- <strong>{ itoa(p.MergeRequests) }</strong> merge requests · <strong>{ itoa(p.Issues) }</strong> issues · <strong>{ itoa(p.PendingCount) }</strong> awaiting moderation24+ <strong>{ itoa(p.MergeRequests) }</strong> merge requests · <strong>{ itoa(p.Issues) }</strong> issues · <strong>{ itoa(p.PendingCount) }</strong> awaiting moderation · <strong>{ itoa(p.Running) }</strong> running
24 </p>25 </p>
25 }26 }
26 27
@@ -113,3 +114,35 @@ templ inboxRows(rows []InboxRow, label string) {
113 </table>114 </table>
114 }115 }
115 }116 }
117+
118+// InboxRuns stays as an empty fragment when CI is disabled.
119+templ InboxRuns(p InboxPage) {
120+ <section id="inbox-runs" hidden?={ !p.RunsEnabled }
121+ if p.RunsEnabled {
122+ aria-labelledby="inbox-runs-h"
123+ }
124+ >
125+ if p.RunsEnabled {
126+ <h2 id="inbox-runs-h" class="rule-h"><span>Running</span><span class="count">{ itoa(len(p.Runs)) }</span></h2>
127+ if len(p.Runs) == 0 {
128+ <p class="lede dim">no runs in progress.</p>
129+ } else {
130+ <table class="list mrs" aria-label="Running">
131+ <tbody data-rows>
132+ for _, r := range p.Runs {
133+ <tr>
134+ <td class="c-st">@CheckIcon(r.Kind, r.State)<span class="dim">{ r.State }</span></td>
135+ <td class="c-subject"><a href={ templ.SafeURL(r.Href) }>{ r.Workflow }{ " " }{ r.Number }</a> <a class="mute" href={ templ.SafeURL(r.RepoHref) }>{ r.Repo }</a> <span class="dim">{ r.Ref }{ " " }{ r.Event }</span></td>
136+ <td class="c-when nowrap dim">
137+ if r.WhenAt != "" {
138+ <time datetime={ r.WhenAt } title={ r.WhenAt }>{ r.When }</time>
139+ }
140+ </td>
141+ </tr>
142+ }
143+ </tbody>
144+ </table>
145+ }
146+ }
147+ </section>
148+}
M internal/adapters/web/inbox_templ.go +451 -173 generated file: collapsed
M internal/adapters/web/inbox_test.go +6 -4
@@ -180,12 +180,13 @@ func TestInboxLiveFragments(t *testing.T) {
180180 ls := &liveStream{s: srv, view: "inbox"}
181181 frags, err := ls.render(withOwner(t.Context()), ports.WatchEvent{ID: "b-1", Event: domain.IssueOpened{Issue: domain.Issue{Repo: repoName(t, "beta"), Number: 7}}})
182182 require.NoError(t, err)
183- require.Len(t, frags, 5)
183+ require.Len(t, frags, 6)
184184 assert.Contains(t, frags[0].html, `id="inbox-counts"`)
185185 assert.Contains(t, frags[1].html, `id="inbox-repos"`)
186186 assert.Contains(t, frags[2].html, `id="inbox-pending"`)
187- assert.Contains(t, frags[3].html, `id="inbox-mrs"`)
188- assert.Contains(t, frags[4].html, `id="inbox-issues"`)
187+ assert.Contains(t, frags[3].html, `id="inbox-runs"`)
188+ assert.Contains(t, frags[4].html, `id="inbox-mrs"`)
189+ assert.Contains(t, frags[5].html, `id="inbox-issues"`)
189190
190191 none, err := ls.render(withOwner(t.Context()), ports.WatchEvent{Event: domain.PushEvent{Repo: repoName(t, "beta")}})
191192 require.NoError(t, err)
@@ -286,6 +287,7 @@ func TestInboxRefreshesOnRepoEvents(t *testing.T) {
286287 t.Parallel()
287288 for name, ev := range map[string]domain.Event{
288289 "issues turned off": domain.RepoUpdated{Repo: repoName(t, "beta")},
290+ "run updated": domain.RunUpdated{},
289291 "repo deleted": domain.RepoDeleted{Repo: repoName(t, "beta")},
290292 } {
291293 t.Run(name, func(t *testing.T) {
@@ -297,7 +299,7 @@ func TestInboxRefreshesOnRepoEvents(t *testing.T) {
297299 ls := &liveStream{s: srv, view: "inbox"}
298300 frags, err := ls.render(withOwner(t.Context()), ports.WatchEvent{ID: "b-1", Event: ev})
299301 require.NoError(t, err)
300- require.Len(t, frags, 5)
302+ require.Len(t, frags, 6)
301303 all := ""
302304 for _, f := range frags {
303305 all += f.html
@@ -180,12 +180,13 @@ func TestInboxLiveFragments(t *testing.T) {
180 ls := &liveStream{s: srv, view: "inbox"}180 ls := &liveStream{s: srv, view: "inbox"}
181 frags, err := ls.render(withOwner(t.Context()), ports.WatchEvent{ID: "b-1", Event: domain.IssueOpened{Issue: domain.Issue{Repo: repoName(t, "beta"), Number: 7}}})181 frags, err := ls.render(withOwner(t.Context()), ports.WatchEvent{ID: "b-1", Event: domain.IssueOpened{Issue: domain.Issue{Repo: repoName(t, "beta"), Number: 7}}})
182 require.NoError(t, err)182 require.NoError(t, err)
183- require.Len(t, frags, 5)183+ require.Len(t, frags, 6)
184 assert.Contains(t, frags[0].html, `id="inbox-counts"`)184 assert.Contains(t, frags[0].html, `id="inbox-counts"`)
185 assert.Contains(t, frags[1].html, `id="inbox-repos"`)185 assert.Contains(t, frags[1].html, `id="inbox-repos"`)
186 assert.Contains(t, frags[2].html, `id="inbox-pending"`)186 assert.Contains(t, frags[2].html, `id="inbox-pending"`)
187- assert.Contains(t, frags[3].html, `id="inbox-mrs"`)187+ assert.Contains(t, frags[3].html, `id="inbox-runs"`)
188- assert.Contains(t, frags[4].html, `id="inbox-issues"`)188+ assert.Contains(t, frags[4].html, `id="inbox-mrs"`)
189+ assert.Contains(t, frags[5].html, `id="inbox-issues"`)
189 190
190 none, err := ls.render(withOwner(t.Context()), ports.WatchEvent{Event: domain.PushEvent{Repo: repoName(t, "beta")}})191 none, err := ls.render(withOwner(t.Context()), ports.WatchEvent{Event: domain.PushEvent{Repo: repoName(t, "beta")}})
191 require.NoError(t, err)192 require.NoError(t, err)
@@ -286,6 +287,7 @@ func TestInboxRefreshesOnRepoEvents(t *testing.T) {
286 t.Parallel()287 t.Parallel()
287 for name, ev := range map[string]domain.Event{288 for name, ev := range map[string]domain.Event{
288 "issues turned off": domain.RepoUpdated{Repo: repoName(t, "beta")},289 "issues turned off": domain.RepoUpdated{Repo: repoName(t, "beta")},
290+ "run updated": domain.RunUpdated{},
289 "repo deleted": domain.RepoDeleted{Repo: repoName(t, "beta")},291 "repo deleted": domain.RepoDeleted{Repo: repoName(t, "beta")},
290 } {292 } {
291 t.Run(name, func(t *testing.T) {293 t.Run(name, func(t *testing.T) {
@@ -297,7 +299,7 @@ func TestInboxRefreshesOnRepoEvents(t *testing.T) {
297 ls := &liveStream{s: srv, view: "inbox"}299 ls := &liveStream{s: srv, view: "inbox"}
298 frags, err := ls.render(withOwner(t.Context()), ports.WatchEvent{ID: "b-1", Event: ev})300 frags, err := ls.render(withOwner(t.Context()), ports.WatchEvent{ID: "b-1", Event: ev})
299 require.NoError(t, err)301 require.NoError(t, err)
300- require.Len(t, frags, 5)302+ require.Len(t, frags, 6)
301 all := ""303 all := ""
302 for _, f := range frags {304 for _, f := range frags {
303 all += f.html305 all += f.html
A internal/adapters/web/jstest/keys.test.mjs +29 -0
@@ -0,0 +1,29 @@
1+// Exercise the production keydown handler without loading the rest of the page.
2+import assert from 'node:assert/strict';
3+import { readFileSync } from 'node:fs';
4+import { test } from 'node:test';
5+const src = readFileSync(new URL('../static/js/ermite.js', import.meta.url), 'utf8');
6+const block = src.slice(src.indexOf(' // 1. Keys.'), src.indexOf(' // 2. A [data-fire]'));
7+
8+function page({ owner = true, off = false, typing = false, dialog = false } = {}) {
9+ let handler;
10+ const fired = [];
11+ const window = { ermiteKeys: { blocks: () => off } };
12+ const $ = (s) => s === 'dialog[open]' ? dialog : s === '[data-inbox]' ? owner : null;
13+ new Function('window', '$', 'ermite', 'addEventListener', block)(window, $, { fire: (n) => fired.push(n) }, (_, f) => { handler = f; });
14+ return (key) => {
15+ const e = { key, shiftKey: key === 'I', preventDefault() {}, target: { closest: (s) => s.startsWith('input') && typing } };
16+ handler(e);
17+ return fired;
18+ };
19+}
20+test('Shift+I opens inbox while lowercase i retains the issues tab', () => {
21+ const press = page();
22+ assert.deepEqual(press('I'), ['inbox']);
23+ assert.deepEqual(press('i'), ['inbox', 'i']);
24+});
25+test('inbox shortcut respects preference, fields, dialogs, and owner target', () => {
26+ for (const options of [{ off: true }, { typing: true }, { dialog: true }, { owner: false }]) {
27+ assert.deepEqual(page(options)('I'), [], JSON.stringify(options));
28+ }
29+});
@@ -0,0 +1,29 @@
1+// Exercise the production keydown handler without loading the rest of the page.
2+import assert from 'node:assert/strict';
3+import { readFileSync } from 'node:fs';
4+import { test } from 'node:test';
5+const src = readFileSync(new URL('../static/js/ermite.js', import.meta.url), 'utf8');
6+const block = src.slice(src.indexOf(' // 1. Keys.'), src.indexOf(' // 2. A [data-fire]'));
7+
8+function page({ owner = true, off = false, typing = false, dialog = false } = {}) {
9+ let handler;
10+ const fired = [];
11+ const window = { ermiteKeys: { blocks: () => off } };
12+ const $ = (s) => s === 'dialog[open]' ? dialog : s === '[data-inbox]' ? owner : null;
13+ new Function('window', '$', 'ermite', 'addEventListener', block)(window, $, { fire: (n) => fired.push(n) }, (_, f) => { handler = f; });
14+ return (key) => {
15+ const e = { key, shiftKey: key === 'I', preventDefault() {}, target: { closest: (s) => s.startsWith('input') && typing } };
16+ handler(e);
17+ return fired;
18+ };
19+}
20+test('Shift+I opens inbox while lowercase i retains the issues tab', () => {
21+ const press = page();
22+ assert.deepEqual(press('I'), ['inbox']);
23+ assert.deepEqual(press('i'), ['inbox', 'i']);
24+});
25+test('inbox shortcut respects preference, fields, dialogs, and owner target', () => {
26+ for (const options of [{ off: true }, { typing: true }, { dialog: true }, { owner: false }]) {
27+ assert.deepEqual(page(options)('I'), [], JSON.stringify(options));
28+ }
29+});
M internal/adapters/web/keys.templ +1 -0
@@ -28,6 +28,7 @@ templ Keys() {
2828 k-r from:body -> ermite.tab('r');
2929 k-m from:body -> ermite.tab('m');
3030 k-i from:body -> ermite.tab('i');
31+ k-inbox from:body -> ermite.go('[data-inbox]');
3132 k-next from:body -> ermite.nextNote();
3233 k-split from:body -> ermite.layout('split');
3334 k-unified from:body -> ermite.layout('unified');
@@ -28,6 +28,7 @@ templ Keys() {
28 k-r from:body -> ermite.tab('r');28 k-r from:body -> ermite.tab('r');
29 k-m from:body -> ermite.tab('m');29 k-m from:body -> ermite.tab('m');
30 k-i from:body -> ermite.tab('i');30 k-i from:body -> ermite.tab('i');
31+ k-inbox from:body -> ermite.go('[data-inbox]');
31 k-next from:body -> ermite.nextNote();32 k-next from:body -> ermite.nextNote();
32 k-split from:body -> ermite.layout('split');33 k-split from:body -> ermite.layout('split');
33 k-unified from:body -> ermite.layout('unified');34 k-unified from:body -> ermite.layout('unified');
M internal/adapters/web/keys_templ.go +1 -1 generated file: collapsed
M internal/adapters/web/overlays.templ +3 -0
@@ -27,6 +27,9 @@ templ OverlayHelp(mr bool) {
2727 <div><dt><kbd>/</kbd></dt><dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd></div>
2828 <div><dt><kbd>b</kbd></dt><dd>{ "switch" } branch or tag</dd></div>
2929 <div><dt><kbd>s</kbd> <kbd>t</kbd> <kbd>c</kbd> <kbd>r</kbd> <kbd>m</kbd> <kbd>i</kbd></dt><dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd></div>
30+ if SignedIn(ctx) {
31+ <div><dt><kbd>I</kbd></dt><dd>inbox</dd></div>
32+ }
3033 <div><dt><kbd>g h</kbd></dt><dd>home</dd></div>
3134 <div><dt><kbd>w</kbd></dt><dd>wide mode: use the whole width of the screen, or not (remembered)</dd></div>
3235 <div><dt><kbd>y</kbd></dt><dd>copy a permalink pinned to the commit</dd></div>
@@ -27,6 +27,9 @@ templ OverlayHelp(mr bool) {
27 <div><dt><kbd>/</kbd></dt><dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd></div>27 <div><dt><kbd>/</kbd></dt><dd>filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)</dd></div>
28 <div><dt><kbd>b</kbd></dt><dd>{ "switch" } branch or tag</dd></div>28 <div><dt><kbd>b</kbd></dt><dd>{ "switch" } branch or tag</dd></div>
29 <div><dt><kbd>s</kbd> <kbd>t</kbd> <kbd>c</kbd> <kbd>r</kbd> <kbd>m</kbd> <kbd>i</kbd></dt><dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd></div>29 <div><dt><kbd>s</kbd> <kbd>t</kbd> <kbd>c</kbd> <kbd>r</kbd> <kbd>m</kbd> <kbd>i</kbd></dt><dd>tabs: summary, files, commits, branches &amp; tags, merge requests, issues</dd></div>
30+ if SignedIn(ctx) {
31+ <div><dt><kbd>I</kbd></dt><dd>inbox</dd></div>
32+ }
30 <div><dt><kbd>g h</kbd></dt><dd>home</dd></div>33 <div><dt><kbd>g h</kbd></dt><dd>home</dd></div>
31 <div><dt><kbd>w</kbd></dt><dd>wide mode: use the whole width of the screen, or not (remembered)</dd></div>34 <div><dt><kbd>w</kbd></dt><dd>wide mode: use the whole width of the screen, or not (remembered)</dd></div>
32 <div><dt><kbd>y</kbd></dt><dd>copy a permalink pinned to the commit</dd></div>35 <div><dt><kbd>y</kbd></dt><dd>copy a permalink pinned to the commit</dd></div>
M internal/adapters/web/overlays_templ.go +49 -39 generated file: collapsed
M internal/adapters/web/shell.templ +5 -1
@@ -67,6 +67,9 @@ templ Shell(l Layout, rail templ.Component) {
6767 </div>
6868 @Footer(l)
6969 @Hint(l)
70+ if SignedIn(ctx) {
71+ <a hidden data-inbox href={ inboxPath }>Inbox</a>
72+ }
7073 @OverlayHelp(l.MR)
7174 @OverlayRefs(l)
7275 @OverlayCmd(l.Cmd)
@@ -87,7 +90,8 @@ templ Header(l Layout) {
8790 <a
8891 class="btn inbox-btn"
8992 href={ templ.SafeURL(l.Inbox.Href) }
90- title="inbox"
93+ title="inbox (I)"
94+ aria-keyshortcuts="Shift+I"
9195 >
9296 <span class="ico i-mailbox" aria-hidden="true"></span>
9397 Inbox
@@ -67,6 +67,9 @@ templ Shell(l Layout, rail templ.Component) {
67 </div>67 </div>
68 @Footer(l)68 @Footer(l)
69 @Hint(l)69 @Hint(l)
70+ if SignedIn(ctx) {
71+ <a hidden data-inbox href={ inboxPath }>Inbox</a>
72+ }
70 @OverlayHelp(l.MR)73 @OverlayHelp(l.MR)
71 @OverlayRefs(l)74 @OverlayRefs(l)
72 @OverlayCmd(l.Cmd)75 @OverlayCmd(l.Cmd)
@@ -87,7 +90,8 @@ templ Header(l Layout) {
87 <a90 <a
88 class="btn inbox-btn"91 class="btn inbox-btn"
89 href={ templ.SafeURL(l.Inbox.Href) }92 href={ templ.SafeURL(l.Inbox.Href) }
90- title="inbox"93+ title="inbox (I)"
94+ aria-keyshortcuts="Shift+I"
91 >95 >
92 <span class="ico i-mailbox" aria-hidden="true"></span>96 <span class="ico i-mailbox" aria-hidden="true"></span>
93 Inbox97 Inbox
M internal/adapters/web/shell_templ.go +152 -133
@@ -426,6 +426,25 @@ func Shell(l Layout, rail templ.Component) templ.Component {
426426 if templ_7745c5c3_Err != nil {
427427 return templ_7745c5c3_Err
428428 }
429+ if SignedIn(ctx) {
430+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "<a hidden data-inbox href=\"")
431+ if templ_7745c5c3_Err != nil {
432+ return templ_7745c5c3_Err
433+ }
434+ var templ_7745c5c3_Var26 templ.SafeURL
435+ templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinURLErrs(inboxPath)
436+ if templ_7745c5c3_Err != nil {
437+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 71, Col: 41}
438+ }
439+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
440+ if templ_7745c5c3_Err != nil {
441+ return templ_7745c5c3_Err
442+ }
443+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "\">Inbox</a>")
444+ if templ_7745c5c3_Err != nil {
445+ return templ_7745c5c3_Err
446+ }
447+ }
429448 templ_7745c5c3_Err = OverlayHelp(l.MR).Render(ctx, templ_7745c5c3_Buffer)
430449 if templ_7745c5c3_Err != nil {
431450 return templ_7745c5c3_Err
@@ -442,7 +461,7 @@ func Shell(l Layout, rail templ.Component) templ.Component {
442461 if templ_7745c5c3_Err != nil {
443462 return templ_7745c5c3_Err
444463 }
445- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</body></html>")
464+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</body></html>")
446465 if templ_7745c5c3_Err != nil {
447466 return templ_7745c5c3_Err
448467 }
@@ -468,366 +487,366 @@ func Header(l Layout) templ.Component {
468487 }()
469488 }
470489 ctx = templ.InitializeContext(ctx)
471- templ_7745c5c3_Var26 := templ.GetChildren(ctx)
472- if templ_7745c5c3_Var26 == nil {
473- templ_7745c5c3_Var26 = templ.NopComponent
490+ templ_7745c5c3_Var27 := templ.GetChildren(ctx)
491+ if templ_7745c5c3_Var27 == nil {
492+ templ_7745c5c3_Var27 = templ.NopComponent
474493 }
475494 ctx = templ.ClearChildren(ctx)
476- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "<header class=\"hdr\"><div class=\"hdr-top\"><a class=\"brand\" href=\"/\" data-g=\"h\" aria-label=\"home\" title=\"home (g h)\"><span class=\"brand-mark\" aria-hidden=\"true\"></span></a> ")
495+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "<header class=\"hdr\"><div class=\"hdr-top\"><a class=\"brand\" href=\"/\" data-g=\"h\" aria-label=\"home\" title=\"home (g h)\"><span class=\"brand-mark\" aria-hidden=\"true\"></span></a> ")
477496 if templ_7745c5c3_Err != nil {
478497 return templ_7745c5c3_Err
479498 }
480499 if l.Inbox != nil {
481- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "<a class=\"btn inbox-btn\" href=\"")
500+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "<a class=\"btn inbox-btn\" href=\"")
482501 if templ_7745c5c3_Err != nil {
483502 return templ_7745c5c3_Err
484503 }
485- var templ_7745c5c3_Var27 templ.SafeURL
486- templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.Inbox.Href))
504+ var templ_7745c5c3_Var28 templ.SafeURL
505+ templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.Inbox.Href))
487506 if templ_7745c5c3_Err != nil {
488- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 89, Col: 39}
507+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 92, Col: 39}
489508 }
490- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
509+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
491510 if templ_7745c5c3_Err != nil {
492511 return templ_7745c5c3_Err
493512 }
494- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "\" title=\"inbox\"><span class=\"ico i-mailbox\" aria-hidden=\"true\"></span> Inbox ")
513+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "\" title=\"inbox (I)\" aria-keyshortcuts=\"Shift+I\"><span class=\"ico i-mailbox\" aria-hidden=\"true\"></span> Inbox ")
495514 if templ_7745c5c3_Err != nil {
496515 return templ_7745c5c3_Err
497516 }
498517 if l.Inbox.Count >= 0 {
499- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "<span class=\"count\">")
518+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "<span class=\"count\">")
500519 if templ_7745c5c3_Err != nil {
501520 return templ_7745c5c3_Err
502521 }
503- var templ_7745c5c3_Var28 string
504- templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(l.Inbox.Count))
522+ var templ_7745c5c3_Var29 string
523+ templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(l.Inbox.Count))
505524 if templ_7745c5c3_Err != nil {
506- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 95, Col: 47}
525+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 99, Col: 47}
507526 }
508- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
527+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
509528 if templ_7745c5c3_Err != nil {
510529 return templ_7745c5c3_Err
511530 }
512- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "</span>")
531+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "</span>")
513532 if templ_7745c5c3_Err != nil {
514533 return templ_7745c5c3_Err
515534 }
516535 }
517- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "</a> ")
536+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "</a> ")
518537 if templ_7745c5c3_Err != nil {
519538 return templ_7745c5c3_Err
520539 }
521540 }
522541 if len(l.Crumbs) > 0 {
523- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "<nav class=\"crumbs\" aria-label=\"Path\"><ol>")
542+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "<nav class=\"crumbs\" aria-label=\"Path\"><ol>")
524543 if templ_7745c5c3_Err != nil {
525544 return templ_7745c5c3_Err
526545 }
527546 for _, c := range l.Crumbs {
528- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "<li>")
547+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "<li>")
529548 if templ_7745c5c3_Err != nil {
530549 return templ_7745c5c3_Err
531550 }
532551 if c.Href != "" {
533- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "<a href=\"")
552+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "<a href=\"")
534553 if templ_7745c5c3_Err != nil {
535554 return templ_7745c5c3_Err
536555 }
537- var templ_7745c5c3_Var29 templ.SafeURL
538- templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Href))
556+ var templ_7745c5c3_Var30 templ.SafeURL
557+ templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Href))
539558 if templ_7745c5c3_Err != nil {
540- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 106, Col: 38}
559+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 110, Col: 38}
541560 }
542- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
561+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
543562 if templ_7745c5c3_Err != nil {
544563 return templ_7745c5c3_Err
545564 }
546- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "\"")
565+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "\"")
547566 if templ_7745c5c3_Err != nil {
548567 return templ_7745c5c3_Err
549568 }
550569 if c.Current {
551- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, " aria-current=\"page\"")
570+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, " aria-current=\"page\"")
552571 if templ_7745c5c3_Err != nil {
553572 return templ_7745c5c3_Err
554573 }
555574 }
556- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, ">")
575+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, ">")
557576 if templ_7745c5c3_Err != nil {
558577 return templ_7745c5c3_Err
559578 }
560- var templ_7745c5c3_Var30 string
561- templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label)
579+ var templ_7745c5c3_Var31 string
580+ templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label)
562581 if templ_7745c5c3_Err != nil {
563- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 110, Col: 19}
582+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 114, Col: 19}
564583 }
565- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
584+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
566585 if templ_7745c5c3_Err != nil {
567586 return templ_7745c5c3_Err
568587 }
569- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "</a>")
588+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "</a>")
570589 if templ_7745c5c3_Err != nil {
571590 return templ_7745c5c3_Err
572591 }
573592 } else {
574- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "<span")
593+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "<span")
575594 if templ_7745c5c3_Err != nil {
576595 return templ_7745c5c3_Err
577596 }
578597 if c.Current {
579- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, " aria-current=\"page\"")
598+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, " aria-current=\"page\"")
580599 if templ_7745c5c3_Err != nil {
581600 return templ_7745c5c3_Err
582601 }
583602 }
584- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, ">")
603+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, ">")
585604 if templ_7745c5c3_Err != nil {
586605 return templ_7745c5c3_Err
587606 }
588- var templ_7745c5c3_Var31 string
589- templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label)
607+ var templ_7745c5c3_Var32 string
608+ templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label)
590609 if templ_7745c5c3_Err != nil {
591- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 116, Col: 19}
610+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 120, Col: 19}
592611 }
593- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
612+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
594613 if templ_7745c5c3_Err != nil {
595614 return templ_7745c5c3_Err
596615 }
597- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "</span>")
616+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "</span>")
598617 if templ_7745c5c3_Err != nil {
599618 return templ_7745c5c3_Err
600619 }
601620 }
602- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "</li>")
621+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "</li>")
603622 if templ_7745c5c3_Err != nil {
604623 return templ_7745c5c3_Err
605624 }
606625 }
607- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "</ol></nav>")
626+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "</ol></nav>")
608627 if templ_7745c5c3_Err != nil {
609628 return templ_7745c5c3_Err
610629 }
611630 }
612- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "<div class=\"hdr-acts\">")
631+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "<div class=\"hdr-acts\">")
613632 if templ_7745c5c3_Err != nil {
614633 return templ_7745c5c3_Err
615634 }
616635 if l.Ref != "" {
617- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "<a class=\"btn ref\" href=\"")
636+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "<a class=\"btn ref\" href=\"")
618637 if templ_7745c5c3_Err != nil {
619638 return templ_7745c5c3_Err
620639 }
621- var templ_7745c5c3_Var32 templ.SafeURL
622- templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(repoHref(l.Repo, "refs")))
640+ var templ_7745c5c3_Var33 templ.SafeURL
641+ templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(repoHref(l.Repo, "refs")))
623642 if templ_7745c5c3_Err != nil {
624- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 127, Col: 52}
643+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 131, Col: 52}
625644 }
626- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
645+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))
627646 if templ_7745c5c3_Err != nil {
628647 return templ_7745c5c3_Err
629648 }
630- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "\" data-fire=\"refs\" aria-keyshortcuts=\"b\" title=\"switch branch or tag (b)\"><span class=\"ico i-branch\" aria-hidden=\"true\"></span> <span class=\"ref-name\">")
649+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "\" data-fire=\"refs\" aria-keyshortcuts=\"b\" title=\"switch branch or tag (b)\"><span class=\"ico i-branch\" aria-hidden=\"true\"></span> <span class=\"ref-name\">")
631650 if templ_7745c5c3_Err != nil {
632651 return templ_7745c5c3_Err
633652 }
634- var templ_7745c5c3_Var33 string
635- templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(l.Ref)
653+ var templ_7745c5c3_Var34 string
654+ templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(l.Ref)
636655 if templ_7745c5c3_Err != nil {
637- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 133, Col: 36}
656+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 137, Col: 36}
638657 }
639- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))
658+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34))
640659 if templ_7745c5c3_Err != nil {
641660 return templ_7745c5c3_Err
642661 }
643- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "</span> <span class=\"sha\">")
662+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "</span> <span class=\"sha\">")
644663 if templ_7745c5c3_Err != nil {
645664 return templ_7745c5c3_Err
646665 }
647- var templ_7745c5c3_Var34 string
648- templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(l.Short)
666+ var templ_7745c5c3_Var35 string
667+ templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(l.Short)
649668 if templ_7745c5c3_Err != nil {
650- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 134, Col: 33}
669+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 138, Col: 33}
651670 }
652- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34))
671+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35))
653672 if templ_7745c5c3_Err != nil {
654673 return templ_7745c5c3_Err
655674 }
656- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "</span></a> ")
675+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "</span></a> ")
657676 if templ_7745c5c3_Err != nil {
658677 return templ_7745c5c3_Err
659678 }
660679 }
661- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "<button type=\"button\" class=\"btn search\" data-js aria-haspopup=\"dialog\" aria-controls=\"panel-cmd\" aria-keyshortcuts=\"Control+K :\" title=\"go to a section, ref, file or repo (ctrl k or :)\" data-fire=\"cmd\"><span class=\"ico i-search\" aria-hidden=\"true\"></span> <span class=\"search-label\">Go to…</span> <kbd aria-hidden=\"true\">ctrl k</kbd></button> ")
680+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "<button type=\"button\" class=\"btn search\" data-js aria-haspopup=\"dialog\" aria-controls=\"panel-cmd\" aria-keyshortcuts=\"Control+K :\" title=\"go to a section, ref, file or repo (ctrl k or :)\" data-fire=\"cmd\"><span class=\"ico i-search\" aria-hidden=\"true\"></span> <span class=\"search-label\">Go to…</span> <kbd aria-hidden=\"true\">ctrl k</kbd></button> ")
662681 if templ_7745c5c3_Err != nil {
663682 return templ_7745c5c3_Err
664683 }
665684 if SignedIn(ctx) {
666- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "<form class=\"owner\" method=\"post\" action=\"/-/logout\"><span class=\"owner-badge\" title=\"signed in as the owner\"><span class=\"ico i-user\" aria-hidden=\"true\"></span>")
685+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "<form class=\"owner\" method=\"post\" action=\"/-/logout\"><span class=\"owner-badge\" title=\"signed in as the owner\"><span class=\"ico i-user\" aria-hidden=\"true\"></span>")
667686 if templ_7745c5c3_Err != nil {
668687 return templ_7745c5c3_Err
669688 }
670- var templ_7745c5c3_Var35 string
671- templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(l.Owner)
689+ var templ_7745c5c3_Var36 string
690+ templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(l.Owner)
672691 if templ_7745c5c3_Err != nil {
673- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 153, Col: 123}
692+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 157, Col: 123}
674693 }
675- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35))
694+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36))
676695 if templ_7745c5c3_Err != nil {
677696 return templ_7745c5c3_Err
678697 }
679- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "</span> ")
698+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "</span> ")
680699 if templ_7745c5c3_Err != nil {
681700 return templ_7745c5c3_Err
682701 }
683702 if l.Passkeys {
684- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "<a class=\"btn icon\" href=\"/-/passkeys\" aria-label=\"Passkeys\" title=\"passkeys: add or remove the passkeys that sign you in\"><span class=\"ico i-key\" aria-hidden=\"true\"></span></a> ")
703+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "<a class=\"btn icon\" href=\"/-/passkeys\" aria-label=\"Passkeys\" title=\"passkeys: add or remove the passkeys that sign you in\"><span class=\"ico i-key\" aria-hidden=\"true\"></span></a> ")
685704 if templ_7745c5c3_Err != nil {
686705 return templ_7745c5c3_Err
687706 }
688707 }
689- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "<button type=\"submit\" class=\"btn icon\" aria-label=\"Sign out\" title=\"sign out of this browser\"><span class=\"ico i-log-out\" aria-hidden=\"true\"></span></button></form>")
708+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "<button type=\"submit\" class=\"btn icon\" aria-label=\"Sign out\" title=\"sign out of this browser\"><span class=\"ico i-log-out\" aria-hidden=\"true\"></span></button></form>")
690709 if templ_7745c5c3_Err != nil {
691710 return templ_7745c5c3_Err
692711 }
693712 } else if l.Passkeys {
694- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "<a class=\"btn icon\" href=\"/-/login\" aria-label=\"Sign in\" title=\"sign in with a passkey\"><span class=\"ico i-log-in\" aria-hidden=\"true\"></span></a>")
713+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "<a class=\"btn icon\" href=\"/-/login\" aria-label=\"Sign in\" title=\"sign in with a passkey\"><span class=\"ico i-log-in\" aria-hidden=\"true\"></span></a>")
695714 if templ_7745c5c3_Err != nil {
696715 return templ_7745c5c3_Err
697716 }
698717 }
699- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "</div></div>")
718+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "</div></div>")
700719 if templ_7745c5c3_Err != nil {
701720 return templ_7745c5c3_Err
702721 }
703722 if len(l.Sections) > 0 {
704- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "<nav class=\"tabs\" aria-label=\"Sections\"><ul>")
723+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "<nav class=\"tabs\" aria-label=\"Sections\"><ul>")
705724 if templ_7745c5c3_Err != nil {
706725 return templ_7745c5c3_Err
707726 }
708727 for _, s := range l.Sections {
709- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "<li><a href=\"")
728+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "<li><a href=\"")
710729 if templ_7745c5c3_Err != nil {
711730 return templ_7745c5c3_Err
712731 }
713- var templ_7745c5c3_Var36 templ.SafeURL
714- templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(s.Href))
732+ var templ_7745c5c3_Var37 templ.SafeURL
733+ templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(s.Href))
715734 if templ_7745c5c3_Err != nil {
716- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 170, Col: 36}
735+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 174, Col: 36}
717736 }
718- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36))
737+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37))
719738 if templ_7745c5c3_Err != nil {
720739 return templ_7745c5c3_Err
721740 }
722- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "\" data-key=\"")
741+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "\" data-key=\"")
723742 if templ_7745c5c3_Err != nil {
724743 return templ_7745c5c3_Err
725744 }
726- var templ_7745c5c3_Var37 string
727- templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key)
745+ var templ_7745c5c3_Var38 string
746+ templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key)
728747 if templ_7745c5c3_Err != nil {
729- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 171, Col: 24}
748+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 175, Col: 24}
730749 }
731- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var37)
750+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var38)
732751 if templ_7745c5c3_Err != nil {
733752 return templ_7745c5c3_Err
734753 }
735- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "\" aria-keyshortcuts=\"")
754+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "\" aria-keyshortcuts=\"")
736755 if templ_7745c5c3_Err != nil {
737756 return templ_7745c5c3_Err
738757 }
739- var templ_7745c5c3_Var38 string
740- templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key)
758+ var templ_7745c5c3_Var39 string
759+ templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key)
741760 if templ_7745c5c3_Err != nil {
742- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 172, Col: 33}
761+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 176, Col: 33}
743762 }
744- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var38)
763+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var39)
745764 if templ_7745c5c3_Err != nil {
746765 return templ_7745c5c3_Err
747766 }
748- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "\"")
767+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "\"")
749768 if templ_7745c5c3_Err != nil {
750769 return templ_7745c5c3_Err
751770 }
752771 if s.Current {
753- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, " aria-current=\"page\"")
772+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, " aria-current=\"page\"")
754773 if templ_7745c5c3_Err != nil {
755774 return templ_7745c5c3_Err
756775 }
757776 }
758- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, ">")
777+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, ">")
759778 if templ_7745c5c3_Err != nil {
760779 return templ_7745c5c3_Err
761780 }
762- var templ_7745c5c3_Var39 string
763- templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(s.Label)
781+ var templ_7745c5c3_Var40 string
782+ templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(s.Label)
764783 if templ_7745c5c3_Err != nil {
765- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 177, Col: 17}
784+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 181, Col: 17}
766785 }
767- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39))
786+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40))
768787 if templ_7745c5c3_Err != nil {
769788 return templ_7745c5c3_Err
770789 }
771- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, " ")
790+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, " ")
772791 if templ_7745c5c3_Err != nil {
773792 return templ_7745c5c3_Err
774793 }
775794 if s.Count != "" {
776- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "<span class=\"count\">")
795+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "<span class=\"count\">")
777796 if templ_7745c5c3_Err != nil {
778797 return templ_7745c5c3_Err
779798 }
780- var templ_7745c5c3_Var40 string
781- templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(s.Count)
799+ var templ_7745c5c3_Var41 string
800+ templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(s.Count)
782801 if templ_7745c5c3_Err != nil {
783- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 179, Col: 38}
802+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 183, Col: 38}
784803 }
785- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40))
804+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41))
786805 if templ_7745c5c3_Err != nil {
787806 return templ_7745c5c3_Err
788807 }
789- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "<span class=\"sr\">")
808+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "<span class=\"sr\">")
790809 if templ_7745c5c3_Err != nil {
791810 return templ_7745c5c3_Err
792811 }
793- var templ_7745c5c3_Var41 string
794- templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(" open")
812+ var templ_7745c5c3_Var42 string
813+ templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs(" open")
795814 if templ_7745c5c3_Err != nil {
796- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 179, Col: 66}
815+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 183, Col: 66}
797816 }
798- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41))
817+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42))
799818 if templ_7745c5c3_Err != nil {
800819 return templ_7745c5c3_Err
801820 }
802- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "</span></span> ")
821+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "</span></span> ")
803822 if templ_7745c5c3_Err != nil {
804823 return templ_7745c5c3_Err
805824 }
806825 }
807- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "<kbd aria-hidden=\"true\">")
826+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "<kbd aria-hidden=\"true\">")
808827 if templ_7745c5c3_Err != nil {
809828 return templ_7745c5c3_Err
810829 }
811- var templ_7745c5c3_Var42 string
812- templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs(s.Key)
830+ var templ_7745c5c3_Var43 string
831+ templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(s.Key)
813832 if templ_7745c5c3_Err != nil {
814- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 181, Col: 39}
833+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 185, Col: 39}
815834 }
816- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42))
835+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43))
817836 if templ_7745c5c3_Err != nil {
818837 return templ_7745c5c3_Err
819838 }
820- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "</kbd></a></li>")
839+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "</kbd></a></li>")
821840 if templ_7745c5c3_Err != nil {
822841 return templ_7745c5c3_Err
823842 }
824843 }
825- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "</ul></nav>")
844+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "</ul></nav>")
826845 if templ_7745c5c3_Err != nil {
827846 return templ_7745c5c3_Err
828847 }
829848 }
830- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "</header>")
849+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, "</header>")
831850 if templ_7745c5c3_Err != nil {
832851 return templ_7745c5c3_Err
833852 }
@@ -853,38 +872,38 @@ func Footer(l Layout) templ.Component {
853872 }()
854873 }
855874 ctx = templ.InitializeContext(ctx)
856- templ_7745c5c3_Var43 := templ.GetChildren(ctx)
857- if templ_7745c5c3_Var43 == nil {
858- templ_7745c5c3_Var43 = templ.NopComponent
875+ templ_7745c5c3_Var44 := templ.GetChildren(ctx)
876+ if templ_7745c5c3_Var44 == nil {
877+ templ_7745c5c3_Var44 = templ.NopComponent
859878 }
860879 ctx = templ.ClearChildren(ctx)
861- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "<footer class=\"foot\"><span>ermite ")
880+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, "<footer class=\"foot\"><span>ermite ")
862881 if templ_7745c5c3_Err != nil {
863882 return templ_7745c5c3_Err
864883 }
865- var templ_7745c5c3_Var44 string
866- templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(l.Version)
884+ var templ_7745c5c3_Var45 string
885+ templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinStringErrs(l.Version)
867886 if templ_7745c5c3_Err != nil {
868- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 195, Col: 26}
887+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 199, Col: 26}
869888 }
870- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44))
889+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45))
871890 if templ_7745c5c3_Err != nil {
872891 return templ_7745c5c3_Err
873892 }
874- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, "</span> <a href=\"")
893+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "</span> <a href=\"")
875894 if templ_7745c5c3_Err != nil {
876895 return templ_7745c5c3_Err
877896 }
878- var templ_7745c5c3_Var45 templ.SafeURL
879- templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.SourceURL))
897+ var templ_7745c5c3_Var46 templ.SafeURL
898+ templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.SourceURL))
880899 if templ_7745c5c3_Err != nil {
881- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 196, Col: 38}
900+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 200, Col: 38}
882901 }
883- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45))
902+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var46))
884903 if templ_7745c5c3_Err != nil {
885904 return templ_7745c5c3_Err
886905 }
887- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, "\" rel=\"noopener\" title=\"source of this version (AGPL-3.0-or-later)\">source</a> <button type=\"button\" class=\"foot-keys\" data-js data-fire=\"help\" aria-haspopup=\"dialog\" aria-controls=\"panel-help\" aria-keyshortcuts=\"?\"><kbd aria-hidden=\"true\">?</kbd> keyboard shortcuts</button></footer>")
906+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "\" rel=\"noopener\" title=\"source of this version (AGPL-3.0-or-later)\">source</a> <button type=\"button\" class=\"foot-keys\" data-js data-fire=\"help\" aria-haspopup=\"dialog\" aria-controls=\"panel-help\" aria-keyshortcuts=\"?\"><kbd aria-hidden=\"true\">?</kbd> keyboard shortcuts</button></footer>")
888907 if templ_7745c5c3_Err != nil {
889908 return templ_7745c5c3_Err
890909 }
@@ -912,12 +931,12 @@ func Hint(l Layout) templ.Component {
912931 }()
913932 }
914933 ctx = templ.InitializeContext(ctx)
915- templ_7745c5c3_Var46 := templ.GetChildren(ctx)
916- if templ_7745c5c3_Var46 == nil {
917- templ_7745c5c3_Var46 = templ.NopComponent
934+ templ_7745c5c3_Var47 := templ.GetChildren(ctx)
935+ if templ_7745c5c3_Var47 == nil {
936+ templ_7745c5c3_Var47 = templ.NopComponent
918937 }
919938 ctx = templ.ClearChildren(ctx)
920- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "<div class=\"hint\" role=\"status\" aria-live=\"polite\"><span class=\"hint-nav\" :text=\"document.body.dataset.nav || ''\"></span> <span class=\"hint-sel\" :text=\"document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''\"></span> <span class=\"hint-flash\" :text=\"document.body.dataset.flash || ''\"></span>")
939+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 90, "<div class=\"hint\" role=\"status\" aria-live=\"polite\"><span class=\"hint-nav\" :text=\"document.body.dataset.nav || ''\"></span> <span class=\"hint-sel\" :text=\"document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''\"></span> <span class=\"hint-flash\" :text=\"document.body.dataset.flash || ''\"></span>")
921940 if templ_7745c5c3_Err != nil {
922941 return templ_7745c5c3_Err
923942 }
@@ -925,7 +944,7 @@ func Hint(l Layout) templ.Component {
925944 if templ_7745c5c3_Err != nil {
926945 return templ_7745c5c3_Err
927946 }
928- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "</div>")
947+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 91, "</div>")
929948 if templ_7745c5c3_Err != nil {
930949 return templ_7745c5c3_Err
931950 }
@@ -426,6 +426,25 @@ func Shell(l Layout, rail templ.Component) templ.Component {
426 if templ_7745c5c3_Err != nil {426 if templ_7745c5c3_Err != nil {
427 return templ_7745c5c3_Err427 return templ_7745c5c3_Err
428 }428 }
429+ if SignedIn(ctx) {
430+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "<a hidden data-inbox href=\"")
431+ if templ_7745c5c3_Err != nil {
432+ return templ_7745c5c3_Err
433+ }
434+ var templ_7745c5c3_Var26 templ.SafeURL
435+ templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinURLErrs(inboxPath)
436+ if templ_7745c5c3_Err != nil {
437+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 71, Col: 41}
438+ }
439+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
440+ if templ_7745c5c3_Err != nil {
441+ return templ_7745c5c3_Err
442+ }
443+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "\">Inbox</a>")
444+ if templ_7745c5c3_Err != nil {
445+ return templ_7745c5c3_Err
446+ }
447+ }
429 templ_7745c5c3_Err = OverlayHelp(l.MR).Render(ctx, templ_7745c5c3_Buffer)448 templ_7745c5c3_Err = OverlayHelp(l.MR).Render(ctx, templ_7745c5c3_Buffer)
430 if templ_7745c5c3_Err != nil {449 if templ_7745c5c3_Err != nil {
431 return templ_7745c5c3_Err450 return templ_7745c5c3_Err
@@ -442,7 +461,7 @@ func Shell(l Layout, rail templ.Component) templ.Component {
442 if templ_7745c5c3_Err != nil {461 if templ_7745c5c3_Err != nil {
443 return templ_7745c5c3_Err462 return templ_7745c5c3_Err
444 }463 }
445- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</body></html>")464+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</body></html>")
446 if templ_7745c5c3_Err != nil {465 if templ_7745c5c3_Err != nil {
447 return templ_7745c5c3_Err466 return templ_7745c5c3_Err
448 }467 }
@@ -468,366 +487,366 @@ func Header(l Layout) templ.Component {
468 }()487 }()
469 }488 }
470 ctx = templ.InitializeContext(ctx)489 ctx = templ.InitializeContext(ctx)
471- templ_7745c5c3_Var26 := templ.GetChildren(ctx)490+ templ_7745c5c3_Var27 := templ.GetChildren(ctx)
472- if templ_7745c5c3_Var26 == nil {491+ if templ_7745c5c3_Var27 == nil {
473- templ_7745c5c3_Var26 = templ.NopComponent492+ templ_7745c5c3_Var27 = templ.NopComponent
474 }493 }
475 ctx = templ.ClearChildren(ctx)494 ctx = templ.ClearChildren(ctx)
476- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "<header class=\"hdr\"><div class=\"hdr-top\"><a class=\"brand\" href=\"/\" data-g=\"h\" aria-label=\"home\" title=\"home (g h)\"><span class=\"brand-mark\" aria-hidden=\"true\"></span></a> ")495+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "<header class=\"hdr\"><div class=\"hdr-top\"><a class=\"brand\" href=\"/\" data-g=\"h\" aria-label=\"home\" title=\"home (g h)\"><span class=\"brand-mark\" aria-hidden=\"true\"></span></a> ")
477 if templ_7745c5c3_Err != nil {496 if templ_7745c5c3_Err != nil {
478 return templ_7745c5c3_Err497 return templ_7745c5c3_Err
479 }498 }
480 if l.Inbox != nil {499 if l.Inbox != nil {
481- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "<a class=\"btn inbox-btn\" href=\"")500+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "<a class=\"btn inbox-btn\" href=\"")
482 if templ_7745c5c3_Err != nil {501 if templ_7745c5c3_Err != nil {
483 return templ_7745c5c3_Err502 return templ_7745c5c3_Err
484 }503 }
485- var templ_7745c5c3_Var27 templ.SafeURL504+ var templ_7745c5c3_Var28 templ.SafeURL
486- templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.Inbox.Href))505+ templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.Inbox.Href))
487 if templ_7745c5c3_Err != nil {506 if templ_7745c5c3_Err != nil {
488- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 89, Col: 39}507+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 92, Col: 39}
489 }508 }
490- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))509+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
491 if templ_7745c5c3_Err != nil {510 if templ_7745c5c3_Err != nil {
492 return templ_7745c5c3_Err511 return templ_7745c5c3_Err
493 }512 }
494- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "\" title=\"inbox\"><span class=\"ico i-mailbox\" aria-hidden=\"true\"></span> Inbox ")513+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "\" title=\"inbox (I)\" aria-keyshortcuts=\"Shift+I\"><span class=\"ico i-mailbox\" aria-hidden=\"true\"></span> Inbox ")
495 if templ_7745c5c3_Err != nil {514 if templ_7745c5c3_Err != nil {
496 return templ_7745c5c3_Err515 return templ_7745c5c3_Err
497 }516 }
498 if l.Inbox.Count >= 0 {517 if l.Inbox.Count >= 0 {
499- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "<span class=\"count\">")518+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "<span class=\"count\">")
500 if templ_7745c5c3_Err != nil {519 if templ_7745c5c3_Err != nil {
501 return templ_7745c5c3_Err520 return templ_7745c5c3_Err
502 }521 }
503- var templ_7745c5c3_Var28 string522+ var templ_7745c5c3_Var29 string
504- templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(l.Inbox.Count))523+ templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(l.Inbox.Count))
505 if templ_7745c5c3_Err != nil {524 if templ_7745c5c3_Err != nil {
506- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 95, Col: 47}525+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 99, Col: 47}
507 }526 }
508- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))527+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
509 if templ_7745c5c3_Err != nil {528 if templ_7745c5c3_Err != nil {
510 return templ_7745c5c3_Err529 return templ_7745c5c3_Err
511 }530 }
512- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "</span>")531+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "</span>")
513 if templ_7745c5c3_Err != nil {532 if templ_7745c5c3_Err != nil {
514 return templ_7745c5c3_Err533 return templ_7745c5c3_Err
515 }534 }
516 }535 }
517- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "</a> ")536+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "</a> ")
518 if templ_7745c5c3_Err != nil {537 if templ_7745c5c3_Err != nil {
519 return templ_7745c5c3_Err538 return templ_7745c5c3_Err
520 }539 }
521 }540 }
522 if len(l.Crumbs) > 0 {541 if len(l.Crumbs) > 0 {
523- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "<nav class=\"crumbs\" aria-label=\"Path\"><ol>")542+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "<nav class=\"crumbs\" aria-label=\"Path\"><ol>")
524 if templ_7745c5c3_Err != nil {543 if templ_7745c5c3_Err != nil {
525 return templ_7745c5c3_Err544 return templ_7745c5c3_Err
526 }545 }
527 for _, c := range l.Crumbs {546 for _, c := range l.Crumbs {
528- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "<li>")547+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "<li>")
529 if templ_7745c5c3_Err != nil {548 if templ_7745c5c3_Err != nil {
530 return templ_7745c5c3_Err549 return templ_7745c5c3_Err
531 }550 }
532 if c.Href != "" {551 if c.Href != "" {
533- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "<a href=\"")552+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "<a href=\"")
534 if templ_7745c5c3_Err != nil {553 if templ_7745c5c3_Err != nil {
535 return templ_7745c5c3_Err554 return templ_7745c5c3_Err
536 }555 }
537- var templ_7745c5c3_Var29 templ.SafeURL556+ var templ_7745c5c3_Var30 templ.SafeURL
538- templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Href))557+ templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Href))
539 if templ_7745c5c3_Err != nil {558 if templ_7745c5c3_Err != nil {
540- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 106, Col: 38}559+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 110, Col: 38}
541 }560 }
542- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))561+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
543 if templ_7745c5c3_Err != nil {562 if templ_7745c5c3_Err != nil {
544 return templ_7745c5c3_Err563 return templ_7745c5c3_Err
545 }564 }
546- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "\"")565+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "\"")
547 if templ_7745c5c3_Err != nil {566 if templ_7745c5c3_Err != nil {
548 return templ_7745c5c3_Err567 return templ_7745c5c3_Err
549 }568 }
550 if c.Current {569 if c.Current {
551- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, " aria-current=\"page\"")570+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, " aria-current=\"page\"")
552 if templ_7745c5c3_Err != nil {571 if templ_7745c5c3_Err != nil {
553 return templ_7745c5c3_Err572 return templ_7745c5c3_Err
554 }573 }
555 }574 }
556- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, ">")575+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, ">")
557 if templ_7745c5c3_Err != nil {576 if templ_7745c5c3_Err != nil {
558 return templ_7745c5c3_Err577 return templ_7745c5c3_Err
559 }578 }
560- var templ_7745c5c3_Var30 string579+ var templ_7745c5c3_Var31 string
561- templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label)580+ templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label)
562 if templ_7745c5c3_Err != nil {581 if templ_7745c5c3_Err != nil {
563- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 110, Col: 19}582+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 114, Col: 19}
564 }583 }
565- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))584+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
566 if templ_7745c5c3_Err != nil {585 if templ_7745c5c3_Err != nil {
567 return templ_7745c5c3_Err586 return templ_7745c5c3_Err
568 }587 }
569- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "</a>")588+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "</a>")
570 if templ_7745c5c3_Err != nil {589 if templ_7745c5c3_Err != nil {
571 return templ_7745c5c3_Err590 return templ_7745c5c3_Err
572 }591 }
573 } else {592 } else {
574- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "<span")593+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "<span")
575 if templ_7745c5c3_Err != nil {594 if templ_7745c5c3_Err != nil {
576 return templ_7745c5c3_Err595 return templ_7745c5c3_Err
577 }596 }
578 if c.Current {597 if c.Current {
579- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, " aria-current=\"page\"")598+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, " aria-current=\"page\"")
580 if templ_7745c5c3_Err != nil {599 if templ_7745c5c3_Err != nil {
581 return templ_7745c5c3_Err600 return templ_7745c5c3_Err
582 }601 }
583 }602 }
584- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, ">")603+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, ">")
585 if templ_7745c5c3_Err != nil {604 if templ_7745c5c3_Err != nil {
586 return templ_7745c5c3_Err605 return templ_7745c5c3_Err
587 }606 }
588- var templ_7745c5c3_Var31 string607+ var templ_7745c5c3_Var32 string
589- templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label)608+ templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label)
590 if templ_7745c5c3_Err != nil {609 if templ_7745c5c3_Err != nil {
591- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 116, Col: 19}610+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 120, Col: 19}
592 }611 }
593- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))612+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
594 if templ_7745c5c3_Err != nil {613 if templ_7745c5c3_Err != nil {
595 return templ_7745c5c3_Err614 return templ_7745c5c3_Err
596 }615 }
597- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "</span>")616+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "</span>")
598 if templ_7745c5c3_Err != nil {617 if templ_7745c5c3_Err != nil {
599 return templ_7745c5c3_Err618 return templ_7745c5c3_Err
600 }619 }
601 }620 }
602- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "</li>")621+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "</li>")
603 if templ_7745c5c3_Err != nil {622 if templ_7745c5c3_Err != nil {
604 return templ_7745c5c3_Err623 return templ_7745c5c3_Err
605 }624 }
606 }625 }
607- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "</ol></nav>")626+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "</ol></nav>")
608 if templ_7745c5c3_Err != nil {627 if templ_7745c5c3_Err != nil {
609 return templ_7745c5c3_Err628 return templ_7745c5c3_Err
610 }629 }
611 }630 }
612- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "<div class=\"hdr-acts\">")631+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "<div class=\"hdr-acts\">")
613 if templ_7745c5c3_Err != nil {632 if templ_7745c5c3_Err != nil {
614 return templ_7745c5c3_Err633 return templ_7745c5c3_Err
615 }634 }
616 if l.Ref != "" {635 if l.Ref != "" {
617- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "<a class=\"btn ref\" href=\"")636+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "<a class=\"btn ref\" href=\"")
618 if templ_7745c5c3_Err != nil {637 if templ_7745c5c3_Err != nil {
619 return templ_7745c5c3_Err638 return templ_7745c5c3_Err
620 }639 }
621- var templ_7745c5c3_Var32 templ.SafeURL640+ var templ_7745c5c3_Var33 templ.SafeURL
622- templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(repoHref(l.Repo, "refs")))641+ templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(repoHref(l.Repo, "refs")))
623 if templ_7745c5c3_Err != nil {642 if templ_7745c5c3_Err != nil {
624- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 127, Col: 52}643+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 131, Col: 52}
625 }644 }
626- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))645+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))
627 if templ_7745c5c3_Err != nil {646 if templ_7745c5c3_Err != nil {
628 return templ_7745c5c3_Err647 return templ_7745c5c3_Err
629 }648 }
630- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "\" data-fire=\"refs\" aria-keyshortcuts=\"b\" title=\"switch branch or tag (b)\"><span class=\"ico i-branch\" aria-hidden=\"true\"></span> <span class=\"ref-name\">")649+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "\" data-fire=\"refs\" aria-keyshortcuts=\"b\" title=\"switch branch or tag (b)\"><span class=\"ico i-branch\" aria-hidden=\"true\"></span> <span class=\"ref-name\">")
631 if templ_7745c5c3_Err != nil {650 if templ_7745c5c3_Err != nil {
632 return templ_7745c5c3_Err651 return templ_7745c5c3_Err
633 }652 }
634- var templ_7745c5c3_Var33 string653+ var templ_7745c5c3_Var34 string
635- templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(l.Ref)654+ templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(l.Ref)
636 if templ_7745c5c3_Err != nil {655 if templ_7745c5c3_Err != nil {
637- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 133, Col: 36}656+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 137, Col: 36}
638 }657 }
639- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))658+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34))
640 if templ_7745c5c3_Err != nil {659 if templ_7745c5c3_Err != nil {
641 return templ_7745c5c3_Err660 return templ_7745c5c3_Err
642 }661 }
643- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "</span> <span class=\"sha\">")662+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "</span> <span class=\"sha\">")
644 if templ_7745c5c3_Err != nil {663 if templ_7745c5c3_Err != nil {
645 return templ_7745c5c3_Err664 return templ_7745c5c3_Err
646 }665 }
647- var templ_7745c5c3_Var34 string666+ var templ_7745c5c3_Var35 string
648- templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(l.Short)667+ templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(l.Short)
649 if templ_7745c5c3_Err != nil {668 if templ_7745c5c3_Err != nil {
650- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 134, Col: 33}669+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 138, Col: 33}
651 }670 }
652- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34))671+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35))
653 if templ_7745c5c3_Err != nil {672 if templ_7745c5c3_Err != nil {
654 return templ_7745c5c3_Err673 return templ_7745c5c3_Err
655 }674 }
656- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "</span></a> ")675+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "</span></a> ")
657 if templ_7745c5c3_Err != nil {676 if templ_7745c5c3_Err != nil {
658 return templ_7745c5c3_Err677 return templ_7745c5c3_Err
659 }678 }
660 }679 }
661- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "<button type=\"button\" class=\"btn search\" data-js aria-haspopup=\"dialog\" aria-controls=\"panel-cmd\" aria-keyshortcuts=\"Control+K :\" title=\"go to a section, ref, file or repo (ctrl k or :)\" data-fire=\"cmd\"><span class=\"ico i-search\" aria-hidden=\"true\"></span> <span class=\"search-label\">Go to…</span> <kbd aria-hidden=\"true\">ctrl k</kbd></button> ")680+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "<button type=\"button\" class=\"btn search\" data-js aria-haspopup=\"dialog\" aria-controls=\"panel-cmd\" aria-keyshortcuts=\"Control+K :\" title=\"go to a section, ref, file or repo (ctrl k or :)\" data-fire=\"cmd\"><span class=\"ico i-search\" aria-hidden=\"true\"></span> <span class=\"search-label\">Go to…</span> <kbd aria-hidden=\"true\">ctrl k</kbd></button> ")
662 if templ_7745c5c3_Err != nil {681 if templ_7745c5c3_Err != nil {
663 return templ_7745c5c3_Err682 return templ_7745c5c3_Err
664 }683 }
665 if SignedIn(ctx) {684 if SignedIn(ctx) {
666- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "<form class=\"owner\" method=\"post\" action=\"/-/logout\"><span class=\"owner-badge\" title=\"signed in as the owner\"><span class=\"ico i-user\" aria-hidden=\"true\"></span>")685+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "<form class=\"owner\" method=\"post\" action=\"/-/logout\"><span class=\"owner-badge\" title=\"signed in as the owner\"><span class=\"ico i-user\" aria-hidden=\"true\"></span>")
667 if templ_7745c5c3_Err != nil {686 if templ_7745c5c3_Err != nil {
668 return templ_7745c5c3_Err687 return templ_7745c5c3_Err
669 }688 }
670- var templ_7745c5c3_Var35 string689+ var templ_7745c5c3_Var36 string
671- templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(l.Owner)690+ templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(l.Owner)
672 if templ_7745c5c3_Err != nil {691 if templ_7745c5c3_Err != nil {
673- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 153, Col: 123}692+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 157, Col: 123}
674 }693 }
675- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35))694+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36))
676 if templ_7745c5c3_Err != nil {695 if templ_7745c5c3_Err != nil {
677 return templ_7745c5c3_Err696 return templ_7745c5c3_Err
678 }697 }
679- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "</span> ")698+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "</span> ")
680 if templ_7745c5c3_Err != nil {699 if templ_7745c5c3_Err != nil {
681 return templ_7745c5c3_Err700 return templ_7745c5c3_Err
682 }701 }
683 if l.Passkeys {702 if l.Passkeys {
684- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "<a class=\"btn icon\" href=\"/-/passkeys\" aria-label=\"Passkeys\" title=\"passkeys: add or remove the passkeys that sign you in\"><span class=\"ico i-key\" aria-hidden=\"true\"></span></a> ")703+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "<a class=\"btn icon\" href=\"/-/passkeys\" aria-label=\"Passkeys\" title=\"passkeys: add or remove the passkeys that sign you in\"><span class=\"ico i-key\" aria-hidden=\"true\"></span></a> ")
685 if templ_7745c5c3_Err != nil {704 if templ_7745c5c3_Err != nil {
686 return templ_7745c5c3_Err705 return templ_7745c5c3_Err
687 }706 }
688 }707 }
689- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "<button type=\"submit\" class=\"btn icon\" aria-label=\"Sign out\" title=\"sign out of this browser\"><span class=\"ico i-log-out\" aria-hidden=\"true\"></span></button></form>")708+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "<button type=\"submit\" class=\"btn icon\" aria-label=\"Sign out\" title=\"sign out of this browser\"><span class=\"ico i-log-out\" aria-hidden=\"true\"></span></button></form>")
690 if templ_7745c5c3_Err != nil {709 if templ_7745c5c3_Err != nil {
691 return templ_7745c5c3_Err710 return templ_7745c5c3_Err
692 }711 }
693 } else if l.Passkeys {712 } else if l.Passkeys {
694- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "<a class=\"btn icon\" href=\"/-/login\" aria-label=\"Sign in\" title=\"sign in with a passkey\"><span class=\"ico i-log-in\" aria-hidden=\"true\"></span></a>")713+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "<a class=\"btn icon\" href=\"/-/login\" aria-label=\"Sign in\" title=\"sign in with a passkey\"><span class=\"ico i-log-in\" aria-hidden=\"true\"></span></a>")
695 if templ_7745c5c3_Err != nil {714 if templ_7745c5c3_Err != nil {
696 return templ_7745c5c3_Err715 return templ_7745c5c3_Err
697 }716 }
698 }717 }
699- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "</div></div>")718+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "</div></div>")
700 if templ_7745c5c3_Err != nil {719 if templ_7745c5c3_Err != nil {
701 return templ_7745c5c3_Err720 return templ_7745c5c3_Err
702 }721 }
703 if len(l.Sections) > 0 {722 if len(l.Sections) > 0 {
704- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "<nav class=\"tabs\" aria-label=\"Sections\"><ul>")723+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "<nav class=\"tabs\" aria-label=\"Sections\"><ul>")
705 if templ_7745c5c3_Err != nil {724 if templ_7745c5c3_Err != nil {
706 return templ_7745c5c3_Err725 return templ_7745c5c3_Err
707 }726 }
708 for _, s := range l.Sections {727 for _, s := range l.Sections {
709- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "<li><a href=\"")728+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "<li><a href=\"")
710 if templ_7745c5c3_Err != nil {729 if templ_7745c5c3_Err != nil {
711 return templ_7745c5c3_Err730 return templ_7745c5c3_Err
712 }731 }
713- var templ_7745c5c3_Var36 templ.SafeURL732+ var templ_7745c5c3_Var37 templ.SafeURL
714- templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(s.Href))733+ templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(s.Href))
715 if templ_7745c5c3_Err != nil {734 if templ_7745c5c3_Err != nil {
716- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 170, Col: 36}735+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 174, Col: 36}
717 }736 }
718- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36))737+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37))
719 if templ_7745c5c3_Err != nil {738 if templ_7745c5c3_Err != nil {
720 return templ_7745c5c3_Err739 return templ_7745c5c3_Err
721 }740 }
722- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "\" data-key=\"")741+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "\" data-key=\"")
723 if templ_7745c5c3_Err != nil {742 if templ_7745c5c3_Err != nil {
724 return templ_7745c5c3_Err743 return templ_7745c5c3_Err
725 }744 }
726- var templ_7745c5c3_Var37 string745+ var templ_7745c5c3_Var38 string
727- templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key)746+ templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key)
728 if templ_7745c5c3_Err != nil {747 if templ_7745c5c3_Err != nil {
729- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 171, Col: 24}748+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 175, Col: 24}
730 }749 }
731- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var37)750+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var38)
732 if templ_7745c5c3_Err != nil {751 if templ_7745c5c3_Err != nil {
733 return templ_7745c5c3_Err752 return templ_7745c5c3_Err
734 }753 }
735- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "\" aria-keyshortcuts=\"")754+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "\" aria-keyshortcuts=\"")
736 if templ_7745c5c3_Err != nil {755 if templ_7745c5c3_Err != nil {
737 return templ_7745c5c3_Err756 return templ_7745c5c3_Err
738 }757 }
739- var templ_7745c5c3_Var38 string758+ var templ_7745c5c3_Var39 string
740- templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key)759+ templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key)
741 if templ_7745c5c3_Err != nil {760 if templ_7745c5c3_Err != nil {
742- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 172, Col: 33}761+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 176, Col: 33}
743 }762 }
744- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var38)763+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var39)
745 if templ_7745c5c3_Err != nil {764 if templ_7745c5c3_Err != nil {
746 return templ_7745c5c3_Err765 return templ_7745c5c3_Err
747 }766 }
748- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "\"")767+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "\"")
749 if templ_7745c5c3_Err != nil {768 if templ_7745c5c3_Err != nil {
750 return templ_7745c5c3_Err769 return templ_7745c5c3_Err
751 }770 }
752 if s.Current {771 if s.Current {
753- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, " aria-current=\"page\"")772+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, " aria-current=\"page\"")
754 if templ_7745c5c3_Err != nil {773 if templ_7745c5c3_Err != nil {
755 return templ_7745c5c3_Err774 return templ_7745c5c3_Err
756 }775 }
757 }776 }
758- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, ">")777+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, ">")
759 if templ_7745c5c3_Err != nil {778 if templ_7745c5c3_Err != nil {
760 return templ_7745c5c3_Err779 return templ_7745c5c3_Err
761 }780 }
762- var templ_7745c5c3_Var39 string781+ var templ_7745c5c3_Var40 string
763- templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(s.Label)782+ templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(s.Label)
764 if templ_7745c5c3_Err != nil {783 if templ_7745c5c3_Err != nil {
765- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 177, Col: 17}784+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 181, Col: 17}
766 }785 }
767- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39))786+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40))
768 if templ_7745c5c3_Err != nil {787 if templ_7745c5c3_Err != nil {
769 return templ_7745c5c3_Err788 return templ_7745c5c3_Err
770 }789 }
771- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, " ")790+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, " ")
772 if templ_7745c5c3_Err != nil {791 if templ_7745c5c3_Err != nil {
773 return templ_7745c5c3_Err792 return templ_7745c5c3_Err
774 }793 }
775 if s.Count != "" {794 if s.Count != "" {
776- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "<span class=\"count\">")795+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "<span class=\"count\">")
777 if templ_7745c5c3_Err != nil {796 if templ_7745c5c3_Err != nil {
778 return templ_7745c5c3_Err797 return templ_7745c5c3_Err
779 }798 }
780- var templ_7745c5c3_Var40 string799+ var templ_7745c5c3_Var41 string
781- templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(s.Count)800+ templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(s.Count)
782 if templ_7745c5c3_Err != nil {801 if templ_7745c5c3_Err != nil {
783- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 179, Col: 38}802+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 183, Col: 38}
784 }803 }
785- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40))804+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41))
786 if templ_7745c5c3_Err != nil {805 if templ_7745c5c3_Err != nil {
787 return templ_7745c5c3_Err806 return templ_7745c5c3_Err
788 }807 }
789- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "<span class=\"sr\">")808+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "<span class=\"sr\">")
790 if templ_7745c5c3_Err != nil {809 if templ_7745c5c3_Err != nil {
791 return templ_7745c5c3_Err810 return templ_7745c5c3_Err
792 }811 }
793- var templ_7745c5c3_Var41 string812+ var templ_7745c5c3_Var42 string
794- templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(" open")813+ templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs(" open")
795 if templ_7745c5c3_Err != nil {814 if templ_7745c5c3_Err != nil {
796- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 179, Col: 66}815+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 183, Col: 66}
797 }816 }
798- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41))817+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42))
799 if templ_7745c5c3_Err != nil {818 if templ_7745c5c3_Err != nil {
800 return templ_7745c5c3_Err819 return templ_7745c5c3_Err
801 }820 }
802- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "</span></span> ")821+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "</span></span> ")
803 if templ_7745c5c3_Err != nil {822 if templ_7745c5c3_Err != nil {
804 return templ_7745c5c3_Err823 return templ_7745c5c3_Err
805 }824 }
806 }825 }
807- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "<kbd aria-hidden=\"true\">")826+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "<kbd aria-hidden=\"true\">")
808 if templ_7745c5c3_Err != nil {827 if templ_7745c5c3_Err != nil {
809 return templ_7745c5c3_Err828 return templ_7745c5c3_Err
810 }829 }
811- var templ_7745c5c3_Var42 string830+ var templ_7745c5c3_Var43 string
812- templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs(s.Key)831+ templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(s.Key)
813 if templ_7745c5c3_Err != nil {832 if templ_7745c5c3_Err != nil {
814- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 181, Col: 39}833+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 185, Col: 39}
815 }834 }
816- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42))835+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43))
817 if templ_7745c5c3_Err != nil {836 if templ_7745c5c3_Err != nil {
818 return templ_7745c5c3_Err837 return templ_7745c5c3_Err
819 }838 }
820- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "</kbd></a></li>")839+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "</kbd></a></li>")
821 if templ_7745c5c3_Err != nil {840 if templ_7745c5c3_Err != nil {
822 return templ_7745c5c3_Err841 return templ_7745c5c3_Err
823 }842 }
824 }843 }
825- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "</ul></nav>")844+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "</ul></nav>")
826 if templ_7745c5c3_Err != nil {845 if templ_7745c5c3_Err != nil {
827 return templ_7745c5c3_Err846 return templ_7745c5c3_Err
828 }847 }
829 }848 }
830- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "</header>")849+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, "</header>")
831 if templ_7745c5c3_Err != nil {850 if templ_7745c5c3_Err != nil {
832 return templ_7745c5c3_Err851 return templ_7745c5c3_Err
833 }852 }
@@ -853,38 +872,38 @@ func Footer(l Layout) templ.Component {
853 }()872 }()
854 }873 }
855 ctx = templ.InitializeContext(ctx)874 ctx = templ.InitializeContext(ctx)
856- templ_7745c5c3_Var43 := templ.GetChildren(ctx)875+ templ_7745c5c3_Var44 := templ.GetChildren(ctx)
857- if templ_7745c5c3_Var43 == nil {876+ if templ_7745c5c3_Var44 == nil {
858- templ_7745c5c3_Var43 = templ.NopComponent877+ templ_7745c5c3_Var44 = templ.NopComponent
859 }878 }
860 ctx = templ.ClearChildren(ctx)879 ctx = templ.ClearChildren(ctx)
861- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "<footer class=\"foot\"><span>ermite ")880+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, "<footer class=\"foot\"><span>ermite ")
862 if templ_7745c5c3_Err != nil {881 if templ_7745c5c3_Err != nil {
863 return templ_7745c5c3_Err882 return templ_7745c5c3_Err
864 }883 }
865- var templ_7745c5c3_Var44 string884+ var templ_7745c5c3_Var45 string
866- templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(l.Version)885+ templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinStringErrs(l.Version)
867 if templ_7745c5c3_Err != nil {886 if templ_7745c5c3_Err != nil {
868- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 195, Col: 26}887+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 199, Col: 26}
869 }888 }
870- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44))889+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45))
871 if templ_7745c5c3_Err != nil {890 if templ_7745c5c3_Err != nil {
872 return templ_7745c5c3_Err891 return templ_7745c5c3_Err
873 }892 }
874- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, "</span> <a href=\"")893+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "</span> <a href=\"")
875 if templ_7745c5c3_Err != nil {894 if templ_7745c5c3_Err != nil {
876 return templ_7745c5c3_Err895 return templ_7745c5c3_Err
877 }896 }
878- var templ_7745c5c3_Var45 templ.SafeURL897+ var templ_7745c5c3_Var46 templ.SafeURL
879- templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.SourceURL))898+ templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.SourceURL))
880 if templ_7745c5c3_Err != nil {899 if templ_7745c5c3_Err != nil {
881- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 196, Col: 38}900+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 200, Col: 38}
882 }901 }
883- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45))902+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var46))
884 if templ_7745c5c3_Err != nil {903 if templ_7745c5c3_Err != nil {
885 return templ_7745c5c3_Err904 return templ_7745c5c3_Err
886 }905 }
887- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, "\" rel=\"noopener\" title=\"source of this version (AGPL-3.0-or-later)\">source</a> <button type=\"button\" class=\"foot-keys\" data-js data-fire=\"help\" aria-haspopup=\"dialog\" aria-controls=\"panel-help\" aria-keyshortcuts=\"?\"><kbd aria-hidden=\"true\">?</kbd> keyboard shortcuts</button></footer>")906+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "\" rel=\"noopener\" title=\"source of this version (AGPL-3.0-or-later)\">source</a> <button type=\"button\" class=\"foot-keys\" data-js data-fire=\"help\" aria-haspopup=\"dialog\" aria-controls=\"panel-help\" aria-keyshortcuts=\"?\"><kbd aria-hidden=\"true\">?</kbd> keyboard shortcuts</button></footer>")
888 if templ_7745c5c3_Err != nil {907 if templ_7745c5c3_Err != nil {
889 return templ_7745c5c3_Err908 return templ_7745c5c3_Err
890 }909 }
@@ -912,12 +931,12 @@ func Hint(l Layout) templ.Component {
912 }()931 }()
913 }932 }
914 ctx = templ.InitializeContext(ctx)933 ctx = templ.InitializeContext(ctx)
915- templ_7745c5c3_Var46 := templ.GetChildren(ctx)934+ templ_7745c5c3_Var47 := templ.GetChildren(ctx)
916- if templ_7745c5c3_Var46 == nil {935+ if templ_7745c5c3_Var47 == nil {
917- templ_7745c5c3_Var46 = templ.NopComponent936+ templ_7745c5c3_Var47 = templ.NopComponent
918 }937 }
919 ctx = templ.ClearChildren(ctx)938 ctx = templ.ClearChildren(ctx)
920- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "<div class=\"hint\" role=\"status\" aria-live=\"polite\"><span class=\"hint-nav\" :text=\"document.body.dataset.nav || ''\"></span> <span class=\"hint-sel\" :text=\"document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''\"></span> <span class=\"hint-flash\" :text=\"document.body.dataset.flash || ''\"></span>")939+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 90, "<div class=\"hint\" role=\"status\" aria-live=\"polite\"><span class=\"hint-nav\" :text=\"document.body.dataset.nav || ''\"></span> <span class=\"hint-sel\" :text=\"document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''\"></span> <span class=\"hint-flash\" :text=\"document.body.dataset.flash || ''\"></span>")
921 if templ_7745c5c3_Err != nil {940 if templ_7745c5c3_Err != nil {
922 return templ_7745c5c3_Err941 return templ_7745c5c3_Err
923 }942 }
@@ -925,7 +944,7 @@ func Hint(l Layout) templ.Component {
925 if templ_7745c5c3_Err != nil {944 if templ_7745c5c3_Err != nil {
926 return templ_7745c5c3_Err945 return templ_7745c5c3_Err
927 }946 }
928- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "</div>")947+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 91, "</div>")
929 if templ_7745c5c3_Err != nil {948 if templ_7745c5c3_Err != nil {
930 return templ_7745c5c3_Err949 return templ_7745c5c3_Err
931 }950 }
M internal/adapters/web/static/js/ermite.js +2 -2
@@ -612,7 +612,7 @@
612612
613613 // 1. Keys.
614614 const single = { j: 'j', k: 'k', G: 'bottom', '/': 'filter', '?': 'help', b: 'refs', f: 'cmd', ':': 'cmd', y: 'permalink',
615- s: 's', t: 't', c: 'c', r: 'r', m: 'm', i: 'i', n: 'next', h: 'left', l: 'right', d: 'split', u: 'unified', x: 'collapse', w: 'wide', e: 'expand', E: 'expandAll', Enter: 'open', Escape: 'esc',
615+ s: 's', t: 't', c: 'c', r: 'r', m: 'm', i: 'i', I: 'inbox', n: 'next', h: 'left', l: 'right', d: 'split', u: 'unified', x: 'collapse', w: 'wide', e: 'expand', E: 'expandAll', Enter: 'open', Escape: 'esc',
616616 // the merge request page (mr.js): the merge menu, mark read
617617 M: 'merge', R: 'read' };
618618 // "g t" is the key "t": the tabs have one event each.
@@ -654,7 +654,7 @@
654654 // page-level keys, as on the other lists.
655655 if (!e.shiftKey && e.target.closest?.('#tree-rows') && window.ermiteTree?.arrow(e.key)) { e.preventDefault(); return; }
656656 const name = single[e.key];
657- if (!name) return;
657+ if (!name || (name === 'inbox' && !$('[data-inbox]'))) return;
658658 if (e.key !== 'Enter' || !$('dialog[open]')) e.preventDefault();
659659 ermite.fire(name);
660660 });
@@ -612,7 +612,7 @@
612 612
613 // 1. Keys.613 // 1. Keys.
614 const single = { j: 'j', k: 'k', G: 'bottom', '/': 'filter', '?': 'help', b: 'refs', f: 'cmd', ':': 'cmd', y: 'permalink',614 const single = { j: 'j', k: 'k', G: 'bottom', '/': 'filter', '?': 'help', b: 'refs', f: 'cmd', ':': 'cmd', y: 'permalink',
615- s: 's', t: 't', c: 'c', r: 'r', m: 'm', i: 'i', n: 'next', h: 'left', l: 'right', d: 'split', u: 'unified', x: 'collapse', w: 'wide', e: 'expand', E: 'expandAll', Enter: 'open', Escape: 'esc',615+ s: 's', t: 't', c: 'c', r: 'r', m: 'm', i: 'i', I: 'inbox', n: 'next', h: 'left', l: 'right', d: 'split', u: 'unified', x: 'collapse', w: 'wide', e: 'expand', E: 'expandAll', Enter: 'open', Escape: 'esc',
616 // the merge request page (mr.js): the merge menu, mark read616 // the merge request page (mr.js): the merge menu, mark read
617 M: 'merge', R: 'read' };617 M: 'merge', R: 'read' };
618 // "g t" is the key "t": the tabs have one event each.618 // "g t" is the key "t": the tabs have one event each.
@@ -654,7 +654,7 @@
654 // page-level keys, as on the other lists.654 // page-level keys, as on the other lists.
655 if (!e.shiftKey && e.target.closest?.('#tree-rows') && window.ermiteTree?.arrow(e.key)) { e.preventDefault(); return; }655 if (!e.shiftKey && e.target.closest?.('#tree-rows') && window.ermiteTree?.arrow(e.key)) { e.preventDefault(); return; }
656 const name = single[e.key];656 const name = single[e.key];
657- if (!name) return;657+ if (!name || (name === 'inbox' && !$('[data-inbox]'))) return;
658 if (e.key !== 'Enter' || !$('dialog[open]')) e.preventDefault();658 if (e.key !== 'Enter' || !$('dialog[open]')) e.preventDefault();
659 ermite.fire(name);659 ermite.fire(name);
660 });660 });
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(),
415+ Meta: store, MergeRequests: store.MergeRequests(), Issues: store.Issues(), Pending: store.PendingIssues(), CI: store.CI(),
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(),415+ Meta: store, MergeRequests: store.MergeRequests(), Issues: store.Issues(), Pending: store.PendingIssues(), CI: store.CI(),
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 +1 -0
@@ -428,6 +428,7 @@
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]');
431432 k-next from:body -> ermite.nextNote();
432433 k-split from:body -> ermite.layout('split');
433434 k-unified from:body -> ermite.layout('unified');
@@ -428,6 +428,7 @@
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]');
431 k-next from:body -> ermite.nextNote();432 k-next from:body -> ermite.nextNote();
432 k-split from:body -> ermite.layout('split');433 k-split from:body -> ermite.layout('split');
433 k-unified from:body -> ermite.layout('unified');434 k-unified from:body -> ermite.layout('unified');
M internal/app/testdata/web/blob-code.html +1 -0
@@ -498,6 +498,7 @@
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]');
501502 k-next from:body -> ermite.nextNote();
502503 k-split from:body -> ermite.layout('split');
503504 k-unified from:body -> ermite.layout('unified');
@@ -498,6 +498,7 @@
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]');
501 k-next from:body -> ermite.nextNote();502 k-next from:body -> ermite.nextNote();
502 k-split from:body -> ermite.layout('split');503 k-split from:body -> ermite.layout('split');
503 k-unified from:body -> ermite.layout('unified');504 k-unified from:body -> ermite.layout('unified');
M internal/app/testdata/web/blob-empty.html +1 -0
@@ -428,6 +428,7 @@
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]');
431432 k-next from:body -> ermite.nextNote();
432433 k-split from:body -> ermite.layout('split');
433434 k-unified from:body -> ermite.layout('unified');
@@ -428,6 +428,7 @@
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]');
431 k-next from:body -> ermite.nextNote();432 k-next from:body -> ermite.nextNote();
432 k-split from:body -> ermite.layout('split');433 k-split from:body -> ermite.layout('split');
433 k-unified from:body -> ermite.layout('unified');434 k-unified from:body -> ermite.layout('unified');
M internal/app/testdata/web/blob-image.html +1 -0
@@ -430,6 +430,7 @@
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]');
433434 k-next from:body -> ermite.nextNote();
434435 k-split from:body -> ermite.layout('split');
435436 k-unified from:body -> ermite.layout('unified');
@@ -430,6 +430,7 @@
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]');
433 k-next from:body -> ermite.nextNote();434 k-next from:body -> ermite.nextNote();
434 k-split from:body -> ermite.layout('split');435 k-split from:body -> ermite.layout('split');
435 k-unified from:body -> ermite.layout('unified');436 k-unified from:body -> ermite.layout('unified');
M internal/app/testdata/web/blob-markdown.html +1 -0
@@ -495,6 +495,7 @@
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]');
498499 k-next from:body -> ermite.nextNote();
499500 k-split from:body -> ermite.layout('split');
500501 k-unified from:body -> ermite.layout('unified');
@@ -495,6 +495,7 @@
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]');
498 k-next from:body -> ermite.nextNote();499 k-next from:body -> ermite.nextNote();
499 k-split from:body -> ermite.layout('split');500 k-split from:body -> ermite.layout('split');
500 k-unified from:body -> ermite.layout('unified');501 k-unified from:body -> ermite.layout('unified');
M internal/app/testdata/web/blob-svg.html +1 -0
@@ -453,6 +453,7 @@
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]');
456457 k-next from:body -> ermite.nextNote();
457458 k-split from:body -> ermite.layout('split');
458459 k-unified from:body -> ermite.layout('unified');
@@ -453,6 +453,7 @@
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]');
456 k-next from:body -> ermite.nextNote();457 k-next from:body -> ermite.nextNote();
457 k-split from:body -> ermite.layout('split');458 k-split from:body -> ermite.layout('split');
458 k-unified from:body -> ermite.layout('unified');459 k-unified from:body -> ermite.layout('unified');
M internal/app/testdata/web/blob-text.html +1 -0
@@ -448,6 +448,7 @@
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]');
451452 k-next from:body -> ermite.nextNote();
452453 k-split from:body -> ermite.layout('split');
453454 k-unified from:body -> ermite.layout('unified');
@@ -448,6 +448,7 @@
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]');
451 k-next from:body -> ermite.nextNote();452 k-next from:body -> ermite.nextNote();
452 k-split from:body -> ermite.layout('split');453 k-split from:body -> ermite.layout('split');
453 k-unified from:body -> ermite.layout('unified');454 k-unified from:body -> ermite.layout('unified');
M internal/app/testdata/web/blob-too-large.html +1 -0
@@ -428,6 +428,7 @@
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]');
431432 k-next from:body -> ermite.nextNote();
432433 k-split from:body -> ermite.layout('split');
433434 k-unified from:body -> ermite.layout('unified');
@@ -428,6 +428,7 @@
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]');
431 k-next from:body -> ermite.nextNote();432 k-next from:body -> ermite.nextNote();
432 k-split from:body -> ermite.layout('split');433 k-split from:body -> ermite.layout('split');
433 k-unified from:body -> ermite.layout('unified');434 k-unified from:body -> ermite.layout('unified');
M internal/app/testdata/web/blob-trojan.html +1 -0
@@ -495,6 +495,7 @@
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]');
498499 k-next from:body -> ermite.nextNote();
499500 k-split from:body -> ermite.layout('split');
500501 k-unified from:body -> ermite.layout('unified');
@@ -495,6 +495,7 @@
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]');
498 k-next from:body -> ermite.nextNote();499 k-next from:body -> ermite.nextNote();
499 k-split from:body -> ermite.layout('split');500 k-split from:body -> ermite.layout('split');
500 k-unified from:body -> ermite.layout('unified');501 k-unified from:body -> ermite.layout('unified');
M internal/app/testdata/web/commit-big.html +1 -0
@@ -480,6 +480,7 @@
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]');
483484 k-next from:body -> ermite.nextNote();
484485 k-split from:body -> ermite.layout('split');
485486 k-unified from:body -> ermite.layout('unified');
@@ -480,6 +480,7 @@
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]');
483 k-next from:body -> ermite.nextNote();484 k-next from:body -> ermite.nextNote();
484 k-split from:body -> ermite.layout('split');485 k-split from:body -> ermite.layout('split');
485 k-unified from:body -> ermite.layout('unified');486 k-unified from:body -> ermite.layout('unified');
M internal/app/testdata/web/commit-binary.html +1 -0
M internal/app/testdata/web/commit-merge.html +1 -0
M internal/app/testdata/web/commit-modify-split.html +1 -0
M internal/app/testdata/web/commit-modify.html +1 -0
M internal/app/testdata/web/commit-rename.html +1 -0
M internal/app/testdata/web/commit-root.html +1 -0
M internal/app/testdata/web/commit-trojan.html +1 -0
M internal/app/testdata/web/commits-page2.html +1 -0
M internal/app/testdata/web/commits-path.html +1 -0
M internal/app/testdata/web/commits-root.html +1 -0
M internal/app/testdata/web/commits-small.html +1 -0
M internal/app/testdata/web/empty.html +1 -0
M internal/app/testdata/web/home.html +1 -0
M internal/app/testdata/web/not-found-page.html +1 -0
M internal/app/testdata/web/not-found.html +1 -0
M internal/app/testdata/web/refs.html +1 -0
M internal/app/testdata/web/summary.html +1 -0
M internal/app/testdata/web/tree-nested.html +1 -0
M internal/app/testdata/web/tree-root.html +1 -0
M internal/app/testdata/web/tree-slash-ref.html +1 -0
M internal/app/testdata/web/tree-subdir.html +1 -0
M internal/app/web_inbox_test.go +32 -2
M internal/ports/dto.go +6 -3
M internal/usecase/events.go +2 -2
M internal/usecase/inbox.go +31 -2
M internal/usecase/inbox_test.go +33 -0
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