@nanocollective/roster 0.1.0-alpha.2 → 0.1.0-alpha.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/dist/cli.js +848 -164
  2. package/docs/README.md +10 -5
  3. package/docs/agents.md +320 -9
  4. package/docs/commands.md +5 -6
  5. package/docs/concepts.md +27 -9
  6. package/docs/cost.md +3 -2
  7. package/docs/doctor-codes.md +13 -4
  8. package/docs/export.md +2 -1
  9. package/docs/extending.md +11 -2
  10. package/docs/getting-started.md +89 -84
  11. package/docs/images/brain.jpg +0 -0
  12. package/docs/images/org.jpg +0 -0
  13. package/docs/images/prompt.jpg +0 -0
  14. package/docs/images/setup-org.jpg +0 -0
  15. package/docs/images/setup-plan.jpg +0 -0
  16. package/docs/images/staff.jpg +0 -0
  17. package/docs/manual-steps.md +36 -13
  18. package/docs/memory.md +9 -6
  19. package/docs/org-yaml.md +37 -9
  20. package/docs/portal.md +197 -31
  21. package/docs/prompts.md +50 -11
  22. package/docs/security.md +19 -7
  23. package/docs/session-workflow.md +8 -10
  24. package/docs/staff-yaml.md +3 -5
  25. package/docs/troubleshooting.md +17 -14
  26. package/docs/writing-a-charter.md +18 -17
  27. package/package.json +1 -1
  28. package/templates/brain/.github/workflows/%%STAFF%%-daily.yaml +1 -0
  29. package/templates/brain/.github/workflows/%%STAFF%%-mention.yaml +10 -4
  30. package/templates/brain/staff.yaml +0 -1
  31. package/templates/ops/.github/workflows/session.yaml +9 -26
  32. package/templates/ops/agents.mjs +121 -6
  33. package/templates/ops/compose.mjs +61 -4
  34. package/templates/ops/org/operating.md +0 -6
  35. package/templates/ops/prompts/_identity.md +8 -1
  36. package/templates/ops/prompts/mention.md +16 -2
  37. package/templates/portal/css/base.css +122 -8
  38. package/templates/portal/css/brain.css +8 -1
  39. package/templates/portal/css/diff.css +6 -2
  40. package/templates/portal/css/health.css +21 -2
  41. package/templates/portal/css/inbox.css +93 -5
  42. package/templates/portal/css/layout.css +26 -4
  43. package/templates/portal/css/markdown.css +23 -3
  44. package/templates/portal/css/setup.css +11 -6
  45. package/templates/portal/index.html +7 -1
  46. package/templates/portal/js/api.js +33 -0
  47. package/templates/portal/js/app.js +33 -7
  48. package/templates/portal/js/dialog.js +47 -4
  49. package/templates/portal/js/dom.js +25 -0
  50. package/templates/portal/js/icons.js +8 -1
  51. package/templates/portal/js/lightbox.js +273 -0
  52. package/templates/portal/js/md.js +23 -6
  53. package/templates/portal/js/mention.js +264 -0
  54. package/templates/portal/js/refresh.js +136 -6
  55. package/templates/portal/js/state.js +47 -5
  56. package/templates/portal/js/views/checklist.js +20 -7
  57. package/templates/portal/js/views/docs.js +94 -4
  58. package/templates/portal/js/views/files.js +58 -14
  59. package/templates/portal/js/views/health.js +163 -35
  60. package/templates/portal/js/views/inbox.js +882 -96
  61. package/templates/portal/js/views/memory.js +16 -1
  62. package/templates/portal/js/views/org.js +142 -62
  63. package/templates/portal/js/views/prompt.js +50 -63
  64. package/templates/portal/js/views/setup.js +37 -13
  65. package/templates/portal/js/views/staff.js +62 -2
  66. package/templates/portal/js/yaml.js +134 -0
  67. package/templates/brain/.github/workflows/%%STAFF%%-pr-mention.yaml +0 -50
  68. package/templates/ops/prompts/pr-mention.md +0 -57
@@ -5,7 +5,7 @@
5
5
  --ink:#e6e9ef; --ink-dim:#9aa3b2; --ink-faint:#6b7484;
6
6
  --accent:#7cc7a4; --accent-dim:#3d6b56;
7
7
  --warn:#d8a657; --err:#e07b7b;
8
- --will:#7aa2f7; --measured:#7cc7a4; --derived:#c99bd6;
8
+ --ruled:#7aa2f7; --measured:#7cc7a4; --derived:#c99bd6;
9
9
  --mono:ui-monospace,SFMono-Regular,"SF Mono",Menlo,monospace;
10
10
  --sans:-apple-system,BlinkMacSystemFont,"Segoe UI",Inter,system-ui,sans-serif;
11
11
  }
@@ -16,7 +16,7 @@
16
16
  --ink:#1b1f26; --ink-dim:#5b6472; --ink-faint:#8b95a5;
17
17
  --accent:#2f7d5b; --accent-dim:#a8d5bf;
18
18
  --warn:#a5701d; --err:#b34a4a;
19
- --will:#3b5fa8; --measured:#2f7d5b; --derived:#8a52a8;
19
+ --ruled:#3b5fa8; --measured:#2f7d5b; --derived:#8a52a8;
20
20
  }
21
21
  }
22
22
  :root[data-theme="light"]{
@@ -24,10 +24,16 @@
24
24
  --ink:#1b1f26; --ink-dim:#5b6472; --ink-faint:#8b95a5;
25
25
  --accent:#2f7d5b; --accent-dim:#a8d5bf;
26
26
  --warn:#a5701d; --err:#b34a4a;
27
- --will:#3b5fa8; --measured:#2f7d5b; --derived:#8a52a8;
27
+ --ruled:#3b5fa8; --measured:#2f7d5b; --derived:#8a52a8;
28
28
  }
29
29
 
30
30
  *{box-sizing:border-box}
31
+ /* `hidden` loses to any author rule that sets `display`, which is most of them: a hidden
32
+ `.field` is still `display:grid` and a hidden `.btn` is still `display:block`. Every element
33
+ the page hides would otherwise need its own `[hidden]` rule, and the ones nobody remembered
34
+ just stayed on screen. Said once, here. */
35
+ [hidden]{display:none!important}
36
+
31
37
  body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.55 var(--sans);
