tinyrelaycommit

Commit e39e529674a7

browse files at this commit | history

1diff --git a/internal/webui/collaboration.go b/internal/webui/collaboration.go
2index 0e2e2c3..06f8afe 100644
3--- a/internal/webui/collaboration.go
4+++ b/internal/webui/collaboration.go
5@@ -20,6 +20,32 @@ func collaborationSlice(value any, field string) []any {
6 func collaborationItems(value any) []any { return collaborationSlice(value, "items") }
7 func collaborationReplies(value any) []any { return collaborationSlice(value, "replies") }
8
9+// collaborationProposalView is how an issue, pull request or comment's
10+// proposal state reads for one viewer, with what the decision buttons need.
11+// Shown, Decide, State, At and By follow wikiProposalView.
12+type collaborationProposalView struct {
13+ wikiProposalView
14+ ID, Author, Kind string
15+}
16+
17+// collaborationProposal reads an item's proposal state for the viewer. The
18+// item is a list or detail item, whose author is under author, or a reply,
19+// whose author is under pubkey. canApprove is the browse result's
20+// can_approve flag.
21+func collaborationProposal(item any, actor string, canApprove any) collaborationProposalView {
22+ m := valueMap(item)
23+ if m == nil || m["proposal"] != true {
24+ return collaborationProposalView{}
25+ }
26+ author := plainString(m["author"])
27+ if author == "" {
28+ author = plainString(m["pubkey"])
29+ }
30+ view := collaborationProposalView{ID: plainString(m["id"]), Author: author, Kind: plainString(m["kind"])}
31+ view.wikiProposalView = wikiProposal(map[string]any{"proposal": true, "author": author, "approval": m["approval"], "approval_at": m["approval_at"], "approval_by": m["approval_by"]}, actor, canApprove)
32+ return view
33+}
34+
35 func collaborationLabels(value any) []string {
36 var labels []string
37 encoded, err := json.Marshal(valueMap(value)["labels"])
38diff --git a/internal/webui/collaboration.html b/internal/webui/collaboration.html
39index 34fcdc2..1647cd9 100644
40--- a/internal/webui/collaboration.html
41+++ b/internal/webui/collaboration.html
42@@ -8,7 +8,7 @@
43 </form>
44 {{$items := collabItems .Event}}{{if $items}}<table id="rows">
45 <thead><tr><th>status</th><th>title</th><th>author</th><th>opened</th></tr></thead>
46-<tbody>{{range $item := $items}}{{$item := asMap $item}}<tr><td>{{index $item "status"}}</td><td><a href="{{if $issues}}{{repoURL $.Query "issue" (str (index $item "id"))}}{{else}}{{repoURL $.Query "pr" (str (index $item "id"))}}{{end}}">{{index $item "title"}}</a>{{with collabLabels $item}} <small>{{join . ", "}}</small>{{end}}</td><td>{{template "nostrName" (str (index $item "author"))}}</td><td><time datetime="{{datetime (index $item "created_at")}}">{{when (index $item "created_at")}}</time></td></tr>
47+<tbody>{{range $item := $items}}{{$item := asMap $item}}{{$s := collabProposal $item $.Actor (index $data "can_approve")}}<tr{{if $s.Shown}} data-state="{{$s.State}}"{{end}}><td>{{index $item "status"}}</td><td><a href="{{if $issues}}{{repoURL $.Query "issue" (str (index $item "id"))}}{{else}}{{repoURL $.Query "pr" (str (index $item "id"))}}{{end}}">{{index $item "title"}}</a>{{if $s.Shown}} <small>proposed | {{$s.State}}</small>{{end}}{{with collabLabels $item}} <small>{{join . ", "}}</small>{{end}}</td><td>{{template "nostrName" (str (index $item "author"))}}</td><td><time datetime="{{datetime (index $item "created_at")}}">{{when (index $item "created_at")}}</time></td></tr>
48 {{end}}</tbody></table>{{else}}<p>No matching {{if $issues}}issues{{else}}pull requests{{end}}.</p>{{end}}
49 {{with index $data "next_cursor"}}<p id="pages"><a href="{{browsePageURL "/repo" $.Query "cursor" .}}">next page</a></p>{{end}}
50 {{if .Actor}}<details id="collaboration-new"><summary>{{if $issues}}New issue{{else}}New pull request{{end}}</summary>
51@@ -23,14 +23,14 @@
52 <h1>{{index $item "title"}}</h1>
53 <p>{{index $item "status"}} by {{template "nostrName" (str (index $item "author"))}} <time datetime="{{datetime (index $item "created_at")}}">{{when (index $item "created_at")}}</time></p>
54 {{with collabLabels $item}}<p>Labels: {{join . ", "}}</p>{{end}}
55-<article id="collaboration-body">{{markdown (str (index $item "content"))}}</article>
56+{{$can := index $data "can_approve"}}{{template "collaborationProposal" (collabProposal $item .Actor $can)}}<article id="collaboration-body">{{markdown (str (index $item "content"))}}</article>
57 {{if not $issues}}<h2>Changes</h2>{{if index $data "diff_unavailable"}}<p>{{index $data "diff_unavailable"}}. The diff will be available after the required commits have been synchronized.</p>{{else}}<diff-view><table><tbody>
58 {{range $line := $diff.Lines}}<tr id="D{{$line.Number}}"><td><a href="#D{{$line.Number}}">{{$line.Number}}</a></td><td><code>{{$line.HTML}}</code>{{with $line.CommentURL}} <small><a href="{{.}}">comment</a></small>{{end}}</td></tr>
59-{{range $c := $line.Comments}}{{$r := asMap $c}}<tr><td></td><td>{{template "nostrEvent" $c}}{{if $.Actor}}<details><summary>Reply to this comment</summary><nostr-compose kind="1111" coordinate="{{$coordinate}}" root="{{index $item "id"}}" root-pubkey="{{index $item "author"}}" root-kind="{{index $item "kind"}}" parent="{{index $r "id"}}" parent-pubkey="{{index $r "pubkey"}}" parent-kind="1111"><label>Reply <textarea name="content" required></textarea></label><button>Sign and reply</button></nostr-compose></details>{{end}}</td></tr>
60+{{range $c := $line.Comments}}{{$r := asMap $c}}<tr><td></td><td>{{template "collaborationProposal" (collabProposal $r $.Actor $can)}}{{template "nostrEvent" $c}}{{if $.Actor}}<details><summary>Reply to this comment</summary><nostr-compose kind="1111" coordinate="{{$coordinate}}" root="{{index $item "id"}}" root-pubkey="{{index $item "author"}}" root-kind="{{index $item "kind"}}" parent="{{index $r "id"}}" parent-pubkey="{{index $r "pubkey"}}" parent-kind="1111"><label>Reply <textarea name="content" required></textarea></label><button>Sign and reply</button></nostr-compose></details>{{end}}</td></tr>
61 {{end}}{{end}}</tbody>{{if $diff.Truncated}}<tfoot><tr><td colspan="2">Diff preview truncated (256 KiB or 10,000 lines).</td></tr></tfoot>{{end}}</table></diff-view>{{if index $data "diff_truncated"}}<p>This diff is truncated. Clone the repository to review the complete change.</p>{{end}}{{end}}{{end}}
62 <h2>Conversation</h2>
63 {{$replies := $diff.Thread}}{{if $replies}}<ol id="events">
64-{{range $reply := $replies}}{{$r := asMap $reply}}<li>{{template "nostrEvent" $reply}}{{with reviewLabel $reply}}<p><small>on {{.}}</small></p>{{end}}
65+{{range $reply := $replies}}{{$r := asMap $reply}}<li>{{template "collaborationProposal" (collabProposal $r $.Actor $can)}}{{template "nostrEvent" $reply}}{{with reviewLabel $reply}}<p><small>on {{.}}</small></p>{{end}}
66 {{if $.Actor}}<details><summary>Reply to this comment</summary><nostr-compose kind="1111" coordinate="{{$coordinate}}" root="{{index $item "id"}}" root-pubkey="{{index $item "author"}}" root-kind="{{index $item "kind"}}" parent="{{index $r "id"}}" parent-pubkey="{{index $r "pubkey"}}" parent-kind="1111"><label>Reply <textarea name="content" required></textarea></label><button>Sign and reply</button></nostr-compose></details>{{end}}</li>
67 {{end}}</ol>{{else}}<p>No replies yet.</p>{{end}}
68 {{with index $data "reply_cursor"}}<p id="pages"><a href="{{browsePageURL "/repo" $.Query "cursor" .}}">older replies</a></p>{{end}}
69@@ -38,3 +38,7 @@
70 {{if index $data "can_status"}}<details id="status-controls"><summary>Change status</summary><nostr-compose kind="status" coordinate="{{$coordinate}}" root="{{index $item "id"}}" root-pubkey="{{index $item "author"}}" root-kind="{{index $item "kind"}}"><label>Status <select name="status"><option value="1630">Reopen</option><option value="1631">{{if $issues}}Resolve{{else}}Mark merged{{end}}</option><option value="1632">Close</option><option value="1633">Draft</option></select></label><label>Comment <textarea name="content"></textarea></label><button>Sign and update</button></nostr-compose></details>{{end}}
71 <noscript><p>JavaScript and a connected signer are required to publish.</p></noscript>{{else}}<p><a href="/signin">Sign in</a> to reply.</p>{{end}}
72 </section>{{end}}{{end}}
73+
74+{{/* collaborationProposal is the bar above a proposed issue, pull request or comment: its state for the people who may see it, with the decision for those who may make it. */}}
75+{{define "collaborationProposal"}}{{if .Shown}}<collaboration-proposal data-state="{{.State}}">Proposed by {{template "nostrName" .Author}} | {{.State}}{{if .By}} {{when .At}} by {{template "nostrName" .By}}{{end}}{{if .Decide}}<nostr-react event="{{.ID}}" pubkey="{{.Author}}" kind="{{.Kind}}"><button name="reaction" value="+">Accept</button><button name="reaction" value="-">Reject</button></nostr-react>{{end}}</collaboration-proposal>
76+{{end}}{{end}}
77diff --git a/internal/webui/collaboration_test.go b/internal/webui/collaboration_test.go
78index 90762b2..065e6b8 100644
79--- a/internal/webui/collaboration_test.go
80+++ b/internal/webui/collaboration_test.go
81@@ -85,3 +85,130 @@ func TestCollaborationPagesUseTypedResultsAndWorkingDetailLinks(t *testing.T) {
82 t.Fatalf("wrong detail request: %s %v", backend.call, request)
83 }
84 }
85+
86+const (
87+ proposalAgent = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"
88+ proposalMember = "6666666666666666666666666666666666666666666666666666666666666666"
89+ proposalIssue = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
90+ proposalComment = "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"
91+ proposalPull = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
92+ proposalReview = "1111111111111111111111111111111111111111111111111111111111111111"
93+)
94+
95+// proposalBackend serves one repository with an issue and a pull request
96+// proposed by an agent, each with a proposed comment. state is the
97+// proposals' approval and approver marks the caller as allowed to decide.
98+type proposalBackend struct {
99+ fakeBackend
100+ state string
101+ approver bool
102+}
103+
104+func (b *proposalBackend) Query(_ context.Context, method string, params []json.RawMessage, actor string) (any, error) {
105+ b.call = method
106+ b.params = params
107+ proposal := func(m map[string]any) map[string]any {
108+ m["proposal"] = true
109+ m["approval"] = b.state
110+ if b.state != "pending" {
111+ m["approval_event"], m["approval_at"], m["approval_by"] = strings.Repeat("9", 64), float64(1788882000), b.policy.Owner
112+ }
113+ return m
114+ }
115+ ownerIssue := map[string]any{"id": strings.Repeat("b", 64), "title": "Owner issue", "kind": float64(1621), "author": b.policy.Owner, "content": "Owner body", "status": "open", "created_at": float64(1788800000)}
116+ agentIssue := proposal(map[string]any{"id": proposalIssue, "title": "Agent issue", "kind": float64(1621), "author": proposalAgent, "content": "Agent body", "status": "open", "created_at": float64(1788800100), "labels": []any{"bug"}})
117+ agentPull := proposal(map[string]any{"id": proposalPull, "title": "Agent pull", "kind": float64(1618), "author": proposalAgent, "content": "Pull body", "status": "open", "created_at": float64(1788800200)})
118+ comment := proposal(map[string]any{"id": proposalComment, "kind": float64(1111), "pubkey": proposalAgent, "content": "Agent comment", "created_at": float64(1788800300), "tags": []any{}})
119+ plain := map[string]any{"id": strings.Repeat("2", 64), "kind": float64(1111), "pubkey": proposalMember, "content": "Member comment", "created_at": float64(1788800400), "tags": []any{}}
120+ review := proposal(map[string]any{"id": proposalReview, "kind": float64(1111), "pubkey": proposalAgent, "content": "Agent review", "created_at": float64(1788800500), "tags": []any{}, "file": "f.txt", "line": float64(1), "side": "new"})
121+ repository := map[string]any{"owner": b.policy.Owner, "clone": []any{}, "maintainers": []any{}}
122+ var request struct {
123+ Event string `json:"event"`
124+ }
125+ if len(params) > 0 {
126+ _ = json.Unmarshal(params[0], &request)
127+ }
128+ switch method {
129+ case "browseissues":
130+ return map[string]any{"repository": repository, "items": []any{agentIssue, ownerIssue}, "can_approve": b.approver}, nil
131+ case "browsepulls":
132+ return map[string]any{"repository": repository, "items": []any{agentPull}, "can_approve": b.approver}, nil
133+ case "browseissue":
134+ item := ownerIssue
135+ if request.Event == proposalIssue {
136+ item = agentIssue
137+ }
138+ return map[string]any{"repository": repository, "item": item, "replies": []any{comment, plain}, "can_approve": b.approver, "can_status": false}, nil
139+ case "browsepull":
140+ return map[string]any{"repository": repository, "item": agentPull, "replies": []any{review, plain}, "diff": "diff --git a/f.txt b/f.txt\n--- a/f.txt\n+++ b/f.txt\n@@ -1 +1 @@\n-old\n+new\n", "can_approve": b.approver, "can_status": false}, nil
141+ }
142+ return map[string]any{}, nil
143+}
144+
145+func proposalPage(t *testing.T, b *proposalBackend, actor, view, id string) string {
146+ t.Helper()
147+ app, err := New(b, Options{Actor: func(*http.Request) (string, error) { return actor, nil }})
148+ if err != nil {
149+ t.Fatal(err)
150+ }
151+ path := repoURL(url.Values{"owner": {b.policy.Owner}, "repo": {"test"}}, view, id)
152+ return wikiGet(t, app, path)
153+}
154+
155+func TestCollaborationProposalBarOffersTheDecisionToApprovers(t *testing.T) {
156+ b := &proposalBackend{fakeBackend: fakeBackend{policy: policy.Defaults(strings.Repeat("a", 64))}, state: "pending", approver: true}
157+ owner := b.policy.Owner
158+ body := proposalPage(t, b, owner, "issue", proposalIssue)
159+ wantAll(t, "issue", body,
160+ `<collaboration-proposal data-state="pending">Proposed by <nostr-name pubkey="`+proposalAgent+`"`,
161+ `| pending<nostr-react event="`+proposalIssue+`" pubkey="`+proposalAgent+`" kind="1621"><button name="reaction" value="+">Accept</button><button name="reaction" value="-">Reject</button></nostr-react></collaboration-proposal>
162+<article id="collaboration-body">`,
163+ `<li><collaboration-proposal data-state="pending">Proposed by <nostr-name pubkey="`+proposalAgent+`"`,
164+ `<nostr-react event="`+proposalComment+`" pubkey="`+proposalAgent+`" kind="1111">`,
165+ `<li><nostr-event id="event-`+strings.Repeat("2", 64)+`">`)
166+ if strings.Count(body, "<collaboration-proposal") != 2 {
167+ t.Fatalf("issue page bars: %s", body)
168+ }
169+ // A comment placed on a diff line carries its bar inside the diff.
170+ body = proposalPage(t, b, owner, "pr", proposalPull)
171+ wantAll(t, "pr", body,
172+ `<nostr-react event="`+proposalPull+`" pubkey="`+proposalAgent+`" kind="1618">`,
173+ `<td></td><td><collaboration-proposal data-state="pending">Proposed by <nostr-name pubkey="`+proposalAgent+`"`,
174+ `<nostr-react event="`+proposalReview+`" pubkey="`+proposalAgent+`" kind="1111">`)
175+ // The lists mark the rows for those who may see them.
176+ body = proposalPage(t, b, owner, "issues", "")
177+ wantAll(t, "issues", body, `<tr data-state="pending"><td>open</td><td><a href="`, `">Agent issue</a> <small>proposed | pending</small> <small>bug</small></td>`, `<tr><td>open</td><td><a href="`)
178+ body = proposalPage(t, b, owner, "prs", "")
179+ wantAll(t, "prs", body, `<tr data-state="pending"><td>open</td><td><a href="`, `">Agent pull</a> <small>proposed | pending</small></td>`)
180+ // A decided proposal names the decision, its time and the deciding key,
181+ // and offers no buttons.
182+ for _, state := range []string{"approved", "rejected"} {
183+ b.state = state
184+ body = proposalPage(t, b, owner, "issue", proposalIssue)
185+ wantAll(t, state, body, `<collaboration-proposal data-state="`+state+`">Proposed by <nostr-name pubkey="`+proposalAgent+`"`, `| `+state+` 2026-09-08 15:40 UTC by <nostr-name pubkey="`+owner+`"`)
186+ wantNone(t, state, body, "<nostr-react", ">Accept<")
187+ body = proposalPage(t, b, owner, "issues", "")
188+ wantAll(t, state, body, `<tr data-state="`+state+`">`, `<small>proposed | `+state+`</small>`)
189+ }
190+}
191+
192+func TestCollaborationProposalAuthorSeesTheStateWithoutButtons(t *testing.T) {
193+ b := &proposalBackend{fakeBackend: fakeBackend{policy: policy.Defaults(strings.Repeat("a", 64))}, state: "pending"}
194+ body := proposalPage(t, b, proposalAgent, "issue", proposalIssue)
195+ wantAll(t, "issue", body, `<collaboration-proposal data-state="pending">Proposed by <nostr-name pubkey="`+proposalAgent+`"`, `| pending</collaboration-proposal>`)
196+ wantNone(t, "issue", body, "<nostr-react", ">Accept<", ">Reject<")
197+ if strings.Count(body, "<collaboration-proposal") != 2 {
198+ t.Fatalf("author bars: %s", body)
199+ }
200+ body = proposalPage(t, b, proposalAgent, "issues", "")
201+ wantAll(t, "issues", body, `<tr data-state="pending">`, `">Agent issue</a> <small>proposed | pending</small>`)
202+}
203+
204+func TestCollaborationProposalStateIsHiddenFromMembers(t *testing.T) {
205+ b := &proposalBackend{fakeBackend: fakeBackend{policy: policy.Defaults(strings.Repeat("a", 64))}, state: "approved"}
206+ for _, page := range [][2]string{{"issue", proposalIssue}, {"pr", proposalPull}, {"issues", ""}, {"prs", ""}} {
207+ body := proposalPage(t, b, proposalMember, page[0], page[1])
208+ wantAll(t, page[0], body, "Agent ")
209+ wantNone(t, page[0], body, "<collaboration-proposal", "<tr data-state=", "proposed | ", "<nostr-react")
210+ }
211+}
212diff --git a/internal/webui/style.css b/internal/webui/style.css
213index 40e8524..5f1bb31 100644
214--- a/internal/webui/style.css
215+++ b/internal/webui/style.css
216@@ -226,9 +226,10 @@ diff-view td>nostr-event{white-space:normal;max-width:720px;border:0;padding:6px
217 wiki-article{display:block;max-width:720px;overflow-wrap:anywhere}wiki-article>header{font:12px var(--mono);color:var(--muted);margin-bottom:8px}wiki-article>header nostr-key,wiki-article>header nostr-name{color:var(--ink)}
218 wiki-article h1{font-size:20px;margin:18px 0 8px}wiki-article h2{font-size:17px}wiki-article h3{font-size:15px}wiki-article p{margin:0 0 10px}
219 wiki-article pre{background:var(--rail);border:1px solid var(--line);padding:10px 12px;overflow:auto;font-size:12.5px;line-height:1.5;white-space:pre}wiki-article ul,wiki-article ol{padding-left:20px;margin:0 0 10px}
220-merge-request,wiki-proposal{display:block;border:1px solid var(--line);background:var(--soft);padding:8px 10px;margin:0 0 14px;max-width:720px;font:13px var(--sans)}merge-request nostr-key,merge-request nostr-name,wiki-proposal nostr-key,wiki-proposal nostr-name{color:var(--ink);font-weight:500}merge-request>a,merge-request button,wiki-proposal button{margin-left:6px;font-size:12px}
221-merge-request nostr-react,merge-request nostr-react form,wiki-proposal nostr-react,wiki-proposal nostr-react form{display:inline}merge-request nostr-react output,wiki-proposal nostr-react output{display:inline-block;margin-left:6px;font:12px var(--mono);color:var(--muted)}merge-request nostr-react output[data-error],wiki-proposal nostr-react output[data-error]{color:var(--alert)}
222-wiki-proposal[data-state=pending]{border-color:var(--bark)}wiki-proposal[data-state=rejected]{border-color:var(--alert)}#wiki-list #rows td:first-child small,#panel li[data-state]{font:12px var(--mono)}#wiki-list #rows td:first-child small{margin-left:6px}#wiki-list #rows tr[data-state=pending] small,#panel li[data-state=pending]{color:var(--bark)}#wiki-list #rows tr[data-state=approved] small,#panel li[data-state=approved]{color:var(--accent)}#wiki-list #rows tr[data-state=rejected] small,#panel li[data-state=rejected]{color:var(--alert)}
223+merge-request,wiki-proposal,collaboration-proposal{display:block;border:1px solid var(--line);background:var(--soft);padding:8px 10px;margin:0 0 14px;max-width:720px;font:13px var(--sans)}merge-request nostr-key,merge-request nostr-name,wiki-proposal nostr-key,wiki-proposal nostr-name,collaboration-proposal nostr-key,collaboration-proposal nostr-name{color:var(--ink);font-weight:500}merge-request>a,merge-request button,wiki-proposal button,collaboration-proposal button{margin-left:6px;font-size:12px}
224+merge-request nostr-react,merge-request nostr-react form,wiki-proposal nostr-react,wiki-proposal nostr-react form,collaboration-proposal nostr-react,collaboration-proposal nostr-react form{display:inline}merge-request nostr-react output,wiki-proposal nostr-react output,collaboration-proposal nostr-react output{display:inline-block;margin-left:6px;font:12px var(--mono);color:var(--muted)}merge-request nostr-react output[data-error],wiki-proposal nostr-react output[data-error],collaboration-proposal nostr-react output[data-error]{color:var(--alert)}
225+wiki-proposal[data-state=pending],collaboration-proposal[data-state=pending]{border-color:var(--bark)}wiki-proposal[data-state=rejected],collaboration-proposal[data-state=rejected]{border-color:var(--alert)}#wiki-list #rows td:first-child small,#panel li[data-state]{font:12px var(--mono)}#wiki-list #rows td:first-child small{margin-left:6px}#wiki-list #rows tr[data-state=pending] small,#panel li[data-state=pending]{color:var(--bark)}#wiki-list #rows tr[data-state=approved] small,#panel li[data-state=approved]{color:var(--accent)}#wiki-list #rows tr[data-state=rejected] small,#panel li[data-state=rejected]{color:var(--alert)}
226+#events li>collaboration-proposal,diff-view td>collaboration-proposal{margin:6px 0 8px;max-width:none}#collaboration-list #rows tr[data-state] td:nth-child(2)>small:first-of-type{font:12px var(--mono)}#collaboration-list #rows tr[data-state=pending] td:nth-child(2)>small:first-of-type{color:var(--bark)}#collaboration-list #rows tr[data-state=approved] td:nth-child(2)>small:first-of-type{color:var(--accent)}#collaboration-list #rows tr[data-state=rejected] td:nth-child(2)>small:first-of-type{color:var(--alert)}
227 #merge-status{font:12px var(--mono);color:var(--muted)}#wiki-compare h2{margin-top:18px}#wiki-redirects{font:12px var(--mono);color:var(--muted)}
228 wiki-compose{display:block;max-width:720px;border-bottom:1px solid var(--line);padding:10px 0}
229 wiki-compose label{display:inline-block;margin:0 10px 6px 0;vertical-align:top;font:13px var(--sans);color:var(--muted)}wiki-compose label input{display:block;margin-top:2px;font:13px var(--mono);min-width:200px}
230diff --git a/internal/webui/templates.go b/internal/webui/templates.go
231index f09ae87..12ef017 100644
232--- a/internal/webui/templates.go
233+++ b/internal/webui/templates.go
234@@ -257,6 +257,7 @@ func parseTemplates() (*template.Template, error) {
235 "collabItems": collaborationItems,
236 "collabReplies": collaborationReplies,
237 "collabLabels": collaborationLabels,
238+ "collabProposal": collaborationProposal,
239 "reviewDiff": reviewDiffView,
240 "reviewAnchor": reviewAnchorQuery,
241 "reviewLabel": reviewAnchorLabel,

About

ownerc01503e8035f
visibilitypublic
heade39e529674a7

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

  • https://012.run/npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5/tinyrelay.git

Refs