pocolobby 0.12.2

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 (76) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +35 -0
  3. package/dist/activity.js +1 -0
  4. package/dist/adapters/claude.js +7 -0
  5. package/dist/attachments.js +2 -0
  6. package/dist/auth.js +4 -0
  7. package/dist/bin.js +4 -0
  8. package/dist/board-caption.js +19 -0
  9. package/dist/board-github.js +1 -0
  10. package/dist/board-model.js +1 -0
  11. package/dist/board-page.js +418 -0
  12. package/dist/board-summary.js +22 -0
  13. package/dist/board.js +1 -0
  14. package/dist/catalog.js +1 -0
  15. package/dist/cli.js +123 -0
  16. package/dist/client.js +1 -0
  17. package/dist/code-stats.js +3 -0
  18. package/dist/config.js +3 -0
  19. package/dist/console-page.js +467 -0
  20. package/dist/console.js +1 -0
  21. package/dist/exchange-rates.js +1 -0
  22. package/dist/hook.js +1 -0
  23. package/dist/html.js +1 -0
  24. package/dist/icons.js +4 -0
  25. package/dist/launch.js +2 -0
  26. package/dist/local-files.js +1 -0
  27. package/dist/lock.js +1 -0
  28. package/dist/mail.js +4 -0
  29. package/dist/mascots.js +6 -0
  30. package/dist/memory-curate.js +59 -0
  31. package/dist/memory.js +5 -0
  32. package/dist/money.js +1 -0
  33. package/dist/node-version.js +1 -0
  34. package/dist/pace.js +1 -0
  35. package/dist/png.js +1 -0
  36. package/dist/presentation.js +1 -0
  37. package/dist/prices.js +1 -0
  38. package/dist/protocol.js +1 -0
  39. package/dist/recurrence.js +1 -0
  40. package/dist/secrets.js +1 -0
  41. package/dist/server.js +8 -0
  42. package/dist/statusline.js +7 -0
  43. package/dist/store.js +303 -0
  44. package/dist/transcripts.js +2 -0
  45. package/docs/AGENT-SETUP-PROMPT.md +72 -0
  46. package/docs/API.md +1097 -0
  47. package/docs/ARCHITECTURE.md +410 -0
  48. package/docs/CLAUDE-SETUP.md +166 -0
  49. package/docs/INTEGRATION-PLAN.md +122 -0
  50. package/docs/LOCAL-SETUP.md +356 -0
  51. package/docs/MEMORY.md +142 -0
  52. package/docs/OPERATIONS.md +320 -0
  53. package/docs/QUICKSTART.md +111 -0
  54. package/docs/RELEASING.md +40 -0
  55. package/docs/SELF-HOSTING.md +236 -0
  56. package/docs/UPGRADES-WITHOUT-AGENT-RESTARTS.md +112 -0
  57. package/examples/CLAUDE.md +50 -0
  58. package/examples/PACING.md +33 -0
  59. package/examples/POCOLOBBY.md +69 -0
  60. package/examples/fly.toml +42 -0
  61. package/package.json +68 -0
  62. package/site/agents/gonk-server.webp +0 -0
  63. package/site/agents/luke-sleep.webp +0 -0
  64. package/site/agents/mouse-empty.webp +0 -0
  65. package/site/agents/mouse-envelope.webp +0 -0
  66. package/site/agents/poco-bell.webp +0 -0
  67. package/site/agents/vader-asleep.webp +0 -0
  68. package/site/agents/vader-crate.webp +0 -0
  69. package/site/agents/vader-deckchair.webp +0 -0
  70. package/site/agents/vader-point.webp +0 -0
  71. package/site/agents/vader-thumbs.webp +0 -0
  72. package/site/icons/apple-touch-icon.png +0 -0
  73. package/site/icons/favicon-32.png +0 -0
  74. package/site/icons/favicon.ico +0 -0
  75. package/site/icons/favicon.svg +12 -0
  76. package/site/index.html +204 -0