32
38
  -webkit-font-smoothing:antialiased}
33
39
  a{color:inherit}
@@ -44,16 +50,21 @@ h1{font:600 21px var(--sans);margin:0 0 4px;letter-spacing:-.015em}
44
50
  .grid{display:grid;gap:11px;grid-template-columns:repeat(auto-fill,minmax(250px,1fr))}
45
51
 
46
52
  /* ---------- controls ---------- */
47
- input[type=search],select{background:var(--panel-2);border:1px solid var(--line);color:var(--ink);
53
+ input[type=search],input[type=time],select{background:var(--panel-2);
54
+ border:1px solid var(--line);color:var(--ink);
48
55
  border-radius:7px;padding:7px 11px;font:13px var(--sans);outline:none}
49
56
  input[type=search]{width:280px}
50
- input[type=search]:focus,select:focus{border-color:var(--accent-dim)}
57
+ input[type=search]:focus,input[type=time]:focus,select:focus{border-color:var(--accent-dim)}
51
58
  textarea{width:100%;background:var(--panel-2);border:1px solid var(--line);color:var(--ink);
52
59
  border-radius:8px;padding:10px 12px;font:13px/1.55 var(--sans);outline:none;resize:vertical}
53
60
  textarea:focus{border-color:var(--accent-dim)}
54
61
 
55
- .ghbtn{display:inline-block;padding:5px 11px;border:1px solid var(--line);border-radius:7px;
56
- font:12px var(--sans);color:var(--ink-dim);text-decoration:none;white-space:nowrap}
62
+ /* One size, whether it is a button or a link. They sit in rows together, and `a` taking its
63
+ line-height from the font shorthand while `button` set its own made every such row a set of
64
+ controls at two different heights. */
65
+ .ghbtn{display:inline-flex;align-items:center;justify-content:center;min-height:28px;
66
+ padding:5px 11px;border:1px solid var(--line);border-radius:7px;
67
+ font:12px/1.4 var(--sans);color:var(--ink-dim);text-decoration:none;white-space:nowrap}
57
68
  .ghbtn:hover{color:var(--ink);border-color:var(--accent-dim);background:var(--panel-2)}
58
69
  button.ghbtn{cursor:pointer;font-family:var(--sans);background:transparent;
59
70
  -webkit-appearance:none;appearance:none;font-size:12px;line-height:1.4}
@@ -80,12 +91,49 @@ svg.ic{width:var(--ic,15px);height:var(--ic,15px);display:block;flex:none}
80
91
  border:1px solid var(--line);color:var(--ink-faint);white-space:nowrap}
81
92
  .chip.hot{color:var(--err);border-color:color-mix(in srgb,var(--err) 45%,transparent)}
82
93
  .chip.warm{color:var(--warn);border-color:color-mix(in srgb,var(--warn) 45%,transparent)}
83
- .chip.cool{color:var(--will);border-color:color-mix(in srgb,var(--will) 40%,transparent)}
94
+ .chip.cool{color:var(--ruled);border-color:color-mix(in srgb,var(--ruled) 40%,transparent)}
84
95
  .pill{font:11px var(--mono);color:var(--ink-faint);border:1px solid var(--line);
85
96
  border-radius:20px;padding:2px 9px}
86
97
  kbd{font:11px var(--mono);border:1px solid var(--line);border-bottom-width:2px;border-radius:4px;
87
98
  padding:1px 5px;color:var(--ink-dim)}
88
99
 
100
+ /* ---------- waiting ----------
101
+ The inbox reads every repo in the org, which is seconds on a cold cache. What used to stand
102
+ there was one line of grey text in an otherwise empty panel beside a second empty panel,
103
+ which reads as a page that has failed rather than one that is working. These are the shape
104
+ of what is coming: rows in the list, a title and paragraphs in the viewer. */
105
+ .sk{--skbg:color-mix(in srgb,var(--ink) 7%,transparent);border-radius:6px;
106
+ background:linear-gradient(90deg,var(--skbg) 25%,
107
+ color-mix(in srgb,var(--ink) 12%,transparent) 37%,var(--skbg) 63%);
108
+ background-size:400% 100%;animation:shimmer 1.4s ease-in-out infinite}
109
+ .sk-row{background:none;animation:none;padding:11px 10px;border-top:1px solid var(--line)}
110
+ .sk-row:first-of-type{border-top:0}
111
+ .sk-row .skbar{height:11px;border-radius:6px;background:inherit;
112
+ background:linear-gradient(90deg,var(--skbg) 25%,
113
+ color-mix(in srgb,var(--ink) 12%,transparent) 37%,var(--skbg) 63%);
114
+ background-size:400% 100%;animation:shimmer 1.4s ease-in-out infinite}
115
+ .sk-row .skbar.t{width:84%}
116
+ .sk-row .skbar.m{width:38%;height:8px;margin-top:8px}
117
+ /* Staggered, so the list reads as several things loading rather than one block flashing. */
118
+ .sk-row:nth-child(2n) .skbar.t{width:68%}
119
+ .sk-row:nth-child(3n) .skbar.t{width:92%}
120
+ .sk-row:nth-child(3n) .skbar.m{width:52%}
121
+ .sk-head{height:19px;width:56%;margin:6px 0 18px}
122
+ .sk-line{height:10px;width:100%;margin:9px 0}
123
+ .sk-line:nth-child(2n){width:88%}
124
+ .sk-line:nth-child(3n){width:94%}
125
+ .sk-line:last-child{width:42%}
126
+ /* The word beside them. It is the only part a screen reader gets, so it stays real text. */
127
+ .skhead{color:var(--ink-faint);font-size:12.5px;padding:4px 10px 8px}
128
+ @keyframes shimmer{0%{background-position:100% 0}100%{background-position:0 0}}
129
+ /* A count that is still being fetched is not zero, and an empty badge reads as zero. */
130
+ .nav .n.wait{display:inline-block;width:15px;height:8px;border-radius:5px;
131
+ background:color-mix(in srgb,var(--ink) 10%,transparent);animation:pulse 1.3s ease-in-out infinite}
132
+ @keyframes pulse{0%,100%{opacity:.35}50%{opacity:.85}}
133
+ @media (prefers-reduced-motion:reduce){
134
+ .sk,.sk-row .skbar,.nav .n.wait{animation:none}
135
+ }
136
+
89
137
  .notice{background:color-mix(in srgb,var(--warn) 12%,transparent);border:1px solid
90
138
  color-mix(in srgb,var(--warn) 40%,transparent);color:var(--ink-dim);border-radius:8px;
91
139
  padding:10px 13px;margin-bottom:16px;font-size:12.5px;line-height:1.6}
@@ -100,5 +148,71 @@ dialog.ask::backdrop{background:rgba(0,0,0,.45);backdrop-filter:blur(2px)}
100
148
  dialog.ask h3{margin:0;font:600 15.5px var(--sans);letter-spacing:-.012em}
101
149
  dialog.ask .askhint{margin:6px 0 12px;font:12.5px/1.55 var(--sans);color:var(--ink-faint)}
102
150
  dialog.ask textarea{font:13.5px/1.6 var(--sans);min-height:96px;max-height:44vh}
151
+ /* The attach control, when the dialog is a reply rather than a question. */
152
+ dialog.ask .attach{margin-top:9px}
103
153
  dialog.ask .askrow{margin-top:12px;justify-content:flex-end}
104
154
  dialog.ask .askrow .meta{margin-right:auto}
155
+
156
+ /* The lightbox: an image, over the page, that zooms.
157
+ *
158
+ * Not a <dialog>, so the stacking and the backdrop are declared here rather than inherited
159
+ * from ::backdrop. 60 puts it over the `@` mention list (30) and over the dialog, which is
160
+ * correct: a picture opened from inside a reply box should still open. */
161
+ .lightbox{position:fixed;inset:0;z-index:60;display:flex;
162
+ background:rgba(0,0,0,.82);backdrop-filter:blur(3px)}
163
+ /* The stage is inset rather than the whole window, and that is not decoration.
164
+ Most images here are screenshots *of this interface*, and one fitted edge to edge reads as
165
+ the app having navigated rather than as a picture of it. The margin of backdrop around it,
166
+ and the border on it, are the only things that say which you are looking at.
167
+
168
+ Fit and centring are arithmetic in the module rather than layout: the transform has to be
169
+ the single source of truth, or zooming fights the flexbox. The module measures this box, so
170
+ changing the inset here is all it takes to change what "fitted" means. */
171
+ .lightbox .lbstage{position:absolute;inset:46px 56px 52px;overflow:hidden}
172
+ .lightbox img{position:absolute;top:0;left:0;transform-origin:0 0;
173
+ max-width:none;max-height:none;border-radius:8px;
174
+ border:1px solid rgba(255,255,255,.14);
175
+ box-shadow:0 18px 50px rgba(0,0,0,.5);
176
+ cursor:zoom-in;user-select:none;-webkit-user-drag:none}
177
+ /* Zoomed past fitted it is being read, not looked at, so the frame comes off: a border and a
178
+ drop shadow in the middle of a diff you are inspecting is furniture in the way. */
179
+ .lightbox[data-zoomed="true"] img{border-radius:0;border-color:transparent;box-shadow:none}
180
+ .lightbox[data-zoomed="true"] img{cursor:grab}
181
+ .lightbox[data-zoomed="true"] img:active{cursor:grabbing}
182
+
183
+ .lightbox .lbx,.lightbox .lbctl{--ic:17px;
184
+ display:inline-flex;align-items:center;justify-content:center;
185
+ width:34px;height:34px;padding:0;border-radius:9px;
186
+ border:1px solid var(--line);background:var(--panel);color:var(--ink-dim);
187
+ cursor:pointer}
188
+ .lightbox .lbx{position:absolute;top:14px;right:16px;--ic:18px}
189
+ .lightbox .lbx:hover,.lightbox .lbctl:not(:disabled):hover{color:var(--ink);
190
+ border-color:var(--ink-faint)}
191
+ /* At a stop the button stays where it is and stops responding. Removing it would move the two
192
+ beside it, which reads as the control having changed rather than as a limit being reached. */
193
+ .lightbox .lbctl:disabled{opacity:.35;cursor:default}
194
+
195
+ /* Bottom right, which is the one corner nothing else uses: close is top right, the hint is top
196
+ centre and the caption is bottom centre. */
197
+ .lightbox .lbzoom{position:absolute;bottom:12px;right:16px;display:flex;gap:6px;align-items:center}
198
+ /* Wide enough for "1600%" so the group does not resize as the number grows, and tabular so the
199
+ digits do not shuffle while you hold the button down. */
200
+ .lightbox .lblevel{width:auto;min-width:60px;padding:0 9px;
201
+ font:12px var(--mono);font-variant-numeric:tabular-nums;
202
+ color:var(--ink-dim)}
203
+
204
+ /* Both of these are over the picture, so they are quiet and they do not take the pointer:
205
+ a click that lands on the caption still means "fit", not "nothing happened". */
206
+ .lightbox .lbhint,.lightbox .lbcap{position:absolute;left:0;right:0;margin:0;
207
+ pointer-events:none;text-align:center;
208
+ font:11.5px var(--sans);color:var(--ink-faint)}
209
+ .lightbox .lbhint{top:16px;padding:0 60px}
210
+ .lightbox .lbcap{bottom:19px;padding:0 200px;color:var(--ink-dim);font-size:12.5px;
211
+ white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
212
+
213
+ /* The page behind must not scroll under an overlay. A wheel event that misses the image
214
+ would otherwise scroll the document instead of doing nothing. */
215
+ body[data-lightbox] {overflow:hidden}
216
+
217
+ /* And the affordance on the document side: an image you can open should say so. */
218
+ .md img,img.zoomable{cursor:zoom-in}
@@ -47,7 +47,7 @@
47
47
  .factsub{color:var(--ink-faint);font-size:12.5px;margin:0 0 14px}
48
48
  .tag{display:inline-block;font:10.5px var(--mono);padding:1px 6px;border-radius:20px;
49
49
  border:1px solid var(--line);color:var(--ink-faint);margin-left:6px;vertical-align:1px}
50
- .tag.will{color:var(--will);border-color:var(--will)}
50
+ .tag.ruled{color:var(--ruled);border-color:var(--ruled)}
51
51
  .tag.measured{color:var(--measured);border-color:var(--measured)}
52
52
  .tag.derived{color:var(--derived);border-color:var(--derived)}
53
53
  .notelink{font:11.5px var(--mono);color:var(--ink-faint);cursor:pointer;margin-left:6px}
@@ -89,6 +89,13 @@
89
89
  .editor{font:12.5px/1.7 var(--mono);min-height:340px;overflow:hidden;resize:none}
90
90
  .editnote{margin:8px 0 0;font:11.5px var(--sans);color:var(--ink-faint)}
91
91
 
92
+ /* The path, then the controls, pushed to the far end. Editing these files is the whole point
93
+ of the Org screen, and the way in used to be a grey word in a row of grey words — which is
94
+ why "there is no easy way to edit the org docs" was fair about a screen that could. */
95
+ .filehead{margin-bottom:6px;gap:8px;align-items:center}
96
+ .filehead .meta{margin-right:auto;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
97
+ .editbtn{gap:6px;--ic:13px}
98
+
92
99
  /* A prompt audit finding. Not a tree row: it carries an explanation and two buttons, and
93
100
  squeezing that into the navigator's row shape is how it would get ignored. */
94
101
  .prob{border:1px solid var(--line);border-radius:7px;padding:9px 10px;margin:0 3px 6px}
@@ -13,15 +13,19 @@
13
13
  .ddel{background:color-mix(in srgb,var(--err) 13%,transparent)}
14
14
  .ddel .tx{color:var(--ink-dim)}
15
15
  .ddel .ln{background:color-mix(in srgb,var(--err) 22%,transparent);color:var(--err)}
16
- .dhunk{background:color-mix(in srgb,var(--will) 13%,transparent);
16
+ .dhunk{background:color-mix(in srgb,var(--ruled) 13%,transparent);
17
17
  border-top:1px solid var(--line);border-bottom:1px solid var(--line)}
18
- .dhunk .tx{color:var(--will);font-size:11.5px}
18
+ .dhunk .tx{color:var(--ruled);font-size:11.5px}
19
19
  .dmeta .tx{color:var(--ink-faint);font-size:11.5px}
20
20
  .dfocus{box-shadow:inset 3px 0 0 var(--warn)}
21
21
  .dfile{font:11.5px var(--mono);color:var(--ink-dim);background:var(--panel);padding:8px 12px;
22
22
  border-bottom:1px solid var(--line);display:flex;gap:10px;align-items:baseline}
23
23
  .dfile b{color:var(--ink);font-weight:600}
24
24
  .dfile .p{color:var(--accent)} .dfile .m{color:var(--err)}
25
+ /* Pushed to the far end and kept faint: the diff is what this row is for, and a button that
26
+ competes with the filename for attention is one you stop reading past. */
27
+ .askfile{margin-left:auto;padding:1px 8px;font-size:11px;opacity:.55}
28
+ .dblock:hover .askfile,.askfile:focus-visible{opacity:1}
25
29
  .dempty{padding:20px;color:var(--ink-faint);font-size:12.5px}
26
30
 
27
31
  /* The learned/forgotten marker on a "what changed" row. */
@@ -8,8 +8,27 @@
8
8
  .kv.bad .v{color:var(--err)}
9
9
  .card .fix{margin-top:10px}
10
10
  .card .fixed{color:var(--accent);font:11.5px var(--mono)}
11
- .hsect{font:600 10.5px var(--sans);letter-spacing:.09em;text-transform:uppercase;
12
- color:var(--ink-faint);margin:24px 0 10px}
11
+
12
+ /* ---------- sections ----------
13
+ Doctor, the prompt audit and lint ask the same kind of question, and used to answer it in
14
+ three different shapes: hairline rows, bordered boxes, and cards with two buttons each. One
15
+ shape now. A heading carries its own count, so the page can be read by scanning the left
16
+ edge, and each section's own actions sit at its end rather than above its findings. */
17
+ .hsect{display:flex;align-items:baseline;gap:10px;
18
+ font:600 10.5px var(--sans);letter-spacing:.09em;text-transform:uppercase;
19
+ color:var(--ink-faint);margin:28px 0 0;padding-bottom:8px;
20
+ border-bottom:1px solid var(--line)}
21
+ .hsect i{margin-left:auto;font:11.5px var(--mono);font-style:normal;letter-spacing:0;
22
+ text-transform:none;color:var(--ink-faint)}
23
+ .hbody{padding-top:2px}
24
+ /* What the section is, in one line, before any of its findings. */
25
+ .hnote{margin:10px 0 0;font:12.5px/1.55 var(--sans);color:var(--ink-faint)}
26
+ .hacts{margin-top:12px}
27
+ .hacts:empty{margin-top:0}
28
+ /* Per-finding actions, in the text column rather than under the marker. */
29
+ .facts{margin-top:9px}
30
+ /* Two tags in a row read as one word without this: `too-longINDEX.md:49`. */
31
+ .hbody .check .meta + .meta{margin-left:8px}
13
32
 
14
33
  /* ---------- the staff screen ---------- */
15
34
  .staffcard .v small{margin-top:4px}
@@ -1,7 +1,14 @@
1
1
  /* The inbox list, and the thread beside it. */
2
2
 
3
+ /* One row per item, with a hairline between them. Without it a list of two-line titles is a
4
+ wall of sentences and you have to work out where one item ends — which is most of what made
5
+ the inbox hard to scan. The line is dropped either side of the selected row, whose own
6
+ background is already doing the separating. */
3
7
  .irow{display:block;width:100%;text-align:left;background:none;border:0;cursor:pointer;
4
- padding:9px 10px;border-radius:7px;color:var(--ink-dim)}
8
+ padding:11px 10px;border-radius:7px;color:var(--ink-dim);
9
+ border-top:1px solid transparent}
10
+ .irow + .irow{border-top-color:var(--line)}
11
+ .irow[aria-current="true"],.irow[aria-current="true"] + .irow{border-top-color:transparent}
5
12
  .irow:hover{background:var(--panel-2)}
6
13
  .irow:focus{outline:none}
7
14
  .irow:focus-visible{outline:2px solid var(--accent-dim);outline-offset:-2px}
@@ -9,6 +16,12 @@
9
16
  .ititle{font:13.5px/1.4 var(--sans);color:var(--ink);display:-webkit-box;-webkit-line-clamp:2;
10
17
  -webkit-box-orient:vertical;overflow:hidden}
11
18
  .irow[aria-current="true"] .ititle{color:var(--accent)}
19
+ /* The rest of the row follows the title. The project label kept the flat grey it has when
20
+ nothing is selected, which read as a piece of the row that had not got the message. */
21
+ .irow[aria-current="true"] .repo{color:color-mix(in srgb,var(--accent) 72%,var(--ink-dim));
22
+ border-right-color:color-mix(in srgb,var(--accent) 30%,var(--line))}
23
+ .irow[aria-current="true"] .num,.irow[aria-current="true"] .when{
24
+ color:color-mix(in srgb,var(--accent) 45%,var(--ink-faint))}
12
25
  .dot{display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--warn);
13
26
  margin-right:6px;vertical-align:2px}
14
27
  .imeta{display:flex;gap:6px;align-items:center;flex-wrap:wrap;margin-top:5px}
@@ -17,6 +30,9 @@
17
30
  .imeta .num{font:10.5px var(--mono);color:var(--ink-faint)}
18
31
  .imeta .when{font:10.5px var(--sans);color:var(--ink-faint);margin-left:auto}
19
32
  .ck{display:inline-flex;--ic:13px}
33
+ .cc{display:inline-flex;align-items:center;gap:3px;--ic:12px;font:10.5px var(--mono);
34
+ color:var(--ink-faint)}
35
+ .irow[aria-current="true"] .cc{color:color-mix(in srgb,var(--accent) 45%,var(--ink-faint))}
20
36
  /* Closed work reads as finished at a glance, so it can share a list with open work without
21
37
  competing with it. */
22
38
  .irow.shut .ititle{color:var(--ink-dim)}
@@ -26,12 +42,84 @@
26
42
  .ck.passing{color:var(--accent)} .ck.failing{color:var(--err)} .ck.pending{color:var(--warn)}
27
43
 
28
44
  /* ---------- thread ---------- */
29
- .thead{border-bottom:1px solid var(--line);padding-bottom:14px;margin-bottom:4px}
30
- .trow{display:flex;justify-content:space-between;align-items:flex-start;gap:12px}
45
+ .thead{border-bottom:1px solid var(--line);padding-bottom:16px}
31
46
  .thead h3{margin:8px 0 0;font:600 17px/1.35 var(--sans);letter-spacing:-.012em;color:var(--ink)}
47
+ .tlabels{margin-top:10px}
32
48
  .cmt{border-top:1px solid var(--line);padding:16px 0}
33
49
  .cmt.first{border-top:0}
34
- .reply{border-top:1px solid var(--line);margin-top:16px;padding-top:14px}
50
+ /* The header already draws a line under itself. Whatever the reversed thread puts first must
51
+ not draw a second one right under it. */
52
+ .lead{border-top:0}
53
+ /* Reactions. Small, under the comment they belong to, the way GitHub puts them — an agent's
54
+ 👀 is an acknowledgement and it should be readable at a glance rather than counted. */
55
+ .reacts{display:flex;gap:6px;flex-wrap:wrap;margin-top:10px}
56
+ .react{font:11px var(--sans);border:1px solid var(--line);border-radius:20px;padding:2px 9px;
57
+ color:var(--ink-dim);background:var(--panel-2);cursor:default}
58
+ /* Reply, close and merge, beside the title rather than under the last comment. A thread is
59
+ read newest-first and answered from the top, so nothing here is worth scrolling to. */
60
+ .tacts{margin-top:16px;gap:8px}
61
+ .tacts select{min-height:28px;padding:4px 8px;font-size:12px}
62
+ /* The way out of the screen, not one of the things you do on it. */
63
+ .tacts .out{margin-left:auto}
64
+
65
+ /* ---------- pull requests ----------
66
+ Conversation, commits, files. Three views of one thing, so they are tabs on the thread
67
+ rather than three places to navigate to. */
68
+ .tabs{display:flex;gap:2px;border-bottom:1px solid var(--line);margin:2px 0 0}
69
+ .tab{background:none;border:0;border-bottom:2px solid transparent;cursor:pointer;
70
+ font:12.5px var(--sans);color:var(--ink-faint);padding:9px 12px;margin-bottom:-1px}
71
+ .tab:hover{color:var(--ink)}
72
+ .tab[aria-current="true"]{color:var(--ink);border-bottom-color:var(--accent)}
73
+ .tab:focus-visible{outline:2px solid var(--accent-dim);outline-offset:-2px}
74
+
75
+ .prcommit{display:flex;gap:10px;align-items:baseline;padding:8px 0;
76
+ border-top:1px solid var(--line);font-size:12.5px}
77
+ .prcommit:first-of-type{border-top:0}
78
+ .prcommit .sha{font:11.5px var(--mono);color:var(--ink-faint);flex:none;text-decoration:none}
79
+ .prcommit .sha:hover{color:var(--accent)}
80
+ .prcommit .subj{color:var(--ink);flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;
81
+ white-space:nowrap}
82
+ .prcommit .meta{flex:none}
83
+
84
+ /* ---------- writing: labels and attachments ----------
85
+ The label set is small and knowable, so it is a row of toggles rather than a text box that
86
+ quietly accepts a typo as a new label. Pressed state carries the same tones the inbox rows
87
+ use, so a `decision` chip is the same colour wherever you meet it. */
88
+ .labelpick{margin-top:12px}
89
+ .labelpick .chips{display:flex;flex-wrap:wrap;gap:6px;margin-top:6px}
90
+ .chip.pick{background:none;cursor:pointer;padding:3px 9px;font-size:11px;color:var(--ink-dim)}
91
+ .chip.pick:hover{border-color:var(--ink-faint);color:var(--ink)}
92
+ .chip.pick:focus-visible{outline:2px solid var(--accent-dim);outline-offset:1px}
93
+ .chip.pick[aria-pressed="true"]{background:color-mix(in srgb,var(--accent) 16%,transparent);
94
+ border-color:var(--accent-dim);color:var(--ink)}
95
+
96
+ /* ---------- the @ list ----------
97
+ A mention is the mechanism: a staff member's workflow gates on their handle, so a misspelled
98
+ one is a message nobody is woken by. The roster is small and already on the page, so typing
99
+ `@` offers it — only the entries a mention actually reaches, and each says what it does.
100
+
101
+ The height is capped well under a dialog's, because it is placed inside one and a dialog
102
+ scrolls its own box: anything taller is not long, it is cut off. */
103
+ .mentions{position:absolute;z-index:30;min-width:280px;max-width:min(420px,90%);max-height:196px;
104
+ overflow-y:auto;background:var(--panel);border:1px solid var(--line);border-radius:8px;
105
+ padding:4px;box-shadow:0 14px 34px rgba(0,0,0,.28)}
106
+ .mrow{display:grid;grid-template-columns:auto 1fr;gap:2px 8px;width:100%;text-align:left;
107
+ background:none;border:0;border-radius:6px;padding:6px 8px;cursor:pointer;font:inherit}
108
+ .mrow:hover{background:var(--panel-2)}
109
+ .mrow[aria-selected="true"]{background:var(--panel-2);box-shadow:inset 2px 0 0 var(--accent)}
110
+ .mrow .mat{font:12px var(--mono);color:var(--accent)}
111
+ .mrow .mname{font:12.5px var(--sans);color:var(--ink);overflow:hidden;text-overflow:ellipsis;
112
+ white-space:nowrap}
113
+ .mrow .mnote{grid-column:1/-1;font:10.5px var(--sans);color:var(--ink-faint)}
114
+ .mrow.human .mat{color:var(--ruled)}
115
+ /* The measuring copy of a textarea, for finding the caret. Never seen. */
116
+ .mmirror{position:absolute;visibility:hidden;white-space:pre-wrap;word-wrap:break-word;
117
+ pointer-events:none;top:0;left:0}
118
+
119
+ .attach{display:flex;align-items:center;gap:10px;margin-top:9px;flex-wrap:wrap}
120
+ .attach .ghbtn{display:inline-flex;align-items:center;gap:6px;--ic:13px}
121
+ /* The whole textarea is the drop target, because that is where a person aims. */
122
+ textarea.dropping{border-color:var(--accent);background:color-mix(in srgb,var(--accent) 7%,transparent)}
35
123
 
36
124
  /* ---------- timeline events ----------
37
125
  Not comments, so not comment-shaped: a marker, a line of prose, and whatever it points at.
@@ -51,7 +139,7 @@
51
139
  .tev .twhen{color:var(--ink-faint);font-size:11px;white-space:nowrap}
52
140
  .tev.closed .tico{color:var(--err)}
53
141
  .tev.reopened .tico,.tev.merged .tico{color:var(--accent)}
54
- .tev.cross-referenced .tico{color:var(--will)}
142
+ .tev.cross-referenced .tico{color:var(--ruled)}
55
143
 
56
144
  /* An item this thread points at, as a row you can open here rather than a link that throws
57
145
  you out to GitHub. Open and closed read at a glance, which is the whole reason to show it. */
@@ -5,8 +5,12 @@
5
5
  .side{background:var(--panel);border-right:1px solid var(--line);padding:20px 0 14px;
6
6
  position:sticky;top:0;height:100vh;overflow-y:auto;display:flex;flex-direction:column}
7
7
  .brand{padding:0 20px 18px;font:600 15px var(--sans);letter-spacing:-.01em}
8
+ /* The tab icon, in the corner it belongs in. Drawn rather than fetched so it takes the
9
+ theme's accent and costs no request. */
10
+ .brand .mark{display:inline-block;width:9px;height:9px;border-radius:50%;
11
+ background:var(--accent);margin-right:7px;vertical-align:1px}
8
12
  .brand small{display:block;color:var(--ink-faint);font-weight:400;font-size:11.5px;
9
- letter-spacing:.02em;margin-top:2px}
13
+ letter-spacing:.02em;margin-top:2px;margin-left:16px}
10
14
  .refreshall{display:flex;align-items:center;gap:8px;width:calc(100% - 24px);margin:0 12px 4px;
11
15
  background:var(--panel-2);border:1px solid var(--line);color:var(--ink-faint);
12
16
  border-radius:7px;padding:7px 10px;cursor:pointer;font:11px var(--sans);text-align:left}