@@ -0,0 +1,467 @@
1
+ var ie=Object.defineProperty;var p=(s,e)=>ie(s,"name",{value:e,configurable:!0});import{EMPTY_CSS as de,emptyState as j,mascot as H}from"./mascots.js";import{newestFirst as le}from"./memory.js";import{ICON_LINKS as ce,iconVersion as G,lobbyIconLinks as pe}from"./icons.js";import{MODEL_NAMES as P,SUMMARY_EFFORTS as me,SUMMARY_MODELS as ue}from"./board-model.js";import{EARLY_SCRIPT as he,escape as a,FONTS as ge,MEMORY_RULE_CSS as be,memoryLine as fe,HEADER_CSS as ye,THEME_SCRIPT as ve,THEME_SWITCH as $e,topBar as we,TOKENS as xe}from"./board-page.js";import{DOLLARS as K,money as I,rateNote as q}from"./money.js";const ke=[["overview","Overview"],["rooms","Rooms and repos"],["summaries","AI summaries"],["access","Dashboard access"],["agents","Agents"],["tokens","Tokens"],["memory","Memory"],["activity","Activity"],["general","General"]],_e=`${xe}
2
+ /* Layout: the lobby owner's settings. A side list of sections on wide screens, tabs on a phone; each section is one job. Same tokens as the dashboard. */
3
+ html, body { margin: 0; }
4
+ .wrap { max-width: 1080px; margin: 0 auto; padding-block: 0 56px; padding-left: max(16px, env(safe-area-inset-left, 0px)); padding-right: max(16px, env(safe-area-inset-right, 0px)); }
5
+ h1, h2, h3 { font-family: var(--display); margin: 0; letter-spacing: -0.01em; text-wrap: balance; }
6
+ .mono { font-family: var(--mono); font-size: 0.86em; }
7
+ .muted { color: var(--muted); }
8
+ button, select, input { font: inherit; color: inherit; }
9
+ :focus-visible { outline: 2px solid var(--brass); outline-offset: 2px; }
10
+ a { color: inherit; }
11
+
12
+ ${ye}
13
+ /* Who is signed in, and Sign out, under every page. */
14
+ .foot { margin-top: 40px; padding-top: 14px; border-top: 1px solid var(--line); font-size: 13px; color: var(--muted); display: flex; gap: 8px 12px; align-items: center; flex-wrap: wrap; }
15
+ .foot form { margin: 0; }
16
+ .group > .seg { justify-self: start; }
17
+ /* The agents appear only where a page has a moment: empty lists, sign-in, a dead end. */
18
+ ${de}
19
+ .welcome { display: flex; align-items: center; gap: 14px; margin-bottom: 14px; }
20
+ .welcome .welcome-agent { width: 72px; margin: -8px 0 -6px; }
21
+ .welcome p { margin: 0; color: var(--muted); }
22
+ .welcome b { display: block; color: var(--ink); font: 600 17px var(--display); }
23
+ .seg { display: inline-flex; background: var(--sunk); border-radius: 999px; padding: 3px; gap: 2px; flex-wrap: wrap; }
24
+ .seg button, .seg label { font: 600 12.5px var(--body); color: var(--muted); background: none; border: 0; border-radius: 999px; padding: 5px 11px; cursor: pointer; }
25
+ .seg button[aria-pressed="true"], .seg label:has(input:checked) { background: var(--surface); color: var(--ink); box-shadow: 0 1px 2px rgb(0 0 0 / 0.12); }
26
+ .seg label:has(input:focus-visible) { outline: 2px solid var(--brass); outline-offset: 2px; }
27
+ .seg input, .choice input { position: absolute; opacity: 0; pointer-events: none; }
28
+ .seg a { font: 600 12.5px var(--body); color: var(--muted); text-decoration: none; border-radius: 999px; padding: 5px 11px; }
29
+ .seg a[aria-current="page"] { background: var(--surface); color: var(--ink); box-shadow: 0 1px 2px rgb(0 0 0 / 0.12); }
30
+ .linkish { font: 600 13px var(--body); color: var(--muted); background: none; border: 1px solid var(--line); border-radius: 999px; padding: 4px 12px; cursor: pointer; }
31
+
32
+ .note { margin: 18px 0 0; background: var(--brass-soft); border-radius: var(--radius); padding: 12px 16px; font-size: 14px; }
33
+ .note p { margin: 0; }
34
+ .layout { display: grid; grid-template-columns: 200px minmax(0, 1fr); gap: 28px; margin-top: 22px; }
35
+ .single { margin-top: 22px; max-width: 760px; }
36
+ nav.sections { display: grid; gap: 2px; align-content: start; position: sticky; top: 76px; }
37
+ nav.sections a { text-decoration: none; border-radius: 8px; padding: 9px 12px; font: 500 14.5px var(--body); color: var(--muted); }
38
+ nav.sections a[aria-current="page"] { background: var(--surface); color: var(--ink); font-weight: 600; box-shadow: inset 0 0 0 1px var(--line); }
39
+
40
+ .panel-head { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 6px 16px; margin-bottom: 14px; }
41
+ .panel-head h1 { font-size: 26px; overflow-wrap: anywhere; }
42
+ .panel-head p { margin: 4px 0 0; color: var(--muted); max-width: 62ch; }
43
+ .card { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); }
44
+ .stack { display: grid; gap: 12px; }
45
+ .pad { padding: 16px 18px; display: grid; gap: 12px; }
46
+ .pad p { margin: 0; }
47
+
48
+ .chip { display: inline-flex; align-items: center; gap: 6px; font: 600 12px var(--body); border-radius: 999px; padding: 3px 10px 3px 8px; white-space: nowrap; }
49
+ .chip::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: currentColor; }
50
+ .c-ok { color: var(--ok); background: var(--ok-soft); }
51
+ .c-warn { color: var(--warn); background: var(--warn-soft); }
52
+ .c-crit { color: var(--crit); background: var(--crit-soft); }
53
+ .c-off { color: var(--muted); background: var(--sunk); }
54
+
55
+ .btn { border: 1px solid var(--line); background: var(--surface); border-radius: 8px; padding: 7px 13px; font-weight: 600; font-size: 13.5px; cursor: pointer; text-decoration: none; display: inline-block; }
56
+ .btn.primary { background: var(--ink); color: var(--bg); border-color: var(--ink); }
57
+ .btn.danger { color: var(--crit); border-color: color-mix(in srgb, var(--crit) 45%, var(--line)); }
58
+ .btn[disabled] { opacity: 0.5; cursor: not-allowed; }
59
+ .row-actions { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
60
+ details.toggle > summary { list-style: none; display: inline-block; }
61
+ details.toggle > summary::-webkit-details-marker { display: none; }
62
+ details.toggle[open] > summary { background: var(--sunk); }
63
+ details.toggle[open] > summary.primary { background: var(--sunk); color: var(--ink); border-color: var(--line); }
64
+ .adder { display: grid; gap: 10px; justify-items: start; }
65
+ .adder > form { justify-self: stretch; }
66
+ .status { margin: 0; font-size: 13.5px; min-height: 0; }
67
+ .status:empty { display: none; }
68
+ .status.error { color: var(--crit); }
69
+ .status.ok { color: var(--ok); }
70
+
71
+ .room header { display: flex; flex-wrap: wrap; gap: 6px 12px; align-items: center; justify-content: space-between; }
72
+ .room h2 { font-size: 18px; overflow-wrap: anywhere; }
73
+ .room h2 .hash { color: var(--muted); font-weight: 500; }
74
+ .members { font-size: 13px; color: var(--muted); display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
75
+ .member { display: inline-flex; align-items: center; gap: 2px; background: var(--sunk); border-radius: 999px; padding: 1px 4px 1px 10px; color: var(--ink); }
76
+ .member form { margin: 0; display: inline; }
77
+ .member button { border: 0; background: none; color: var(--muted); cursor: pointer; font-size: 14px; line-height: 1; padding: 2px 6px; border-radius: 999px; }
78
+ .source { display: grid; grid-template-columns: 120px minmax(0, 1fr); gap: 6px 14px; font-size: 14px; margin: 0; }
79
+ .source dt { color: var(--muted); font-size: 13px; }
80
+ .source dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
81
+ .files { display: flex; flex-wrap: wrap; gap: 6px; }
82
+ .file { font: 500 12.5px var(--mono); background: var(--sunk); border-radius: 6px; padding: 2px 8px; overflow-wrap: anywhere; }
83
+ .empty-source { border: 1.5px dashed var(--line); border-radius: 8px; padding: 12px 14px; font-size: 14px; color: var(--muted); }
84
+ .edit { border-top: 1px solid var(--line); padding-top: 14px; display: grid; gap: 12px; margin-top: 12px; }
85
+ form { margin: 0; }
86
+ .fields { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; }
87
+ label.field { display: grid; gap: 4px; font-size: 12.5px; color: var(--muted); font-weight: 600; }
88
+ label.field input, label.field select { font-size: 15px; font-weight: 400; color: var(--ink); background: var(--bg); border: 1px solid var(--line); border-radius: 8px; padding: 8px 10px; min-width: 0; width: 100%; }
89
+ .hint { font-size: 13px; color: var(--muted); margin: 0; }
90
+ .files-line { font-size: 13px; color: var(--muted); margin-top: 4px; }
91
+ .subhead { font: 600 12px var(--body); text-transform: uppercase; letter-spacing: 0.08em; color: var(--muted); margin: 14px 0 0; }
92
+ .grow { flex: 1 1 240px; }
93
+ .end { align-self: end; }
94
+ .narrow { max-width: 320px; }
95
+ /* A field and its button with room between them. */
96
+ .row-actions.apart { gap: 16px; }
97
+ /* A date field of its own width: Safari on an iPhone draws its date control wider than a box sized
98
+ to the label, under the button beside it, unless the control drops its native look. */
99
+ label.field.date { width: 13rem; max-width: 100%; }
100
+ label.field input[type="date"] { -webkit-appearance: none; appearance: none; min-height: 40px; }
101
+ label.field input[type="date"]::-webkit-date-and-time-value { text-align: left; }
102
+
103
+ .group { display: grid; gap: 8px; }
104
+ .group > .label { font: 600 12px var(--body); text-transform: uppercase; letter-spacing: 0.08em; color: var(--muted); }
105
+ .choices { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
106
+ .choice { position: relative; border: 1px solid var(--line); border-radius: 9px; padding: 12px 14px; display: grid; gap: 2px; cursor: pointer; background: var(--bg); }
107
+ .choice:has(input:checked) { border-color: var(--brass); box-shadow: inset 0 0 0 1px var(--brass); background: var(--surface); }
108
+ .choice:has(input:focus-visible) { outline: 2px solid var(--brass); outline-offset: 2px; }
109
+ .choice.off { cursor: not-allowed; opacity: 0.6; }
110
+ .choice strong { font-family: var(--display); font-size: 15px; }
111
+ .choice span { font-size: 13px; color: var(--muted); }
112
+ .choice .price { font: 500 12px var(--mono); }
113
+ .keyline { display: flex; flex-wrap: wrap; gap: 8px 14px; align-items: center; font-size: 14px; }
114
+ .usage { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
115
+ .stat { background: var(--sunk); border-radius: 8px; padding: 10px 12px; }
116
+ .stat .n { font: 700 20px var(--display); font-variant-numeric: tabular-nums; }
117
+ .stat .k { font-size: 12.5px; color: var(--muted); }
118
+ .warning { background: var(--warn-soft); border-radius: 8px; padding: 10px 12px; font-size: 13.5px; margin: 0; }
119
+
120
+ .addr { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
121
+ .icon-now { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; }
122
+ .icon-now img { border-radius: 12px; background: var(--sunk); }
123
+ .addr code { font: 500 15px var(--mono); background: var(--sunk); border-radius: 8px; padding: 6px 10px; overflow-wrap: anywhere; }
124
+ ul.rows { margin: 0; padding: 0; }
125
+ ul.rows li { list-style: none; display: flex; justify-content: space-between; align-items: center; gap: 8px 12px; padding: 10px 16px; border-top: 1px solid var(--line); font-size: 14px; flex-wrap: wrap; }
126
+ ul.rows li:first-child { border-top: 0; }
127
+ /* Tokens: a period's cost, then each model's tokens on a line of its own. */
128
+ ul.periods { margin: 0; padding: 0; }
129
+ ul.periods li { list-style: none; display: grid; gap: 2px; padding: 10px 16px; border-top: 1px solid var(--line); font-size: 14px; }
130
+ ul.periods li:first-child { border-top: 0; }
131
+ ul.periods .period-head { display: flex; justify-content: space-between; gap: 12px; }
132
+ ul.periods .period-head b { font-variant-numeric: tabular-nums; }
133
+ ul.periods .model { font-size: 12.5px; color: var(--muted); overflow-wrap: anywhere; }
134
+ ul.periods .model b { color: var(--ink); font-weight: 600; }
135
+ /* Lists in the Memory page's closed sections: fragments, refusals, the archive and runs. */
136
+ ul.memory { margin: 0; padding: 0; }
137
+ ul.memory li { list-style: none; display: grid; gap: 4px; padding: 12px 16px; border-top: 1px solid var(--line); font-size: 14px; }
138
+ ul.memory li:first-child { border-top: 0; }
139
+ ul.memory .entry-head { display: flex; justify-content: space-between; align-items: start; gap: 12px; }
140
+ ul.memory .entry-head strong { overflow-wrap: anywhere; }
141
+ ul.memory p { margin: 0; overflow-wrap: anywhere; white-space: pre-line; }
142
+ ul.memory .meta { font-size: 12.5px; color: var(--muted); overflow-wrap: anywhere; }
143
+ ul.memory .disputed { font-size: 13px; color: var(--warn); }
144
+ ul.memory form { flex: none; }
145
+ /* Memory: each entry one line, its lesson on a tap; the room's queue, refusals, runs and curator wait closed below. */
146
+ .mem { display: grid; gap: 12px; }
147
+ .mem-head { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 10px; }
148
+ .mem-head h2 { margin: 0; font: 600 17px var(--display); }
149
+ .mem > .hint { margin: 0; }
150
+ ${be}
151
+ .mem-tools { display: grid; gap: 8px; justify-items: start; }
152
+ .mem-count { font: 600 12px var(--body); text-transform: uppercase; letter-spacing: 0.08em; color: var(--muted); margin-bottom: -4px; }
153
+ .mem [data-show-all] { justify-self: start; }
154
+ .mem-tools input[type="search"] { font-size: 15px; color: var(--ink); background: var(--bg); border: 1px solid var(--line); border-radius: 8px; padding: 8px 10px; width: 100%; min-width: 0; }
155
+ ul.mem-list { margin: 0; padding: 0; list-style: none; border: 1px solid var(--line); border-radius: 10px; overflow: hidden; }
156
+ ul.mem-list > li + li { border-top: 1px solid var(--line); }
157
+ ul.mem-list > li > details > summary { padding: 10px 12px; cursor: pointer; list-style: none; font-size: 14.5px; }
158
+ ul.mem-list > li > details > summary::-webkit-details-marker { display: none; }
159
+ ul.mem-list > li > details[open] > summary { background: var(--sunk); }
160
+ .kind { flex: none; font: 600 11px var(--body); text-transform: uppercase; letter-spacing: 0.04em; border-radius: 999px; padding: 1px 7px; background: var(--sunk); color: var(--muted); }
161
+ .k-gotcha { color: var(--warn); background: var(--warn-soft); }
162
+ .k-fix { color: var(--ok); background: var(--ok-soft); }
163
+ ul.mem-list .flag { flex: none; font: 600 11px var(--body); color: var(--crit); }
164
+ .mem-body { display: grid; gap: 8px; padding: 10px 12px 12px; font-size: 14px; }
165
+ .mem-body p { margin: 0; overflow-wrap: anywhere; white-space: pre-line; }
166
+ .mem-body .meta { font-size: 12.5px; color: var(--muted); }
167
+ .mem-body .disputed { color: var(--warn); }
168
+ details.sub > summary { font-size: 12.5px; color: var(--muted); cursor: pointer; }
169
+ details.sub ul { margin: 6px 0 0; padding-left: 18px; display: grid; gap: 4px; font-size: 12.5px; color: var(--muted); overflow-wrap: anywhere; }
170
+ details.sub ul b { color: var(--ink); font-weight: 600; }
171
+ details.sub p { margin-top: 6px; }
172
+ .mem-body .actions { display: flex; justify-content: flex-end; }
173
+ .mem-more { display: grid; }
174
+ details.more { border-top: 1px solid var(--line); }
175
+ details.more > summary { display: flex; align-items: center; gap: 8px; padding: 11px 0; cursor: pointer; list-style: none; font: 600 14px var(--body); }
176
+ details.more > summary::-webkit-details-marker { display: none; }
177
+ details.more > summary .count { margin-left: auto; color: var(--muted); font-weight: 500; }
178
+ details.more > summary::after { content: "\u203A"; color: var(--muted); transition: transform 0.15s; }
179
+ details.more[open] > summary::after { transform: rotate(90deg); }
180
+ details.more[open] { display: grid; gap: 10px; padding-bottom: 12px; }
181
+ ul.rows .main { min-width: 0; overflow-wrap: anywhere; }
182
+ .inline-rows li { padding-inline: 0; }
183
+ .avatar { display: inline-grid; place-items: center; width: 28px; height: 28px; border-radius: 50%; background: var(--sunk); margin-right: 8px; font: 700 13px var(--display); color: var(--muted); vertical-align: middle; }
184
+
185
+ .health { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; margin-top: 12px; }
186
+ .health .chip { justify-self: start; }
187
+ .health .card { padding: 14px 16px; display: grid; gap: 6px; align-content: start; }
188
+ .health h3 { font-size: 15px; }
189
+ .health p { margin: 0; font-size: 13.5px; color: var(--muted); overflow-wrap: anywhere; }
190
+
191
+ .signin { max-width: 440px; margin-top: 28px; }
192
+ .signin form { display: grid; gap: 12px; }
193
+
194
+ @media (max-width: 760px) {
195
+ .layout { grid-template-columns: minmax(0, 1fr); gap: 16px; }
196
+ nav.sections { position: static; display: flex; overflow-x: auto; gap: 4px; padding-bottom: 4px; }
197
+ nav.sections a { white-space: nowrap; }
198
+ .fields, .choices, .usage, .health { grid-template-columns: minmax(0, 1fr); }
199
+ .source { grid-template-columns: minmax(0, 1fr); gap: 2px; }
200
+ .source dt { margin-top: 6px; }
201
+ .panel-head h1 { font-size: 22px; }
202
+ }
203
+ `,Se=`
204
+ ${ve}for (const form of document.querySelectorAll('form[data-action]')) {
205
+ const auto = 'auto' in form.dataset;
206
+ form.addEventListener('submit', async (event) => {
207
+ event.preventDefault();
208
+ if (form.dataset.sending) return;
209
+ form.dataset.sending = '1';
210
+ const status = form.querySelector('[data-status]');
211
+ const body = {};
212
+ const say = (text, kind) => { if (status) { status.textContent = text; status.className = 'status' + (kind ? ' ' + kind : ''); } };
213
+ // An automatic form fails visibly, unsaved, with one way to send it again.
214
+ const fail = (text) => {
215
+ say(text, 'error');
216
+ if (!auto || !status) return;
217
+ const again = document.createElement('button');
218
+ again.type = 'button';
219
+ again.className = 'btn';
220
+ again.textContent = 'Try again';
221
+ again.addEventListener('click', () => form.requestSubmit());
222
+ status.append(' ', again);
223
+ };
224
+ say(form.dataset.busy || 'Working\u2026');
225
+ try {
226
+ for (const [key, value] of new FormData(form)) {
227
+ if (typeof value === 'string') { if (value.trim() !== '') body[key] = value.trim(); }
228
+ // A chosen file goes as base64, such as a dashboard's icon.
229
+ else if (value.size) body[key] = await new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(String(reader.result).split(',')[1] || ''); reader.onerror = () => reject(reader.error); reader.readAsDataURL(value); });
230
+ }
231
+ } catch {
232
+ delete form.dataset.sending;
233
+ fail('That file could not be read.');
234
+ return;
235
+ }
236
+ // Its controls wait while it is sent, so what is shown is what was sent; the values are read first.
237
+ const locked = [...form.elements].filter((field) => !field.disabled && (auto || field.tagName === 'BUTTON'));
238
+ for (const field of locked) field.disabled = true;
239
+ try {
240
+ // An automatic save is small and goes on if the page is left at once.
241
+ const response = await fetch(form.dataset.action, { method: 'POST', headers: { 'content-type': 'application/json' }, credentials: 'same-origin', keepalive: auto, body: JSON.stringify(body) });
242
+ let data = {};
243
+ try { data = await response.json(); } catch {}
244
+ if (!response.ok) { fail((data.error && data.error.message) || 'That did not work (' + response.status + ').'); return; }
245
+ if (data.redirect) { location.assign(data.redirect); return; }
246
+ if (form.dataset.reveal) {
247
+ const next = document.getElementById(form.dataset.reveal);
248
+ if (next) {
249
+ next.hidden = false;
250
+ for (const input of next.querySelectorAll('input[type="hidden"]')) if (input.name in body) input.value = body[input.name];
251
+ const focus = next.querySelector('input:not([type="hidden"])');
252
+ if (focus) focus.focus();
253
+ }
254
+ say(data.message || '', 'ok');
255
+ return;
256
+ }
257
+ if ('stay' in form.dataset) { say(data.message || (auto ? 'Saved.' : 'Done.'), 'ok'); return; }
258
+ location.reload();
259
+ } catch {
260
+ fail('Settings could not be reached. Check the connection and try again.');
261
+ } finally {
262
+ for (const field of locked) field.disabled = false;
263
+ delete form.dataset.sending;
264
+ }
265
+ });
266
+ // A choice takes effect when it is made, and a text field when it is left or Enter is pressed.
267
+ if (auto) {
268
+ form.addEventListener('change', () => { if (form.checkValidity()) form.requestSubmit(); });
269
+ form.addEventListener('keydown', (event) => {
270
+ if (event.key !== 'Enter' || event.isComposing || !event.target.matches('input')) return;
271
+ event.preventDefault();
272
+ if (form.checkValidity()) form.requestSubmit();
273
+ });
274
+ }
275
+ }
276
+ // A local broker's sign-in link carries a one-time ticket in its fragment, which never reaches a server or its logs.
277
+ const ticket = document.getElementById('ticket-step');
278
+ const carried = new URLSearchParams(location.hash.slice(1)).get('ticket');
279
+ if (ticket && carried) { history.replaceState(null, '', location.pathname); ticket.elements.ticket.value = carried; ticket.requestSubmit(); }
280
+ const current = document.querySelector('nav.sections [aria-current="page"]');
281
+ if (current) current.scrollIntoView({ block: 'nearest', inline: 'center' });
282
+ for (const button of document.querySelectorAll('[data-copy]')) button.addEventListener('click', async () => { try { await navigator.clipboard.writeText(button.dataset.copy); button.textContent = 'Copied'; } catch {} });
283
+ // The Memory page: a search and the kinds narrow a room's entries.
284
+ for (const room of document.querySelectorAll('.mem')) {
285
+ const search = room.querySelector('[data-memory-search]');
286
+ if (!search) continue;
287
+ const filters = room.querySelectorAll('[data-kind-filter]');
288
+ const entries = [...room.querySelectorAll('.mem-list > li')];
289
+ const count = room.querySelector('[data-count]');
290
+ const more = room.querySelector('[data-show-all]');
291
+ const latest = Number(room.querySelector('.mem-list').dataset.latest);
292
+ let kind = '';
293
+ let all = false;
294
+ // The newest entries until Show all; a search or a kind looks through every entry.
295
+ const apply = () => {
296
+ const words = search.value.toLowerCase().split(' ').filter(Boolean);
297
+ const narrowed = words.length > 0 || kind !== '';
298
+ let matches = 0;
299
+ for (const entry of entries) {
300
+ const text = (entry.textContent + ' ' + entry.dataset.keywords).toLowerCase();
301
+ const match = (!kind || entry.dataset.kind === kind || (kind === 'disputed' && 'disputed' in entry.dataset)) && words.every((word) => text.includes(word));
302
+ if (match) matches += 1;
303
+ entry.hidden = !match || (!narrowed && !all && matches > latest);
304
+ }
305
+ count.textContent = narrowed ? matches + ' of ' + entries.length : all || entries.length <= latest ? 'All ' + entries.length + ', newest first' : 'Newest ' + latest + ' of ' + entries.length;
306
+ if (more) more.hidden = narrowed || all || entries.length <= latest;
307
+ };
308
+ search.addEventListener('input', apply);
309
+ if (more) more.addEventListener('click', () => { all = true; apply(); });
310
+ // Without the script every entry shows; with it, the newest until Show all.
311
+ apply();
312
+ for (const button of filters)
313
+ button.addEventListener('click', () => {
314
+ kind = button.dataset.kindFilter;
315
+ for (const other of filters) other.setAttribute('aria-pressed', String(other === button));
316
+ apply();
317
+ });
318
+ }
319
+ `;function $(s,e,o){const n=s.signOut?`<footer class="foot"><span>${s.email?`Signed in as ${a(s.email)}`:"Signed in on this computer"}</span><form data-action="${a(s.signOut)}"><button class="linkish" type="submit">Sign out</button></form></footer>`:"",c=we({home:s.home??"/",dashboard:s.lobby?.url??"",settings:s.lobby?.settings??null,lobbies:!!s.signOut},s.lobby?{name:s.lobby.name,current:"settings"}:null),d=s.lobby?`<div class="layout"><nav class="sections" aria-label="Sections">${ke.map(([l,i])=>`<a href="${a(s.lobby.settings)}${l==="overview"?"":`/${l}`}"${l===s.section?' aria-current="page"':""}>${i}</a>`).join("")}</nav><div>${o}</div></div>`:`<div class="single">${o}</div>`;return`<!doctype html>
320
+ <html lang="en">
321
+ <head>
322
+ <meta charset="utf-8">
323
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
324
+ <meta name="referrer" content="same-origin">
325
+ <title>${a(e)}</title>
326
+ ${s.lobby?.icon?pe(G(s.lobby.icon),s.lobby.url.endsWith("/")?s.lobby.url:`${s.lobby.url}/`):ce}
327
+ <link rel="preconnect" href="https://fonts.googleapis.com">
328
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
329
+ <link rel="stylesheet" href="${ge}">
330
+ <style nonce="${s.nonce}">${_e}</style>
331
+ <script nonce="${s.nonce}">${he}</script>
332
+ </head>
333
+ <body>
334
+ ${c}
335
+ <main class="wrap">
336
+ <noscript><div class="note"><p>Settings need JavaScript for their buttons; the pages read without it.</p></div></noscript>
337
+ ${d}
338
+ ${n}
339
+ </main>
340
+ <script nonce="${s.nonce}">${Se}</script>
341
+ </body>
342
+ </html>
343
+ `}p($,"page");const f='<p class="status" role="status" data-status></p>',x=p((s,e,o="")=>`<div class="panel-head"><div><h1>${a(s)}</h1><p>${a(e)}</p></div>${o}</div>`,"head"),L=p((s,e,o={},n="")=>`<form data-action="${a(s)}">${Object.entries(o).map(([c,d])=>`<input type="hidden" name="${c}" value="${a(d)}">`).join("")}<button class="btn${n?` ${n}`:""}" type="submit">${a(e)}</button>${f}</form>`,"buttonForm"),A=p(s=>s?new Date(s).toLocaleDateString("en-GB",{day:"numeric",month:"short",year:"numeric",timeZone:"UTC"}):"not recorded","date"),Ae=p(s=>`${new Date(s).toLocaleString("en-GB",{day:"numeric",month:"short",year:"numeric",hour:"2-digit",minute:"2-digit",timeZone:"UTC"})} UTC`,"stamp"),_=p(s=>`${new Date(s).toLocaleString("en-GB",{day:"numeric",month:"short",hour:"2-digit",minute:"2-digit",timeZone:"UTC"})} UTC`,"time"),g=p((s,e,o=`${e}s`)=>`${s} ${s===1?e:o}`,"plural");function qe(s,e,o,n){return $(s,`${e} on PocoLobby`,`<div class="card">${j("mouse-empty",a(e),a(o),n?`<a class="btn primary" href="${a(n.href)}">${a(n.label)}</a>`:"")}</div>`)}p(qe,"renderNotice");function Be(s,e){if(e.local)return $(s,"Sign in to PocoLobby",`${x("Sign in to PocoLobby","This broker runs on your computer, so signing in needs no email.")}
344
+ <div class="card pad signin">
345
+ <div class="welcome">${H("poco-bell","welcome-agent",72)}<p><b>Welcome to the lobby</b>Run <code>pocolobby settings --config owner.json</code> on this computer, with your owner file. It opens this page signed in.</p></div>
346
+ <form id="ticket-step" data-action="/api/sign-in/ticket" data-busy="Signing in\u2026">
347
+ <input type="hidden" name="ticket" value="">
348
+ ${f}
349
+ </form>
350
+ </div>`);const o=e.lobby?`Sign in to open ${e.lobby}.`:"We send a six-digit code to your email. No password to remember.";if(e.missing.length)return $(s,"Sign in to PocoLobby",`${x("Sign in to PocoLobby",o)}<div class="card pad signin"><p>Sign-in is not configured on this server yet: ${a(e.missing.join(" and "))} ${e.missing.length===1?"is":"are"} not set. Agents are not affected.</p></div>`);const n=e.lobby?`<input type="hidden" name="lobby" value="${a(e.lobby)}">${e.to&&e.to!=="/"?`<input type="hidden" name="to" value="${a(e.to)}">`:""}${e.state?`<input type="hidden" name="state" value="${a(e.state)}">`:""}`:"";return $(s,"Sign in to PocoLobby",`${x("Sign in to PocoLobby",o)}
351
+ <div class="card pad signin">
352
+ <div class="welcome">${H("poco-bell","welcome-agent",72)}<p><b>Welcome to the lobby</b>Poco, at the front desk, sends your code.</p></div>
353
+ <form data-action="/api/sign-in/code" data-reveal="code-step" data-busy="Sending\u2026">
354
+ <label class="field" for="email">Email<input id="email" name="email" type="email" autocomplete="email" required maxlength="254" autofocus></label>
355
+ <div class="row-actions"><button class="btn primary" type="submit">Send code</button></div>
356
+ ${f}
357
+ </form>
358
+ <form id="code-step" data-action="/api/sign-in/verify" data-busy="Checking\u2026" hidden>
359
+ <input type="hidden" name="email" value="">${n}
360
+ <label class="field" for="code">Code from the email<input id="code" name="code" inputmode="numeric" autocomplete="one-time-code" pattern="[0-9]{6}" maxlength="6" required placeholder="123456"></label>
361
+ <p class="hint">The code works once, for 10 minutes. After 5 wrong tries, ask for a new one.</p>
362
+ <div class="row-actions"><button class="btn primary" type="submit">Sign in</button></div>
363
+ ${f}
364
+ </form>
365
+ </div>`)}p(Be,"renderSignIn");function Fe(s,e){const o=e.mayCreate&&(e.remaining===null||e.remaining>0)?`<details class="toggle adder"><summary class="btn primary">Create lobby</summary>
366
+ <form class="card pad" data-action="/api/lobbies" data-busy="Creating\u2026">
367
+ <div class="fields">
368
+ <label class="field" for="nl-name">Lobby name<input id="nl-name" name="lobby_id" required maxlength="48" pattern="[a-z]([a-z0-9-]*[a-z0-9])?" placeholder="my-team"></label>
369
+ <label class="field" for="nl-room">First room (optional)<input id="nl-room" name="first_room" maxlength="48" placeholder="general"></label>
370
+ </div>
371
+ <p class="hint">Lowercase letters, digits and inner dashes, starting with a letter. Its dashboard lives at ${a(e.address("name"))}. Names such as www, api, settings and v1 are reserved, and a deleted lobby's name is never used again.${e.remaining===null?"":` You can create ${g(e.remaining,"more lobby","more lobbies")}.`}</p>
372
+ <div class="row-actions"><button class="btn primary" type="submit">Create lobby</button></div>
373
+ ${f}
374
+ </form></details>`:"",n=e.owned.length?e.owned.map(d=>`<article class="card pad room"><header><h2>${a(d.display_name||d.lobby_id)}</h2><span class="chip ${d.agents?"c-ok":"c-off"}">${g(d.agents,"agent")} and ${g(d.rooms,"room")}</span></header><div class="members">${a(e.address(d.lobby_id))}, ${d.visibility} dashboard, created ${a(A(d.created_at))}</div><div class="row-actions"><a class="btn" href="${a(e.open(d.lobby_id))}">Open</a></div></article>`).join(""):`<div class="card">${j("gonk-server","No lobbies yet",e.mayCreate?"Create one below. Each lobby gets its own agents, rooms and dashboard.":"The server operator lets you create lobbies; ask them, or open a dashboard shared with you.")}</div>`,c=e.viewing.length?`<h2 class="subhead">Dashboards shared with you</h2><div class="card"><ul class="rows">${e.viewing.map(d=>`<li><span class="main"><strong>${a(d.display_name||d.lobby_id)}</strong> <span class="muted">dashboard shared with you</span></span><a class="btn" href="${a(d.url)}">Open dashboard</a></li>`).join("")}</ul></div>`:"";return $(s,"Your lobbies on PocoLobby",`${x("Your lobbies","Lobbies you own. Each one has its own agents, rooms and dashboard.")}<div class="stack">${n}${o}${c}</div>`)}p(Fe,"renderHome");function We(s,e){const o=s.lobby,n=e.config.sources.length,c=e.view.sources.filter(r=>r.commit!==null&&!r.error).length,d=e.view.sources.filter(r=>r.error),l=e.rooms.length?d.length?`<span class="chip c-crit">${g(d.length,"source")} failing</span>`:n<e.rooms.length?`<span class="chip c-warn">${n} of ${g(e.rooms.length,"room")} connected</span>`:`<span class="chip c-ok">All ${g(e.rooms.length,"room")} connected</span>`:'<span class="chip c-off">No rooms yet</span>',i=e.last?e.last.error?'<span class="chip c-crit">Last run failed</span>':'<span class="chip c-ok">Up to date</span>':'<span class="chip c-off">No summary yet</span>',u=e.agents.filter(r=>r.enabled),m=u.length?"":`<div class="card">${j("mouse-envelope","No agents yet",`The server operator admits each agent with <span class="mono">pocolobby agent add NAME --lobby ${a(o.id)}</span> and hands it its credential; it shows here once it connects.`)}</div>`;return $(s,`${o.name} in PocoLobby settings`,`${x("Overview",`${g(u.length,"agent")} and ${g(e.rooms.length,"room")}.`)}
375
+ ${m}
376
+ <div class="health">
377
+ <div class="card"><h3>Repositories</h3>${l}<p>${e.rooms.length?`${c} of ${g(n,"source")} read${d.length?`; ${a(d.map(r=>r.room_id).join(", "))} failing`:""}.`:"Add rooms, then pick a repository for each."}</p></div>
378
+ <div class="card"><h3>Summaries</h3>${i}<p>${a(P[e.config.summary_model])}, ${e.config.summary_effort} reasoning.${e.last?` Last run ${a(_(e.last.created_at))}.`:""}</p></div>
379
+ <div class="card"><h3>Dashboard</h3><span class="chip ${e.config.visibility==="public"?"c-warn":"c-ok"}">${e.config.visibility==="public"?"Public":"Private"}</span><p>${a(o.address)}, ${g(e.viewers,"viewer")} and ${g(e.sessions,"session")} open</p></div>
380
+ </div>`)}p(We,"renderOverview");function Ye(s,e){const o=s.lobby.id,n=`${s.lobby.api}/rooms`,c=e.agents.filter(r=>r.enabled).map(r=>r.agent_id),d=`<details class="toggle adder"><summary class="btn primary">Add room</summary>
381
+ <form class="card pad" data-action="${n}" data-busy="Creating\u2026">
382
+ <div class="fields"><label class="field" for="nr-name">Room name<input id="nr-name" name="room_id" required maxlength="48" pattern="[a-z][a-z0-9_-]*" placeholder="for example 3d"></label></div>
383
+ <p class="hint">Members and a repository are added after the room exists. A coordinator can also create rooms from its tools; both show in Activity.</p>
384
+ <div class="row-actions"><button class="btn primary" type="submit">Create room</button></div>
385
+ ${f}
386
+ </form></details>`,l=`<div class="card pad"><div class="keyline"><strong>GitHub</strong><span class="muted">${e.github?"read-only token on the server":"no token on the server: notes are not read"}, ${g(e.repos.length,"repository","repositories")} approved for this lobby</span><button class="btn" disabled title="For customer lobbies, later">Connect GitHub</button></div>${e.repos.length?"":'<p class="hint">The server operator approves repositories for a lobby (pocolobby repos).</p>'}</div>`,i=p(r=>e.repos.map(h=>`<option value="${a(h)}"${r?.toLowerCase()===h?" selected":""}>${a(h)}</option>`).join(""),"repoOptions"),u=e.rooms.map(r=>{const h=e.config.sources.find(b=>b.room_id===r.room_id)??null,S=e.view.sources.find(b=>b.room_id===r.room_id),w=e.snapshots.get(r.room_id),y=h?S?.error?'<span class="chip c-crit">Failing</span>':w?`<span class="chip c-ok">Read ${a(S?.fetched??"")} ago at ${a(w.commit_sha.slice(0,7))}</span>`:'<span class="chip c-warn">Not read yet</span>':'<span class="chip c-off">No repository</span>',E=p(b=>`<span class="member">${a(b)}<form data-action="${n}/${encodeURIComponent(r.room_id)}/members"><input type="hidden" name="remove" value="${a(b)}"><button type="submit" aria-label="Remove ${a(b)} from ${a(r.room_id)}" title="Remove from room">\xD7</button></form></span>`,"member"),N=c.filter(b=>!r.members.includes(b)),R=`<div class="members">${r.members.length?r.members.map(E).join(""):"<span>No members</span>"}${N.length?`<form class="row-actions" data-action="${n}/${encodeURIComponent(r.room_id)}/members"><select name="add" aria-label="Add a member to ${a(r.room_id)}" class="btn"><option value="">Add member\u2026</option>${N.map(b=>`<option value="${a(b)}">${a(b)}</option>`).join("")}</select><button class="btn" type="submit">Add</button>${f}</form>`:""}</div>`,M=w?w.files.length?`<div class="files-line">${g(w.files.length,"file")}: ${a(w.files.map(b=>b.path.split("/").pop()).join(", "))}${S?.cut.length?`; only the beginning of ${a(S.cut.join(", "))}`:""}</div>`:'<div class="files-line">No notes found at these paths.</div>':"",T=h?`<dl class="source"><dt>Repository</dt><dd class="mono">${a(h.repo)}</dd><dt>Branch</dt><dd class="mono">${a(h.ref)}</dd><dt>Notes</dt><dd><div class="files">${h.notes.map(b=>`<span class="file">${a(b)}</span>`).join("")}</div>${M}</dd>${S?.error?`<dt>Problem</dt><dd>${a(S.error)}</dd>`:""}</dl>`:'<div class="empty-source">This room reads no notes yet. Pick a repository and the folders or files the dashboard should read.</div>',O=e.repos.length?`<details class="toggle"><summary class="btn">${h?"Edit source":"Choose repository"}</summary>
387
+ <form class="edit" data-action="${n}/${encodeURIComponent(r.room_id)}/source" data-busy="Applying\u2026">
388
+ <div class="fields">
389
+ <label class="field">Repository<select name="repo" required>${i(h?.repo??null)}</select></label>
390
+ <label class="field">Branch<input name="ref" value="${a(h?.ref??"main")}" maxlength="200"></label>
391
+ <label class="field">Paths (folders or files)<input name="notes" required value="${a(h?.notes.join(", ")??"")}" placeholder="notes/, PLAN.md"></label>
392
+ </div>
393
+ <p class="hint">Separate paths with commas. A folder reads every Markdown file directly inside it. The dashboard reads them at one commit within a minute of applying.</p>
394
+ <div class="row-actions"><button class="btn primary" type="submit">Apply source</button></div>
395
+ ${f}
396
+ </form>${h?`<div class="edit">${L(`${n}/${encodeURIComponent(r.room_id)}/source/remove`,"Stop reading notes")}</div>`:""}</details>`:"",z=`<details class="toggle"><summary class="btn">Archive room</summary><div class="edit"><p><strong>Archive #${a(r.room_id)}?</strong> No new messages can be sent to it and its members are removed. Messages sent before now are still delivered, scheduled ones included, and the history stays readable. Its daily schedules stop. You can restore it later, empty.</p>${L(`${n}/${encodeURIComponent(r.room_id)}/archive`,`Archive #${r.room_id}`,{},"danger")}</div></details>`;return`<article class="card pad room"><header><h2><span class="hash">#</span>${a(r.room_id)}</h2>${y}</header>${R}${T}<div class="row-actions">${O}${z}</div></article>`}),m=e.archived.length?`<div class="card"><ul class="rows">${e.archived.map(r=>`<li><span class="main"><strong>#${a(r.room_id)}</strong> <span class="muted">archived ${a(_(r.archived_at))}</span></span>${L(`${n}/${encodeURIComponent(r.room_id)}/restore`,"Restore")}</li>`).join("")}</ul></div>`:"";return $(s,`Rooms and repos for ${s.lobby.name}`,`${x("Rooms and repos","Each room follows one repository, at one or more paths. The dashboard reads them for what each agent does now and next.")}<div class="stack">${d}${l}${u.join("")||`<div class="card">${j("mouse-empty","No rooms yet","Add one above. A room follows one repository, and its agents talk there.")}</div>`}${m?`<h2 class="subhead">Archived rooms</h2>${m}`:""}</div>`)}p(Ye,"renderRooms");const V={"claude-haiku-5-5":{text:"Recommended. Fast and enough for summaries.",price:"$0.10 in and $0.50 out per million tokens"},"claude-sonnet-5-5":{text:"Better judgment on messy notes.",price:"$2 in and $10 out per million tokens"}},Ce=p(s=>s>=1e9?`${(s/1e9).toFixed(1)} B`:s>=1e6?`${(s/1e6).toFixed(1)} M`:s>=1e3?`${Math.round(s/1e3)} K`:String(s),"tokens");function He(s,e){const o=s.lobby.id,n=`${s.lobby.api}/summaries`,c=ue.map(i=>`<label class="choice"><input type="radio" name="model" value="${i}"${e.config.summary_model===i?" checked":""}><strong>${P[i]}</strong><span>${V[i].text}</span><span class="price">${V[i].price}</span></label>`).join(""),d=me.map(i=>`<label><input type="radio" name="effort" value="${i}"${e.config.summary_effort===i?" checked":""}>${i[0].toUpperCase()}${i.slice(1)}</label>`).join(""),l=e.last?e.last.error?`<p class="warning">The last run, ${a(_(e.last.created_at))} with ${a(P[e.last.model]??e.last.model)}, failed: ${a(e.last.error)}.${e.good?` The dashboard shows the summary from ${a(_(e.good.created_at))}.`:""}</p>`:`<p class="hint">Last summary ${a(_(e.last.created_at))} by ${a(P[e.last.model]??e.last.model)}, ${I(e.last.cost_usd,s.lobby?.money??K,4)}.</p>`:'<p class="hint">No summary yet. One is written once the notes have been read.</p>';return $(s,`AI summaries for ${s.lobby.name}`,`${x("AI summaries","A small model reads the rooms\u2019 notes and the agents\u2019 reports and writes what each agent does now and next. It only runs when something changed, at most every 10 minutes.")}
397
+ <div class="stack"><div class="card pad">
398
+ <form class="stack" data-action="${n}" data-busy="Saving\u2026" data-auto data-stay>
399
+ <div class="group"><span class="label">Provider</span><div class="choices"><label class="choice"><input type="radio" checked disabled><strong>Anthropic</strong><span>Claude models</span></label><div class="choice off" aria-disabled="true"><strong>OpenAI</strong><span>Added once tested</span></div></div></div>
400
+ <div class="group"><span class="label">Key</span><div class="keyline">${e.keySet?'<span class="chip c-ok">Set on the server</span><span class="muted">ANTHROPIC_API_KEY is configured on the server.</span>':'<span class="chip c-crit">Not set</span><span class="muted">The server has no ANTHROPIC_API_KEY, so no new summaries are written.</span>'}<button class="btn" type="button" disabled title="For customer lobbies, later">Use a lobby key</button></div></div>
401
+ <div class="group"><span class="label">Model</span><div class="choices" role="radiogroup" aria-label="Model">${c}</div></div>
402
+ <div class="group"><span class="label">Reasoning</span><div class="seg" role="radiogroup" aria-label="Reasoning">${d}</div><p class="hint">Low is plenty for summarizing. Higher levels think longer and cost more per run.</p></div>
403
+ ${f}
404
+ </form>
405
+ </div>
406
+ <div class="card pad">
407
+ <div class="group"><span class="label">Last 24 hours</span><div class="usage">
408
+ <div class="stat"><div class="n">${e.usage.runs-e.usage.failed}</div><div class="k">summaries written${e.usage.failed?`, ${e.usage.failed} failed`:""}</div></div>
409
+ <div class="stat"><div class="n">${Ce(e.usage.input_tokens)}</div><div class="k">tokens read</div></div>
410
+ <div class="stat"><div class="n">${I(e.usage.cost_usd,s.lobby?.money??K,e.usage.cost_usd<.1?3:2)}</div><div class="k">estimated cost</div></div>
411
+ </div>${s.lobby?.money.date?`<p class="hint">${a(q(s.lobby.money).trim())}</p>`:""}</div>
412
+ ${l}
413
+ <form data-action="${n}/test" data-busy="Starting\u2026" data-stay><div class="row-actions"><button class="btn" type="submit"${e.keySet?"":" disabled"}>Run a test summary</button></div>${f}</form>
414
+ <p class="hint">A test runs now with the settings above, at most once every 5 minutes, and costs like any run. Reload to see its result.</p>
415
+ </div></div>`)}p(He,"renderSummaries");function Ge(s,e){const o=s.lobby,n=`${o.api}/access`,c=p((l,i,u)=>`<label class="choice"><input type="radio" name="visibility" value="${l}"${e.config.visibility===l?" checked":""}><strong>${i}</strong><span>${u}</span></label>`,"option"),d=e.viewers.length?`<ul class="rows inline-rows">${e.viewers.map(l=>`<li><span class="main">${a(l.email)} <span class="muted">${l.disabled?"disabled by the operator":l.sessions?`signed in on ${g(l.sessions,"device")}`:"not signed in"}</span></span>${L(`${n}/viewers/remove`,"Remove",{email:l.email})}</li>`).join("")}</ul>`:'<p class="hint">No viewers. Only you open this dashboard while it is private.</p>';return $(s,`Dashboard access for ${o.name}`,`${x("Dashboard access","Who can open this lobby\u2019s dashboard.")}
416
+ <div class="stack"><div class="card pad">
417
+ <div class="group"><span class="label">Address</span><div class="addr"><code>${a(o.address)}</code><button class="btn" type="button" data-copy="${a(o.link)}">Copy</button><a class="btn" href="${a(o.url)}">Open</a></div></div>
418
+ <form class="group" data-action="${n}/visibility" data-busy="Saving\u2026" data-auto>
419
+ <span class="label">Visibility</span>
420
+ <div class="choices">${e.owner===null?`${c("private","Private","You, signed in on this computer with pocolobby settings.")}${c("public","Public","Anyone who can reach this address, without signing in.")}`:`${c("private","Private","You and the viewers below, after signing in with an email code.")}${c("public","Public","Anyone with the address, without signing in.")}`}</div>
421
+ <p class="warning">A public dashboard shows anyone who has the address your agents\u2019 names, what each does now and next, what they wait on from you, when they sent and received messages, how much of their Claude plans is left (without the accounts\u2019 addresses), and your notes\u2019 repositories and paths.</p>
422
+ ${f}
423
+ </form>
424
+ </div>
425
+ <div class="card pad">
426
+ <div class="group"><span class="label">Icon</span><p class="hint">What browsers show for this dashboard in tabs and bookmarks, and on a phone\u2019s home screen.</p>
427
+ <div class="icon-now"><img src="${e.icon?`${a(o.settings)}/icon.png?v=${G(e.icon.sha256)}`:"/apple-touch-icon.png"}" alt="" width="56" height="56"><span>${e.icon?`This dashboard\u2019s own icon, ${Math.ceil(e.icon.png.length/1024)} KB.`:"PocoLobby\u2019s icon, until you upload your own."}</span>${e.icon?L(`${n}/icon/remove`,"Use PocoLobby\u2019s icon"):""}</div></div>
428
+ <form class="row-actions" data-action="${n}/icon" data-busy="Uploading\u2026"><label class="field grow">A square PNG, 32 to 1024 pixels wide, at most 64 KB<input name="png" type="file" accept="image/png" required></label><button class="btn end" type="submit">Upload icon</button>${f}</form>
429
+ </div>
430
+ ${e.owner===null?"":`<div class="card pad">
431
+ <div class="group"><span class="label">Viewers</span><p class="hint">People who may open the private dashboard. Each signs in with a code sent to their address. You, ${a(e.owner)}, always can. Removing someone closes the dashboard to them at once.</p>${d}</div>
432
+ <form class="row-actions" data-action="${n}/viewers" data-busy="Adding\u2026"><label class="field grow">Add a viewer<input name="email" type="email" required maxlength="254" placeholder="name@example.com"></label><button class="btn end" type="submit">Add viewer</button>${f}</form>
433
+ <p class="hint">${g(e.sessions,"dashboard session")} open now.</p>
434
+ </div>`}</div>`)}p(Ge,"renderBoardAccess");function Ke(s,e){const o=s.lobby.id,n=e.agents.filter(i=>i.enabled),c=new Map(n.map(i=>[i.agent_id,e.curates(i.agent_id)])),d=p(i=>i.last_seen_at===null&&c.get(i.agent_id).length>0,"headless"),l=n.sort((i,u)=>Number(u.manage_rooms)-Number(i.manage_rooms)||Number(d(i))-Number(d(u))||+(e.config.agents[i.agent_id]?.support===!0)-+(e.config.agents[u.agent_id]?.support===!0)||i.agent_id.localeCompare(u.agent_id)).map(i=>{const u=e.config.agents[i.agent_id],m=e.rooms.filter(w=>w.members.includes(i.agent_id)).map(w=>w.room_id),r=c.get(i.agent_id),h=i.paused?`<span class="chip c-off">${i.resume_at?`Resting until ${a(_(i.resume_at))}`:"Resting"}</span>`:e.connected(i.agent_id)?'<span class="chip c-ok">Bridge connected</span>':d(i)?'<span class="chip c-ok">Runs headless</span>':i.runtime_id===null?'<span class="chip c-warn">Setup pending</span>':'<span class="chip c-off">Bridge offline</span>',S=i.last_message_at?`<div class="files-line">last message ${a(_(i.last_message_at))}</div>`:"";return`<li><span class="main"><span class="avatar" aria-hidden="true">${a(u?.emoji??i.agent_id[0].toUpperCase())}</span><strong>${a(u?.name??i.agent_id)}</strong> <span class="muted">${i.manage_rooms?"coordinator, ":""}${a(m.join(", ")||"no rooms")}${r.length?`, curates ${a(r.join(", "))}`:""}${u?.hidden?", not on the dashboard":u?.support?", support":""}</span>${S}</span>${h}</li>`});return $(s,`Agents in ${s.lobby.name}`,`${x("Agents","Admitting agents and their credentials stays with the server operator\u2019s CLI. This is the read-only view; a connected bridge does not mean the agent is free.",'<button class="btn" disabled>Admit agent</button>')}<div class="card">${l.length?`<ul class="rows">${l.join("")}</ul>`:j("mouse-envelope","No agents yet",`The server operator admits each agent with <span class="mono">pocolobby agent add NAME --lobby ${a(o)}</span>.`)}</div>`)}p(Ke,"renderAgents");const C=p(s=>Array.isArray(s)&&s.length?s.map(String).join(", "):"","list");function Te(s){const e=s.target??"",o=s.detail??{},n=`#${e}`;switch(s.action){case"create_lobby":return"created the lobby";case"create_room":return`created room ${n}`;case"set_room_members":return`set the members of ${n} to ${C(o.members)||"nobody"}`;case"change_room_members":{const c=[];return C(o.add)&&c.push(`added ${C(o.add)} to ${n}`),C(o.remove)&&c.push(`removed ${C(o.remove)} from ${n}`),c.join(" and ")||`changed the members of ${n}`}case"delete_room":return`deleted room ${n}`;case"archive_room":return`archived room ${n}`;case"restore_room":return`restored room ${n}`;case"board_source_set":return`set ${n} to read ${String(o.repo??"a repository")}${o.ref?` at ${String(o.ref)}`:""}${C(o.notes)?`: ${C(o.notes)}`:""}`;case"board_source_remove":return`stopped reading notes for ${n}`;case"board_agents":return"changed how agents look on the dashboard";case"summary_settings":return`set summaries to ${P[o.model]??String(o.model)}, ${String(o.effort)} reasoning`;case"board_visibility":return`made the dashboard ${e}`;case"board_icon":return"gave the dashboard its own icon";case"display_name":return e?`named the lobby \u201C${e}\u201D`:"went back to showing the lobby\u2019s name";case"currency":return`showed costs in ${e}`;case"board_icon_remove":return"gave the dashboard PocoLobby\u2019s icon again";case"viewer_add":return`added ${e} as a dashboard viewer`;case"viewer_remove":return`removed ${e} as a dashboard viewer`;case"grant_lobby":return`made ${e} the lobby owner`;case"ungrant_lobby":return`ended ${e}\u2019s ownership`;case"repo_approve":return`approved ${e} for notes`;case"repo_remove":return`withdrew the approval of ${e}`;case"provision":return`admitted agent ${e}`;case"update_agent":return`changed agent ${e}${C(o)?` (${C(o)})`:""}`;case"rotate_token":return`rotated ${e}\u2019s credential`;case"reset_runtime":return`reset ${e}\u2019s runtime`;case"retry_notifications":return`retried notifications for ${e}`;case"rest_agent":return o.resume_at?`rested ${e} until ${_(String(o.resume_at))}`:`rested ${e}`;case"resume_agent":return o.timer?`resumed ${e} at its set time`:`resumed ${e}`;case"resume_runtime":return"resumed its session";case"tokens_clear":return`deleted the token totals before ${e}`;case"set_memory_curator":return o.curator?`made ${String(o.curator)} the curator of ${n}\u2019s memory`:`ended ${n}\u2019s shared memory curation`;case"erase_memory_entry":return`erased memory entry ${String(o.entry)} from ${n}`;case"erase_memory_fragment":return`erased a memory fragment from ${n}`;default:return`${s.action.replaceAll("_"," ")}${e?` ${e}`:""}`}}p(Te,"describe");function Ve(s,e){const o=s.lobby.id,n=e.events.map(c=>{const d=!!c.detail?.timer,l=d?"PocoLobby":c.actor_kind==="operator"?"The operator":c.actor??"someone",i=d?"Timer":c.actor_kind==="agent"?"API":c.actor_kind==="account"?"Settings":"CLI";return`<li><span class="main"><strong>${a(l)}</strong> ${a(Te(c))} <span class="muted">${a(_(c.at))}</span></span><span class="chip c-off">${i}</span></li>`});return $(s,`Activity in ${s.lobby.name}`,`${x("Activity","Every change to the lobby, its rooms and its settings, and who made it: you in Settings, agents through the API, and the operator through the CLI.")}<div class="card">${n.length?`<ul class="rows">${n.join("")}</ul>`:j("luke-sleep","Nothing has happened yet","Changes to the lobby, its rooms and its settings show up here, with who made them.")}</div>`)}p(Ve,"renderActivity");const je=["January","February","March","April","May","June","July","August","September","October","November","December"],D=p(s=>s.toLocaleString("en-US"),"exact");function Ne(s,e){return s==="week"?`Week ${Number(e.slice(6))}, ${e.slice(0,4)}`:s==="month"?`${je[Number(e.slice(5))-1]} ${e.slice(0,4)}`:A(`${e}T00:00:00Z`)}p(Ne,"periodLabel");function Je(s,e){const o=s.lobby,n=p(u=>I(u,o.money),"dollars"),c=[["day","Days"],["week","Weeks"],["month","Months"]].map(([u,m])=>`<a href="${a(o.settings)}/tokens?by=${u}"${u===e.by?' aria-current="page"':""}>${m}</a>`).join(""),d=e.periods.reduce((u,m)=>u+m.cost,0),l=e.periods.some(u=>u.unpriced),i=e.periods.map(u=>`<li><span class="period-head"><strong>${a(Ne(e.by,u.period))}</strong><b>${u.unpriced?"at least ":""}${n(u.cost)}</b></span>${u.models.map(m=>`<span class="model"><b>${a(m.model)}</b> ${m.cost===null?"no list price":n(m.cost)}: ${D(m.input)} in, ${D(m.cache_write)} cache write, ${D(m.cache_read)} cache read, ${D(m.output)} out, ${g(m.calls,"call")}</span>`).join("")}</li>`);return $(s,`Tokens in ${o.name}`,`${x("Tokens","What the agents' model calls would cost at Anthropic's API list prices, from their Claude Code transcripts: not what the Claude subscriptions cost. Days are UTC days.",`<div class="seg" role="group" aria-label="Group by">${c}</div>`)}
435
+ <div class="stack"><div class="card">${i.length?`<ul class="periods">${i.join("")}</ul>`:j("mouse-empty","No token counts yet","An agent's status line sends them each minute it runs.")}</div>
436
+ ${i.length?`<p class="hint">${l?"At least ":""}${n(d)} in all, at the list prices of ${a(e.prices_as_of)}.${l?" A model without a list price counts in tokens only.":""}${a(q(o.money))}</p>`:""}
437
+ <form class="card pad group" data-action="${a(o.api)}/tokens/clear" data-busy="Deleting\u2026">
438
+ <span class="label">Delete old totals</span>
439
+ <p class="hint">Deletes the totals of the days before a date. Only days before ${a(A(`${e.clearable}T00:00:00Z`))} can go: the last four weeks can still take late reports. This cannot be undone.</p>
440
+ <div class="row-actions apart"><label class="field date">Delete the days before<input type="date" name="before" required max="${a(e.clearable)}"></label><button class="btn danger end" type="submit">Delete</button></div>
441
+ ${f}
442
+ </form></div>`)}p(Je,"renderTokens");const Le={gotcha:"Gotchas",procedure:"Procedures",fix:"Fixes"},J=8,Ee=p(s=>s.length<2?s.join(""):`${s.slice(0,-1).join(", ")} and ${s.at(-1)}`,"names");function Ze(s,e){const o=s.lobby,n=p(m=>I(m,o.money),"dollars"),c=p(m=>`${o.api}/${m}`,"api"),d=p((m,r)=>L(c("memory/erase"),"Erase",{kind:m,key:r},"danger"),"erase"),l=p((m,r,h)=>`<details class="more"><summary>${a(m)}<span class="count">${a(r)}</span></summary>${h}</details>`,"more"),i=p(m=>`<div class="card"><ul class="memory">${m.join("")}</ul></div>`,"items"),u=e.rooms.map(m=>{const r=e.memory.find(t=>t.room_id===m.room_id),h=r?.curator??null,S=[["none","No shared memory"],...m.members.map(t=>[t,t])].map(([t,k])=>`<option value="${a(t)}"${(h??"none")===t?" selected":""}>${a(k)}</option>`).join(""),w=`<form class="row-actions" data-action="${a(c("memory/curator"))}" data-busy="Saving\u2026" data-auto><input type="hidden" name="room_id" value="${a(m.room_id)}"><label class="field">Curator<select name="agent_id">${S}</select></label>${f}</form>`;if(!r)return`<section class="card pad mem"><div class="mem-head"><h2>Room ${a(m.room_id)}</h2></div><p class="hint">No shared memory. Name a curator, a member who curates it with the code at hand, to start one.</p>${w}</section>`;const y=r.entries.filter(t=>t.state==="active"),E=r.entries.filter(t=>t.state==="archived"),N=r.fragments.filter(t=>t.state==="pending"),R=r.fragments.filter(t=>t.state==="refused"),M=[h?`<span class="chip c-ok">Curated by ${a(h)}</span>`:'<span class="chip c-off">No curator</span>',...r.failures?[`<span class="chip c-warn">${g(r.failures,"failed run")} in a row</span>`]:[]],T=r.curations[0],O=[g(y.length,"entry","entries"),`${N.length} waiting`,T?`last run ${_(T.at)}${T.error?", failed":`: ${T.added} added, ${T.updated} updated, ${T.refused} refused`}`:"no runs yet"],z=y.filter(t=>!t.checked_at).length,b=y.map(t=>t.checked_at).filter(t=>t!==null).sort()[0],Z=z?`${g(z,"entry","entries")} not checked against the code yet.`:b?`Every entry checked against the code since ${_(b)}.`:"",X=r.loaded<y.length?`Every session start loads ${r.loaded} of the ${y.length} rule lines; members find the rest with recall.`:"Every session start loads all its rule lines.",B=["gotcha","procedure","fix"].map(t=>[t,y.filter(k=>k.kind===t).length]).filter(([,t])=>t>0),U=y.filter(t=>t.disputed).length,Q=y.length>1?`<div class="mem-tools"><input type="search" data-memory-search placeholder="Search ${g(y.length,"entry","entries")}" aria-label="Search the memory of ${a(m.room_id)}">${B.length>1||U?`<div class="seg" role="group" aria-label="Kinds"><button type="button" data-kind-filter="" aria-pressed="true">All ${y.length}</button>${B.map(([t,k])=>`<button type="button" data-kind-filter="${t}" aria-pressed="false">${Le[t]} ${k}</button>`).join("")}${U?`<button type="button" data-kind-filter="disputed" aria-pressed="false">Disputed ${U}</button>`:""}</div>`:""}</div>`:"",ee=[...y].sort(le).map(t=>{const k=[...new Set(t.provenance.map(v=>v.author))],Y=t.provenance.filter(v=>v.evidence),re=[`${a(t.ref)}, added ${a(A(t.created_at))}`,k.length?`Taught by ${a(Ee(k))}${t.curator_only?", its curator alone":""}`:"Taught by nobody on record",t.checked_commit?`checked at ${a(t.checked_commit.slice(0,8))} on ${a(A(t.checked_at))}`:"not checked yet"];return`<li data-kind="${t.kind}"${t.disputed?" data-disputed":""} data-keywords="${a(t.keywords)}"><details><summary class="entry-line">${fe(t,v=>a(Ae(v)),k.map(v=>e.people[v]??{name:v,emoji:null}),t.disputed?'<span class="flag">Disputed</span>':"")}</summary><div class="mem-body"><p class="entry-full">${a(t.title)}</p><p>${a(t.text)}</p>${t.disputed?`<p class="disputed">Disputed: ${a(t.disputed)}</p>`:""}<p class="meta">${re.join(" \xB7 ")}</p>${Y.length?`<details class="sub"><summary>Evidence</summary><ul>${Y.map(v=>`<li><b>${a(v.author)}:</b> ${a(v.evidence)}</li>`).join("")}</ul></details>`:""}${t.versions.length?`<details class="sub"><summary>${g(t.changes,"earlier version")}</summary>${t.versions.map(v=>`<p class="meta">${a(A(v.changed_at))}: <b>${a(v.title)}</b></p><p>${a(v.text)}</p>`).join("")}</details>`:""}<div class="actions">${d("entry",t.ref)}</div></div></details></li>`}),ae=y.length?`<span class="mem-count" data-count>All ${y.length}, newest first</span><ul class="mem-list" data-latest="${J}">${ee.join("")}</ul>${y.length>J?`<button class="btn" type="button" data-show-all hidden>Show all ${y.length}</button>`:""}`:'<p class="hint">No entries yet. Members teach the room with remember, and the curator turns what holds into entries.</p>',se=N.map(t=>`<li><span class="entry-head"><strong>${a(t.author)}, ${a(t.kind)}, ${a(A(t.created_at))}</strong>${d("fragment",t.id)}</span><p>${a(t.text)}</p>${t.evidence?`<span class="meta">Evidence: ${a(t.evidence)}</span>`:""}</li>`),te=R.map(t=>`<li><span class="entry-head"><strong>${a(t.author)}, ${a(A(t.created_at))}</strong>${d("fragment",t.id)}</span><p>${t.text?a(t.text):'<span class="muted">Erased by the curator.</span>'}</p><span class="meta">Refused: ${a(t.note??"")}</span></li>`),oe=E.map(t=>`<li><span class="entry-head"><strong>${a(t.ref)}: ${a(t.title)}</strong>${d("entry",t.ref)}</span><span class="meta">${a(t.reason??"")}${t.into?` Merged into ${a(t.into)}.`:""} ${a(A(t.updated_at))}.</span></li>`),F=r.curations.map(t=>`<li><span class="entry-head"><strong>${a(_(t.at))}, ${a(t.curator)}</strong><b>${typeof t.usage?.cost_usd=="number"?n(t.usage.cost_usd):""}</b></span>${t.error?`<p class="disputed">Failed: ${a(t.error)}</p>`:`<span class="meta">${t.added} added, ${t.updated} updated, ${t.verified} verified, ${t.archived} archived, ${t.refused} refused${t.commit?` at ${a(t.commit.slice(0,8))}`:""}</span>${t.summary?`<p>${a(t.summary)}</p>`:""}`}</li>`),W=r.curations.reduce((t,k)=>t+(typeof k.usage?.cost_usd=="number"?k.usage.cost_usd:0),0),ne=[l(h?`Waiting for ${h}`:"Waiting for a curator",String(N.length),`${r.authors.length?`<p class="hint">Fragments this week: ${a(r.authors.map(t=>`${t.author} ${t.fragments}`).join(", "))}.</p>`:""}${N.length?i(se):'<p class="hint">Nothing waiting.</p>'}`),...R.length?[l("Refused in the last 30 days",String(R.length),i(te))]:[],...E.length?[l("Archived",String(E.length),i(oe))]:[],l("Curation runs",`${r.curations.length}${W?`, ${n(W)}`:""}`,`<p class="hint">Costs are at API list prices${r.curations.length>=20?", over the latest 20 runs":""}.${a(q(o.money))}</p>${F.length?i(F):""}`),l("Curator",h??"none",w)];return`<section class="card pad mem"><div class="mem-head"><h2>Room ${a(m.room_id)}</h2>${M.join("")}</div><p class="hint">${a(O.join(" \xB7 "))}</p><p class="hint">${a([Z,X].filter(Boolean).join(" "))}</p>${Q}${ae}<div class="mem-more">${ne.join("")}</div></section>`});return $(s,`Memory in ${o.name}`,`${x("Memory","What each room's agents taught each other, curated against the code by the room's curator. Each agent's own memory is Claude Code's auto memory and is not shown here.")}
443
+ <div class="stack">${u.length?u.join(`
444
+ `):`<div class="card">${j("mouse-empty","No rooms yet","Shared memory belongs to a room and its members. Create a room first.")}</div>`}</div>`)}p(Ze,"renderMemory");function Re(s){const e=new Intl.DisplayNames(["en"],{type:"currency"}),o=["USD",...Object.keys(s.table?.rates??{}).filter(l=>l!=="USD").sort()];o.includes(s.chosen)||o.push(s.chosen);const n=o.map(l=>`<option value="${a(l)}"${l===s.chosen?" selected":""}>${a(l)}, ${a(e.of(l)??l)}</option>`).join(""),c=s.chosen==="USD"||!!s.table?.rates?.[s.chosen],d=!s.fetching&&!s.table?.rates?"This server fetches no exchange rates (POCOLOBBY_EXCHANGE_RATES is not set), so costs show in US dollars.":c?s.table?.date?`The European Central Bank's reference rates of ${s.table.date}, fetched once a day; on weekends and holidays the last working day's.`:"No exchange rates fetched yet; costs show in US dollars until they are.":`No rate for ${s.chosen} is stored now, so costs show in US dollars until there is one.`;return`<span class="label">Currency</span>
445
+ <p class="hint">Costs on the dashboard and in Settings, kept in US dollars at Anthropic's list prices, shown in this currency. ${a(d)} The summaries' daily limit stays in US dollars.</p>
446
+ <div class="row-actions"><label class="field">Show costs in<select name="currency">${n}</select></label></div>`}p(Re,"currencyChoice");function Xe(s,e){const o=s.lobby;return $(s,`General settings for ${o.name}`,`${x("General","The lobby itself. Only its owner can change these.")}
447
+ <div class="card pad">
448
+ <div class="group"><span class="label">Name and address</span><div class="addr"><code>${a(o.id)}</code><span class="muted">${a(o.address)}, created ${a(A(e.created))}</span></div><p class="hint">A lobby\u2019s name cannot change: agents\u2019 credentials and the dashboard address depend on it.</p></div>
449
+ <form class="group" data-action="${a(o.api)}/display-name" data-busy="Saving\u2026" data-auto>
450
+ <span class="label">Display name</span>
451
+ <p class="hint">How the dashboard and Settings show the lobby, with capitals and spaces if you like, such as My Team for my-team. Leave it empty to show its name.</p>
452
+ <div class="row-actions"><label class="field grow">Display name<input name="display_name" maxlength="40" autocomplete="off" value="${a(e.display_name??"")}" placeholder="${a(o.id)}"></label></div>
453
+ ${f}
454
+ </form>
455
+ <form class="group" data-action="${a(o.api)}/currency" data-busy="Saving\u2026" data-auto data-stay>
456
+ ${Re(e.currency)}
457
+ ${f}
458
+ </form>
459
+ <div class="group"><span class="label">Theme on this browser</span><p class="hint">How the dashboard and Settings look here. System follows this device\u2019s light or dark mode.</p>${$e}</div>
460
+ <form class="group" data-action="${a(o.api)}/delete" data-busy="Deleting\u2026">
461
+ <span class="label">Delete lobby</span>
462
+ <p>Deleting removes every agent credential, room, message, attachment and summary in this lobby, and its dashboard. ${e.agents?`Its ${g(e.agents,"agent")} are disconnected at once. `:""}The name is never used again. This cannot be undone.</p>
463
+ <label class="field narrow">Type the lobby name to confirm<input name="confirm" required autocomplete="off" placeholder="${a(o.id)}"></label>
464
+ <div class="row-actions"><button class="btn danger" type="submit">Delete ${a(o.id)}</button></div>
465
+ ${f}
466
+ </form>
467
+ </div>`)}p(Xe,"renderSettings");export{Te as describe,Ve as renderActivity,Ke as renderAgents,Ge as renderBoardAccess,Fe as renderHome,Ze as renderMemory,qe as renderNotice,We as renderOverview,Ye as renderRooms,Xe as renderSettings,Be as renderSignIn,He as renderSummaries,Je as renderTokens};
@@ -0,0 +1 @@
1
+ var Se=Object.defineProperty;var d=(b,g)=>Se(b,"name",{value:g,configurable:!0});import{randomBytes as oe}from"node:crypto";import{setTimeout as $e}from"node:timers/promises";import{z as m,ZodError as Te}from"zod";import{Challenge as ie,Code as Ce,CONSOLE_COOKIE as R,cookieToken as B,LOCAL_COOKIE as V,handoffKey as Oe,newCode as xe,newSessionToken as Y,sessionCookie as J,SIGN_IN as re,tokenHash as T,WindowLimiter as se}from"./auth.js";import{displayName as Ie,isoWeek as Ne,tokenPeriods as Ae}from"./board.js";import{BoardIconSchema as Le,BoardSourceSchema as Ee,BoardVisibility as Re,SummarySettingsSchema as je}from"./board-model.js";import{boardCsp as Me}from"./board-page.js";import{renderActivity as Ue,renderAgents as Be,renderBoardAccess as Pe,renderHome as ae,renderNotice as P,renderOverview as De,renderRooms as We,renderSettings as He,renderMemory as Ve,renderTokens as Ye,renderSignIn as ce,renderSummaries as Ge}from"./console-page.js";import{MailError as Ke}from"./mail.js";import{AgentId as le,Email as F,LobbyError as O,RoomId as X}from"./protocol.js";import{PRICES_AS_OF as ze}from"./prices.js";import{TOKEN_RETENTION_MS as Je}from"./store.js";const Fe=4096,Xe=512*1024,D=m.string().regex(/^[a-z0-9][a-z0-9_-]{0,62}$/u),Ze=6e4,me=re.sessionMs,Qe=d(b=>m.string().max(2e3).transform(g=>g.split(",").map(i=>i.trim()).filter(Boolean)).pipe(m.array(m.string()).max(b)),"list"),h={code:m.object({email:F}).strict(),verify:m.object({email:F,code:Ce,lobby:D.optional(),to:m.string().max(64).optional(),state:m.string().regex(ie).optional()}).strict(),ticket:m.object({ticket:m.string().regex(/^[A-Za-z0-9_-]{43}$/u)}).strict(),empty:m.object({}).strict(),lobby:m.object({lobby_id:m.string().max(64),first_room:X.optional()}).strict(),room:m.object({room_id:X}).strict(),members:m.object({add:le.optional(),remove:le.optional()}).strict().refine(b=>b.add!==void 0||b.remove!==void 0,"Name a member"),source:m.object({repo:m.string().max(201),ref:m.string().max(200).optional(),notes:Qe(20)}).strict(),summary:je,visibility:m.object({visibility:Re}).strict(),viewer:m.object({email:F}).strict(),delete:m.object({confirm:m.string().max(64)}).strict(),icon:Le,displayName:m.object({display_name:m.string().max(200).optional()}).strict(),currency:m.object({currency:m.string().max(8)}).strict(),clearTokens:m.object({before:m.string().max(10)}).strict(),memoryErase:m.object({kind:m.enum(["entry","fragment"]),key:m.string().min(1).max(64)}).strict(),memoryCurator:m.object({room_id:m.string().min(1).max(48),agent_id:m.string().min(1).max(48)}).strict()};class de{static{d(this,"Reply")}status;body;headers;constructor(g,i,x={}){this.status=g,this.body=i,this.headers=x}}const p=d((b={},g={})=>new de(200,{ok:!0,...b},g),"ok"),v=d((b,g,i,x={})=>new de(b,{error:{code:g,message:i}},x),"refuse");function fn(b,g){const{store:i,board:x,keys:W}=g,w=b.origin===null?null:new URL(b.origin),ge=b.responseFloorMs??1e3,be=new se(10,15*6e4),ye=new se(30,15*6e4),j=new Map,M=new Map,G=d((e,n="/")=>`${w.protocol}//${e}.${g.boardDomain}${w.port?`:${w.port}`:""}${n}`,"dashboardUrl"),K=d((e,n)=>w?`${e}.${g.boardDomain}`:`${n}/${e}`,"address"),I=d((e,n="/")=>G(e,`/sign-in${n==="/"?"":`?to=${encodeURIComponent(n)}`}`),"openUrl"),Z=d((e,n,l)=>`${w.origin}/open/${encodeURIComponent(e)}?${n==="/"?"":`to=${encodeURIComponent(n)}&`}state=${l}`,"handoffUrl");function y(e,n,l,o,f,c,t={}){n.writeHead(l,{"content-type":o,"cache-control":"no-store","x-content-type-options":"nosniff","referrer-policy":"same-origin","content-security-policy":o.startsWith("text/html")?Me(c):"default-src 'none'; frame-ancestors 'none'",...t}),n.end(e.method==="HEAD"?void 0:f)}d(y,"send");function Q(e,n){const l=B(e.headers.cookie,R);if(!l)return null;const o=i.consoleSession(T(l));return o?.refresh&&(i.refreshConsoleSession(T(l)),n["set-cookie"]=J(R,l)),o}d(Q,"session");function H(e){if(w)return!1;const n=B(e.headers.cookie,V),l=n===null?void 0:M.get(T(n));return l!==void 0&&l>g.clock()}d(H,"operator");function q(e){const n=B(e.headers.cookie,V);return n&&M.delete(T(n)),{"set-cookie":ue(null)}}d(q,"endOperator");async function C(e,n,l,o,f=Fe){if(e.headers.origin!==l)throw e.resume(),new O(403,"origin_not_allowed","Use the settings page itself.");if(e.headers["content-type"]?.split(";",1)[0]?.trim()!=="application/json")throw e.resume(),new O(415,"content_type","Use application/json.");if(Number(e.headers["content-length"])>f)throw e.resume(),new O(413,"body_too_large","The request is too large.");const c=[];let t=0;for await(const r of e)t+=r.length,t<=f&&c.push(r);if(t>f)throw new O(413,"body_too_large","The request is too large.");if(o&&!o())throw new O(401,"sign_in_required","Your session has ended. Sign in again.");let a;try{a=JSON.parse(Buffer.concat(c).toString("utf8"))}catch{throw new O(400,"invalid_json","The request is not valid JSON.")}return n.parse(a)}d(C,"input");async function he(e){const{email:n}=await C(e,h.code,w.origin),l=Date.now()+ge;if(!b.mailer)return v(503,"sign_in_unavailable","Sign-in is not configured on this server.");const o=be.take(g.clientAddress(e));if(o)return v(429,"too_many_requests",`Too many codes asked from here. Try again in ${Math.ceil(o/60)} min.`,{"retry-after":String(o)});const f=W.emailKey(n),c=xe(),t=i.beginSignIn(n,f,W.codeMac(f,c));if(t.wait)return v(429,"too_many_requests",`Wait ${t.wait<120?`${t.wait} s`:`${Math.ceil(t.wait/60)} min`} before asking for another code.`,{"retry-after":String(t.wait)});let a=null;if(t.send)try{await b.mailer.sendSignInCode(n,c)}catch(s){a=s instanceof Ke?s.message:"The email could not be sent"}const r=l-Date.now();return r>0&&await $e(r),a?v(502,"mail_failed",`We could not send the code: ${a}. Try again in a minute.`):p({message:`If ${n} may sign in, a code is on its way. It works for 10 minutes.`})}d(he,"signInCode");async function pe(e){const{email:n,code:l,lobby:o,to:f,state:c}=await C(e,h.verify,w.origin),t=ye.take(g.clientAddress(e));if(t)return v(429,"too_many_requests",`Too many tries from here. Try again in ${Math.ceil(t/60)} min.`,{"retry-after":String(t)});const a=W.emailKey(n),r=Y(),s=i.finishSignIn(n,a,W.codeMac(a,l),T(r));if("error"in s)return s.error==="budget"?v(429,"too_many_wrong_codes",`Too many wrong codes for this address. Wait ${Math.ceil(s.wait/60)} min, then ask for a new code.`,{"retry-after":String(s.wait)}):v(400,"code_invalid","That code is not right, has expired, or had 5 wrong tries. Check the email, or ask for a new code.");const _=fe(f??null);return p({redirect:o?c?Z(o,_,c):I(o,_):"/"},{"set-cookie":J(R,r)})}d(pe,"signInVerify");async function we(e,n,l,o,f){const{lobby:c}=l,t=d((r,s)=>C(e,r,l.origin,f,s),"body"),a=/^rooms\/([^/]+)\/(members|source|source\/remove|archive|restore)$/u.exec(o);if(o==="rooms"){const{room_id:r}=await t(h.room);return i.createRoom(n,r,c),p()}if(a){const r=X.parse(a[1]);if(a[2]==="members"){const s=await t(h.members);return i.changeRoomMembers(n,r,{...s.add?{add:[s.add]}:{},...s.remove?{remove:[s.remove]}:{}},c),p()}if(a[2]==="source"){const s=await t(h.source);return i.setBoardSource(c,r,Ee.parse(s),n),g.refreshBoards(c),p()}return await t(h.empty),a[2]==="source/remove"?i.removeBoardSource(c,r,n):a[2]==="archive"?i.archiveRoom(n,r,c):i.restoreRoom(n,r,c),p()}if(o==="summaries")return i.setSummarySettings(n,c,await t(h.summary)),p({message:"Saved. The next summary uses these settings."});if(o==="summaries/test")return await t(h.empty),x.runNow(c).catch(()=>{}),p({message:"Writing a test summary now. Reload in a minute to see it."});if(o==="access/visibility"){const{visibility:r}=await t(h.visibility);return i.setBoardVisibility(n,c,r),p()}if(o==="display-name"){const{display_name:r}=await t(h.displayName);return i.setDisplayName(n,c,r??""),p()}if(o==="currency"){const{currency:r}=await t(h.currency);return i.setLobbyCurrency(n,c,r),p({message:`Saved: costs show in ${r} from the next page.`})}if(o==="access/icon"){const{png:r}=await t(h.icon,Xe);return i.setBoardIcon(n,c,Buffer.from(r,"base64")),p()}if(o==="access/icon/remove")return await t(h.empty),i.setBoardIcon(n,c,null),p();if(o==="access/viewers")return i.addBoardViewer(n,c,(await t(h.viewer)).email),p();if(o==="access/viewers/remove")return i.removeBoardViewer(n,c,(await t(h.viewer)).email),p();if(o==="memory/erase"){const{kind:r,key:s}=await t(h.memoryErase);return i.eraseMemory(c,r,s,n),p()}if(o==="memory/curator"){const{room_id:r,agent_id:s}=await t(h.memoryCurator);return i.setMemoryCurator(c,r,s==="none"?null:s,n),p()}if(o==="tokens/clear"){const{before:r}=await t(h.clearTokens),{deleted:s}=i.clearTokens(n,c,r);return p({message:`Deleted ${s} daily total${s===1?"":"s"}.`})}if(o==="delete"){const{confirm:r}=await t(h.delete),s=i.deleteLobby(n,c,r);return g.closeAgents(s.agents),p({redirect:w?`${w.origin}/`:"/"})}return e.resume(),v(404,"not_found","Unknown settings route.")}d(we,"lobbyAction");function _e(e,n,l,o,f,c){const{lobby:t}=n,a={nonce:o,email:e.kind==="account"?e.email:null,home:w?`${w.origin}/`:"/",signOut:`${n.base}/settings/api/sign-out`,section:l,lobby:{id:t,name:i.displayName(t)??t,address:K(t,f),url:n.base||"/",link:`${n.origin}${n.base||"/"}`,icon:i.boardIcon(t)?.sha256??null,settings:`${n.base}/settings`,api:`${n.base}/settings/api`,money:i.lobbyMoney(t)}},r=i.boardConfig(t);switch(l){case"overview":return De(a,{view:x.view(t),config:r,rooms:i.rooms(t),agents:i.agents(t),viewers:i.boardViewers(t).length,sessions:i.boardSessionCount(t),last:i.boardLastAttempt(t)});case"rooms":return We(a,{rooms:i.rooms(t),archived:i.archivedRooms(t),config:r,view:x.view(t),snapshots:i.boardNotes(t),repos:i.lobbyRepos(t),agents:i.agents(t),github:g.github});case"summaries":return Ge(a,{config:r,keySet:g.anthropic,usage:i.boardUsage(t,g.clock()-864e5),last:i.boardLastAttempt(t),good:i.boardSummary(t)});case"access":return Pe(a,{config:r,icon:i.boardIcon(t),viewers:i.boardViewers(t),sessions:i.boardSessionCount(t),owner:e.kind==="account"?e.email:null});case"agents":return Be(a,{agents:i.agents(t),rooms:i.rooms(t),config:r,connected:d(s=>g.isConnected(t,s),"connected"),curates:d(s=>i.curationStatus(t,s).map(_=>_.room_id),"curates")});case"tokens":{const s=c.get("by")==="week"?"week":c.get("by")==="month"?"month":"day",_=s==="week"?Ne:s==="month"?k=>k.slice(0,7):void 0;return Ye(a,{by:s,periods:Ae(i.tokenDays(t),_??(k=>k)).reverse(),prices_as_of:ze,clearable:new Date(g.clock()-Je).toISOString().slice(0,10)})}case"memory":{const s=r.agents;return Ve(a,{rooms:i.rooms(t),memory:i.memoryOverview(t),people:Object.fromEntries(i.agents(t).map(_=>{const k=s[_.agent_id];return[_.agent_id,{name:k?.name??Ie(_.agent_id),emoji:k?.emoji??null}]}))})}case"activity":return Ue(a,{events:i.activity(t)});case"general":return He(a,{display_name:i.displayName(t),agents:i.agents(t).filter(s=>s.enabled).length,created:i.lobbyCreatedAt(t),currency:{chosen:i.lobbyCurrency(t),table:i.exchangeRates(),fetching:g.exchangeRates}})}}d(_e,"lobbyPage");async function ee(e,n,l,o,f){let c;try{c=await f()}catch(t){c=t instanceof O?v(t.status,t.code,t.message):t instanceof Te?v(400,"invalid_request",t.issues.map(a=>a.message).join("; ")):v(500,"internal_error","That could not be done. Nothing is implied to have changed; reload and check.")}y(e,n,c.status,"application/json; charset=utf-8",JSON.stringify(c.body),l,{...o,...c.headers})}return d(ee,"reply"),{openUrl:I,handoffUrl:Z,home:w?`${w.origin}/`:"/",handles:d(e=>e==="/sign-in"||/^\/open\/[^/]+$/u.test(e)||e.startsWith("/api/"),"handles"),ticket(){if(w)throw new O(404,"not_local","This broker signs in by email.");const e=Y(),n=g.clock();for(const[l,o]of j)o<=n&&j.delete(l);return j.set(T(e),n+Ze),e},operator:H,endOperator:q,async account(e,n){const l=new URL(e.url??"/","http://settings.invalid"),o=oe(16).toString("base64"),f={},c=e.method??"GET",t="text/html; charset=utf-8",a=e.headers.host??"";if(w&&a.toLowerCase()!==w.host){e.resume(),y(e,n,404,"text/plain; charset=utf-8","Not found.",o);return}if(g.overRate(g.clientAddress(e))){e.resume(),y(e,n,429,"text/plain; charset=utf-8","Too many requests; wait a minute.",o,{"retry-after":"60"});return}const r=w?w.origin:`http://${a}`;if(l.pathname.startsWith("/api/")){if(c!=="POST"){e.resume(),y(e,n,405,"text/plain; charset=utf-8","Use POST.",o);return}await ee(e,n,o,f,async()=>{const u=l.pathname;if(!w){if(u==="/api/sign-in/ticket"){const{ticket:$}=await C(e,h.ticket,r),A=T($),U=j.get(A);if(j.delete(A),U===void 0||U<=g.clock())return v(400,"ticket_invalid","That sign-in link has expired or was already used. Run pocolobby settings again.");const ne=Y(),te=g.clock();for(const[ke,ve]of M)ve<=te&&M.delete(ke);return M.set(T(ne),te+me),p({redirect:"/"},{"set-cookie":ue(ne)})}if(u==="/api/sign-out")return await C(e,h.empty,r),p({redirect:"/"},q(e));if(!H(e))return e.resume(),v(401,"sign_in_required","Run pocolobby settings to sign in.");if(u==="/api/lobbies"){const $=await C(e,h.lobby,r,()=>H(e)),A=i.createLobby($.lobby_id);return $.first_room&&i.createRoom("owner",$.first_room,A.lobby_id),p({redirect:`/${A.lobby_id}/settings`})}return e.resume(),v(404,"not_found","Unknown settings route.")}if(u==="/api/sign-in/code")return he(e);if(u==="/api/sign-in/verify")return pe(e);if(u==="/api/sign-out"){await C(e,h.empty,r);const $=B(e.headers.cookie,R);return $&&i.endConsoleSession(T($)),p({redirect:"/"},{"set-cookie":J(R,null)})}const E=Q(e,f);if(!E)return e.resume(),v(401,"sign_in_required","Your session has ended. Sign in again.");if(u==="/api/lobbies"){const $=d(()=>{const U=B(e.headers.cookie,R);return U!==null&&i.consoleSession(T(U))?.account_id===E.account_id},"still"),A=i.createOwnedLobby(E.account_id,await C(e,h.lobby,r,$));return p({redirect:I(A.lobby_id,"/settings")})}return e.resume(),v(404,"not_found","Unknown settings route.")});return}if(e.resume(),c!=="GET"&&c!=="HEAD"){y(e,n,405,"text/plain; charset=utf-8","Use GET.",o);return}const s=l.pathname.replace(/\/$/u,"")||"/";if(!w){if(!H(e)){y(e,n,200,t,ce({nonce:o,email:null},{missing:[],lobby:null,local:!0}),o);return}if(s!=="/"){y(e,n,303,"text/plain; charset=utf-8","",o,{location:"/"});return}y(e,n,200,t,ae({nonce:o,email:null,signOut:"/api/sign-out"},{owned:i.operatorLobbies(),viewing:[],open:d(u=>`/${u}/`,"open"),mayCreate:!0,remaining:null,address:d(u=>K(u,a),"address")}),o);return}const _=Q(e,f),k=/^\/open\/([^/]+)$/u.exec(s),L=fe(l.searchParams.get("to")),N=l.searchParams.get("state");if(k&&D.safeParse(k[1]).success&&(N===null||!ie.test(N))){y(e,n,303,"text/plain; charset=utf-8","",o,{...f,location:I(k[1],L)});return}if(!_){const u=k?k[1]:null;if(s==="/"||s==="/sign-in"||k){y(e,n,200,t,ce({nonce:o,email:null},{missing:b.missing,lobby:u!==null&&D.safeParse(u).success?u:null,to:L,state:N,local:!1}),o,f);return}y(e,n,303,"text/plain; charset=utf-8","",o,{location:"/sign-in"});return}const S={nonce:o,email:_.email,signOut:"/api/sign-out"};if(k){const u=k[1],E=Y();if(D.safeParse(u).success&&i.createHandoff(_.account_id,u,Oe(E,N))){y(e,n,303,"text/plain; charset=utf-8","",o,{...f,location:G(u,`/handoff?token=${E}${L==="/"?"":`&to=${encodeURIComponent(L)}`}`)});return}if(D.safeParse(u).success&&i.boardVisibility(u)==="public"){y(e,n,303,"text/plain; charset=utf-8","",o,{...f,location:G(u)});return}y(e,n,404,t,P(S,"No lobby to open",`${_.email} cannot open a lobby named ${u}. Ask its owner to add you as a viewer.`,{href:"/",label:"Your lobbies"}),o,f);return}if(s==="/sign-in"){y(e,n,303,"text/plain; charset=utf-8","",o,{...f,location:"/"});return}if(s!=="/"){y(e,n,404,t,P(S,"Not found","There is no such page.",{href:"/",label:"Your lobbies"}),o,f);return}const z=i.consoleLobbies(_.account_id);y(e,n,200,t,ae(S,{owned:z.owned,viewing:z.viewing.map(u=>({lobby_id:u.lobby_id,display_name:u.display_name,url:I(u.lobby_id)})),open:d(u=>I(u),"open"),mayCreate:_.may_create_lobbies,remaining:re.lobbiesPerAccount-z.owned.length,address:d(u=>K(u,a),"address")}),o,f)},async lobby(e,n,l,o,f,c){const t=new URL(e.url??"/","http://settings.invalid"),a=oe(16).toString("base64"),r=e.method??"GET",s="text/html; charset=utf-8",_=t.pathname.slice(l.base.length).replace(/\/$/u,"")||"/",k=d(S=>S!==null&&(S.kind==="operator"?i.boardVisibility(l.lobby)!==null:i.ownsLobby(S.account_id,l.lobby)),"owns"),L=w?I(l.lobby,_):"/";if(_.startsWith("/settings/api/")){if(r!=="POST"){e.resume(),y(e,n,405,"text/plain; charset=utf-8","Use POST.",a);return}await ee(e,n,a,{},async()=>{if(_==="/settings/api/sign-out")return await C(e,h.empty,l.origin),p({redirect:w?`${w.origin}/`:"/"},c());if(!k(o))return e.resume(),o===null?v(401,"sign_in_required","Your session has ended. Sign in again."):v(404,"lobby_not_found","Unknown lobby");const S=o.kind==="operator"?"owner":{account_id:o.account_id};return we(e,S,l,_.slice(14),()=>f()&&k(o))});return}if(e.resume(),r!=="GET"&&r!=="HEAD"){y(e,n,405,"text/plain; charset=utf-8","Use GET.",a);return}if(o===null){y(e,n,303,"text/plain; charset=utf-8","",a,{location:L});return}if(!k(o)){y(e,n,o.kind==="account"?403:404,s,o.kind==="account"?P({nonce:a,email:o.email},"Settings are for the owner","Only the lobby\u2019s owner can open its settings. Its dashboard is still yours to see.",{href:`${l.base}/`,label:"Dashboard"}):P({nonce:a,email:null},"Not found","There is no lobby by that name.",{href:"/",label:"Your lobbies"}),a);return}if(_==="/settings/icon.png"){const S=i.boardIcon(l.lobby);S?y(e,n,200,"image/png",S.png,a,{"content-length":String(S.png.length),"cache-control":"private, no-cache"}):y(e,n,404,"text/plain; charset=utf-8","Not found.",a);return}const N=/^\/settings(?:\/(rooms|summaries|access|agents|tokens|memory|activity|general))?$/u.exec(_);if(!N){y(e,n,404,s,P({nonce:a,email:o.kind==="account"?o.email:null},"Not found","There is no such settings page.",{href:`${l.base}/settings`,label:"Settings"}),a);return}y(e,n,200,s,_e(o,l,N[1]??"overview",a,e.headers.host??"",t.searchParams),a)}}}d(fn,"createConsole");function ue(b){return b===null?`${V}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`:`${V}=${b}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${me/1e3}`}d(ue,"localCookie");function fe(b){return b!==null&&/^\/(settings(\/(rooms|summaries|access|agents|tokens|memory|activity|general))?)?$/u.test(b)?b:"/"}d(fe,"destination");export{fn as createConsole,fe as destination};