@@ -67,20 +71,38 @@ main{padding:28px 34px 80px;max-width:1180px;overflow-x:hidden}
67
71
  .tfile:focus{outline:none}
68
72
  .tfile:focus-visible{outline:2px solid var(--accent-dim);outline-offset:-2px}
69
73
 
74
+ /* A search result: the page, and the lines the words were actually found in. Without the lines
75
+ this is a filter on titles, which is the thing that was not good enough. */
76
+ .tfile.thit{align-items:flex-start;padding:8px}
77
+ .tfile.thit .t{white-space:normal;overflow:visible}
78
+ .thit .hitname{display:block;font:13px var(--sans);color:var(--ink)}
79
+ .thit[aria-current="true"] .hitname{color:var(--accent)}
80
+ .thit .hitline{display:block;font:11.5px/1.5 var(--sans);color:var(--ink-faint);margin-top:3px;
81
+ overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}
82
+ .thit .hitline em{font-style:normal;color:var(--ink);background:color-mix(in srgb,var(--accent) 22%,transparent);
83
+ border-radius:3px;padding:0 2px}
84
+
70
85
  .viewer{background:var(--panel);border:1px solid var(--line);border-radius:9px;padding:20px 22px;
71
86
  min-height:340px;max-height:74vh;overflow:auto}
72
87
  .viewer pre{margin:0;white-space:pre-wrap;word-break:break-word;font:12.5px/1.65 var(--mono);color:var(--ink-dim)}
73
88
  /* Source keeps its own line breaks and scrolls, rather than wrapping into soup. */
74
89
  .viewer pre.code{white-space:pre;word-break:normal;overflow-x:auto}
75
90
  .viewer img{max-width:100%;border-radius:6px;background:#fff}
91
+ /* A recording in a brain gets the same room a screenshot does. Black rather than white
92
+ behind it: letterboxing should read as the video's own edges, not as a broken image. */
93
+ .viewer video.player,.viewer audio.player{max-width:100%;width:100%;border-radius:6px;background:#000}
94
+ .viewer audio.player{background:none}
76
95
  .viewer table{border-collapse:collapse;font:12px var(--mono);width:100%}
77
96
  .viewer td,.viewer th{border:1px solid var(--line);padding:4px 8px;text-align:left;white-space:nowrap}
78
97
  .viewer th{position:sticky;top:0;background:var(--panel-2);color:var(--ink)}
79
98
 
80
99
  .gallery{display:grid;gap:10px;grid-template-columns:repeat(auto-fill,minmax(150px,1fr))}
81
- .gallery figure{margin:0}
82
- .gallery img{width:100%;aspect-ratio:4/3;object-fit:contain;background:var(--panel-2);border-radius:6px;
83
- border:1px solid var(--line);cursor:pointer}
100
+ .gallery figure{margin:0;position:relative}
101
+ .gallery img,.gallery video{width:100%;aspect-ratio:4/3;object-fit:contain;background:var(--panel-2);
102
+ border-radius:6px;border:1px solid var(--line);cursor:pointer;display:block}
103
+ /* A still frame and a photograph look identical in a grid, so the video says which it is. */
104
+ .playmark{position:absolute;top:8px;right:8px;display:inline-flex;padding:4px;border-radius:50%;
105
+ background:color-mix(in srgb,#000 55%,transparent);color:#fff;--ic:12px;pointer-events:none}
84
106
  .gallery figcaption{font:10.5px var(--mono);color:var(--ink-faint);margin-top:4px;
85
107
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
86
108
 
@@ -21,7 +21,7 @@
21
21
  padding:11px 13px;font:12px/1.62 var(--mono);margin:10px 0;color:var(--ink-dim);
22
22
  overflow-x:auto;white-space:pre}
23
23
  .md pre code{background:none;padding:0;color:inherit;font-size:inherit}
24
- .md a{color:var(--will)}
24
+ .md a{color:var(--ruled)}
25
25
  .md blockquote{margin:10px 0;padding:2px 0 2px 12px;border-left:2px solid var(--line);color:var(--ink-dim)}
26
26
  .md hr{border:0;border-top:1px solid var(--line);margin:16px 0}
27
27
  .md img{max-width:100%;border-radius:6px;background:var(--panel-2)}
@@ -35,12 +35,25 @@
35
35
  .cbody h1{font-size:17px} .cbody h2{font-size:15.5px} .cbody h3{font-size:14px}
36
36
  .doc{font-size:14px;max-width:80ch}
37
37
 
38
+ /* ---------- YAML ----------
39
+ org.yaml and staff.yaml are the two files here anybody reads closely, and flat grey made
40
+ finding a key in one a matter of reading every line. Structure is what YAML has that prose
41
+ does not; these five colours are it, and nothing else is coloured. Tokens from the theme,
42
+ so light and dark both work without a second palette. */
43
+ .yk{color:var(--ruled)} /* keys */
44
+ .ys{color:var(--accent)} /* quoted strings */
45
+ .yn{color:var(--derived)} /* numbers */
46
+ .yb{color:var(--derived)} /* true, false, null */
47
+ .yc{color:var(--ink-faint);font-style:italic} /* comments */
48
+ .yp,.yd{color:var(--ink-faint)} /* colons, brackets, list dashes */
49
+ pre.yaml{color:var(--ink)}
50
+
38
51
  /* A reference to another issue or PR, as a chip rather than a raw link: half of a status
39
52
  issue is #113, and a wall of blue URLs is the thing that makes them unreadable. */
40
- .ref{display:inline-block;font:11.5px var(--mono);color:var(--will);background:var(--panel-2);
53
+ .ref{display:inline-block;font:11.5px var(--mono);color:var(--ruled);background:var(--panel-2);
41
54
  border:1px solid var(--line);border-radius:20px;padding:0 7px;text-decoration:none;
42
55
  white-space:nowrap;line-height:1.65}
43
- .ref:hover{border-color:var(--will);background:color-mix(in srgb,var(--will) 14%,transparent)}
56
+ .ref:hover{border-color:var(--ruled);background:color-mix(in srgb,var(--ruled) 14%,transparent)}
44
57
  .ref.pr{color:var(--measured)}
45
58
  .ref.pr:hover{border-color:var(--measured);background:color-mix(in srgb,var(--measured) 14%,transparent)}
46
59
 
@@ -52,3 +65,10 @@
52
65
  .at:hover{color:var(--ink);border-color:var(--ink-faint)}
53
66
  .at.you{color:var(--derived);border-color:color-mix(in srgb,var(--derived) 40%,transparent)}
54
67
  .at.you:hover{background:color-mix(in srgb,var(--derived) 14%,transparent);border-color:var(--derived)}
68
+
69
+ /* A screenshot on a doc page.
70
+ Bordered and inset so it reads as a figure rather than the page bleeding into another one:
71
+ these are pictures of this same UI, and without a frame the two run together. */
72
+ .md img{display:block;max-width:100%;height:auto;margin:14px 0;
73
+ border:1px solid var(--line);border-radius:8px}
74
+ .md img+em,.md p>img+em{display:block}
@@ -34,7 +34,10 @@
34
34
  .btn{background:var(--panel-2);border:1px solid var(--line);color:var(--ink);border-radius:7px;
35
35
  padding:8px 13px;font:13px var(--sans);cursor:pointer;text-decoration:none;display:inline-block}
36
36
  .btn:hover{border-color:var(--accent-dim)}
37
- .btn.primary{background:var(--accent-dim);border-color:var(--accent-dim);color:var(--bg);font-weight:600}
37
+ /* The full accent, not the dim one. Dim on the page background is pale-green-on-white with
38
+ near-white text: the button that does the main thing on the screen read as disabled. */
39
+ .btn.primary{background:var(--accent);border-color:var(--accent);color:var(--bg);font-weight:600}
40
+ .btn.primary:hover{filter:brightness(1.08);background:var(--accent);border-color:var(--accent)}
38
41
  .btn[disabled]{opacity:.5;cursor:default}
39
42
 
40
43
  .cmd{background:var(--panel-2);border:1px solid var(--line);border-radius:7px;padding:10px 12px;
@@ -93,12 +96,14 @@
93
96
 
94
97
  .repolist{display:grid;gap:1px;max-height:280px;overflow-y:auto;
95
98
  border:1px solid var(--line);border-radius:7px;background:var(--line)}
96
- .repo{display:flex;align-items:center;justify-content:space-between;gap:12px;
99
+ /* Scoped to the picker. Bare `.repo` also matched the project label on every inbox row,
100
+ which is where the white box and the stray padding behind MARKETING came from. */
101
+ .repolist .repo{display:flex;align-items:center;justify-content:space-between;gap:12px;
97
102
  background:var(--panel);padding:9px 12px}
98
- .repo.added{background:var(--panel-2)}
99
- .repo b{font:600 12.5px var(--mono)}
100
- .repo .meta{margin-left:8px}
101
- .repo .sub{margin:2px 0 0;font-size:11.5px}
103
+ .repolist .repo.added{background:var(--panel-2)}
104
+ .repolist .repo b{font:600 12.5px var(--mono)}
105
+ .repolist .repo .meta{margin-left:8px}
106
+ .repolist .repo .sub{margin:2px 0 0;font-size:11.5px}
102
107
 
103
108
  /* Which of the two things this is: a new org, or one that already runs roster. */
104
109
  .verdict{background:var(--panel-2);border:1px solid var(--line);border-radius:7px;
@@ -21,7 +21,7 @@
21
21
  <body>
22
22
  <div class="app">
23
23
  <aside class="side">
24
- <div class="brand">roster<small id="orgname">loading…</small></div>
24
+ <div class="brand"><span class="mark"></span>roster<small id="orgname">loading…</small></div>
25
25
  <button class="refreshall" id="refreshall" title="Reload everything from disk (r)">
26
26
  <span class="sync" data-icon="refresh"></span><span id="loaded">data now</span>
27
27
  </button>
@@ -30,6 +30,12 @@
30
30
  <span class="ic" data-icon="inbox"></span> Inbox
31
31
  <span class="n" id="inboxcount"></span>
32
32
  </button>
33
+ <!-- "Pending work", not "Pull requests": what is behind it is finished work waiting on you,
34
+ and the word for how it is delivered is not the word for what it is. -->
35
+ <button class="nav" id="prsnav" data-view="prs" title="Finished work waiting on you to merge">
36
+ <span class="ic" data-icon="merged"></span> Pending work
37
+ <span class="n" id="prcount"></span>
38
+ </button>
33
39
  <button class="nav" id="orgnav" data-view="org">
34
40
  <span class="ic" data-icon="file"></span> Org
35
41
  </button>
@@ -7,6 +7,18 @@ export const getDocs = () => json("/api/docs");
7
7
  export const getInbox = (force) => json("/api/inbox" + (force ? "?refresh=1" : ""));
8
8
  export const getSync = () => json("/api/sync").catch(() => null);
9
9
 
10
+ /** The repos org.yaml lists. Off disk, so the new-issue form does not wait on the inbox. */
11
+ export const getRepos = () => json("/api/repos");
12
+
13
+ /** What one repo's labels actually are, so they can be offered rather than typed. */
14
+ export const getLabels = (repo) => json("/api/labels?repo=" + encodeURIComponent(repo));
15
+
16
+ /** Full-text across the docs. Titles are not enough: what you want is usually a paragraph. */
17
+ export const searchDocs = (q) => json("/api/docsearch?q=" + encodeURIComponent(q));
18
+
19
+ /** Every file in the org layer that the portal may write, off disk rather than hardcoded. */
20
+ export const getOrgLayer = () => json("/api/orglayer");
21
+
10
22
  export const getDoc = (page) =>
11
23
  fetch("/api/doc?page=" + encodeURIComponent(page), { cache: "no-store" }).then((r) => r.text());
12
24
 
@@ -18,6 +30,10 @@ export const getThread = (repo, number, kind) =>
18
30
  "/api/thread?repo=" + encodeURIComponent(repo) + "&number=" + number + "&kind=" + kind,
19
31
  );
20
32
 
33
+ /** A pull request's commits and its diff. Asked for on the Files or Commits tab, not before. */
34
+ export const getPr = (repo, number) =>
35
+ json("/api/pr?repo=" + encodeURIComponent(repo) + "&number=" + number);
36
+
21
37
  export const diffUrl = (dir, sha, path) =>
22
38
  "/api/diff?dir=" + encodeURIComponent(dir) + "&sha=" + encodeURIComponent(sha) +
23
39
  (path ? "&path=" + encodeURIComponent(path) : "");
@@ -35,6 +51,23 @@ export async function post(payload, url = "/api/act") {
35
51
  return data;
36
52
  }
37
53
 
54
+ /**
55
+ * Put a file in the repo an issue lives in, and get back the path and the link.
56
+ *
57
+ * Read here rather than posted as a form because everything else on this server speaks JSON,
58
+ * and a file small enough to belong in a repo is small enough to base64.
59
+ */
60
+ export async function upload(repo, file) {
61
+ const data = await new Promise((ok, no) => {
62
+ const r = new FileReader();
63
+ r.onerror = () => no(new Error("could not read " + file.name));
64
+ // A data URL is "data:<type>;base64,<payload>"; only the payload travels.
65
+ r.onload = () => ok(String(r.result).split(",")[1] ?? "");
66
+ r.readAsDataURL(file);
67
+ });
68
+ return post({ repo, name: file.name, data }, "/api/upload");
69
+ }
70
+
38
71
  /* ------------------------------- setup and authoring ------------------------------- */
39
72
 
40
73
  export const getSetup = () => json("/api/setup/status");