viberoom 0.8.0 → 0.9.1
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.
- package/NOTICE +5 -3
- package/README.md +94 -33
- package/assets/vendors/grok.svg +3 -0
- package/assets/vendors/hermes.svg +1 -0
- package/dist/agent-health.js +1 -1
- package/dist/hub.js +4 -1
- package/dist/jsonrpc.js +3 -1
- package/dist/main.js +4 -1
- package/dist/persona.js +8 -3
- package/dist/recipes.js +110 -8
- package/dist/room-design.js +9 -6
- package/dist/room.js +56 -22
- package/dist/server.js +3 -1
- package/package.json +1 -1
- package/ui/app.css +3 -0
- package/ui/app.js +91 -33
- package/ui/index.html +2 -2
- package/ui/theme.css +4 -0
package/NOTICE
CHANGED
|
@@ -27,9 +27,11 @@ This product bundles the following third-party material:
|
|
|
27
27
|
- Cascadia Code: Copyright 2019 Microsoft Corporation.
|
|
28
28
|
|
|
29
29
|
- Vendor icons (assets/vendors/): the logos of Claude, Codex, Gemini, Cursor,
|
|
30
|
-
OpenCode
|
|
31
|
-
registry (https://agentclientprotocol.com)
|
|
32
|
-
|
|
30
|
+
OpenCode, GitHub Copilot and Grok, as published in the Agent Client Protocol
|
|
31
|
+
registry (https://agentclientprotocol.com), and the Nous Research mark for
|
|
32
|
+
Hermes Agent, traced in one colour from the icon published on nousresearch.com.
|
|
33
|
+
Each mark belongs to its owner and is used only to identify the agent it
|
|
34
|
+
stands for.
|
|
33
35
|
|
|
34
36
|
- ACP adapters (vendor/acp/, copied from the npm packages at build time by
|
|
35
37
|
scripts/vendor-acp.mjs, each with its licence file):
|
package/README.md
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/todor-rusev/viberoom/main/docs/screenshots/readme/viberoom-dark.png">
|
|
4
|
+
<img src="https://raw.githubusercontent.com/todor-rusev/viberoom/main/docs/screenshots/readme/viberoom.png" width="620" alt="viberoom">
|
|
5
|
+
</picture>
|
|
3
6
|
</p>
|
|
4
7
|
|
|
5
|
-
<h1 align="center">viberoom</h1>
|
|
6
|
-
|
|
7
8
|
<p align="center"><strong>One chat, many coding agents.</strong><br>
|
|
8
9
|
Open a room, summon the agents you already have, give each one a role, and let them work it out with you and with each other.</p>
|
|
9
10
|
|
|
@@ -14,16 +15,86 @@ Open a room, summon the agents you already have, give each one a role, and let t
|
|
|
14
15
|
</p>
|
|
15
16
|
|
|
16
17
|
<p align="center">
|
|
17
|
-
<img src="https://raw.githubusercontent.com/todor-rusev/viberoom/main/docs/screenshots/
|
|
18
|
+
<a href="https://todor-rusev.github.io/viberoom/demo/vibeclassic.html"><img src="https://raw.githubusercontent.com/todor-rusev/viberoom/main/docs/screenshots/readme/looks.gif" width="960" alt="The recorded session, drawn in each of the six looks in turn; click to open the interactive demo"></a>
|
|
19
|
+
</p>
|
|
20
|
+
|
|
21
|
+
<p align="center">
|
|
22
|
+
<a href="https://todor-rusev.github.io/viberoom/demo/vibeclassic.html"><img src="https://raw.githubusercontent.com/todor-rusev/viberoom/main/docs/screenshots/readme/try-the-demo.png" width="880" alt="Try the interactive demo: a recorded session in the real viberoom window, two rooms, six vibemates, six looks, nothing to install"></a>
|
|
18
23
|
</p>
|
|
19
24
|
|
|
20
25
|
<p align="center">
|
|
21
|
-
<b>Claude</b> · <b>Codex</b> · <b>Gemini</b> · <b>Cursor</b> · <b>OpenCode</b> · <b>GitHub Copilot</b><br>
|
|
26
|
+
<b>Claude</b> · <b>Codex</b> · <b>Gemini</b> · <b>Cursor</b> · <b>OpenCode</b> · <b>GitHub Copilot</b> · <b>Grok Build</b> · <b>Hermes Agent</b><br>
|
|
22
27
|
<sub>whichever of them you have installed, with your own logins, in one room</sub>
|
|
23
28
|
</p>
|
|
24
29
|
|
|
25
30
|
<br>
|
|
26
31
|
|
|
32
|
+
## See it before you install it
|
|
33
|
+
|
|
34
|
+
Two rooms, six vibemates, one afternoon: a small CLI fixed and tested by Claude, Codex and OpenCode, and a
|
|
35
|
+
bakery's launch planned by Claude, Codex and Gemini, with the tool calls, the pictures, the file previews and
|
|
36
|
+
the Mermaid diagrams as they happened. The session is recorded as pages that run the real viberoom window
|
|
37
|
+
with no hub behind it. Every card below opens it in that look: walk between the rooms, open the participants,
|
|
38
|
+
change the look under Settings. (The pages live in [`docs/demo/`](docs/demo/) too, for a copy of your own.)
|
|
39
|
+
|
|
40
|
+
<a href="https://todor-rusev.github.io/viberoom/demo/vibeclassic.html"><img align="right" width="460" src="https://raw.githubusercontent.com/todor-rusev/viberoom/main/docs/screenshots/readme/card-vibeclassic.webp" alt="VibeClassic"></a>
|
|
41
|
+
|
|
42
|
+
**VibeClassic** — the default: lavender light, soft tiles, room to breathe.<br>
|
|
43
|
+
<a href="https://todor-rusev.github.io/viberoom/demo/vibeclassic.html">Open the demo in VibeClassic →</a>
|
|
44
|
+
|
|
45
|
+
<br clear="all">
|
|
46
|
+
|
|
47
|
+
<a href="https://todor-rusev.github.io/viberoom/demo/vibeclassic-dark.html"><img align="left" width="460" src="https://raw.githubusercontent.com/todor-rusev/viberoom/main/docs/screenshots/readme/card-vibeclassic-dark.webp" alt="VibeClassic Dark"></a>
|
|
48
|
+
|
|
49
|
+
**VibeClassic Dark** — the same room after dark: navy paper, the same tiles, nothing glares.<br>
|
|
50
|
+
<a href="https://todor-rusev.github.io/viberoom/demo/vibeclassic-dark.html">Open the demo in VibeClassic Dark →</a>
|
|
51
|
+
|
|
52
|
+
<br clear="all">
|
|
53
|
+
|
|
54
|
+
<a href="https://todor-rusev.github.io/viberoom/demo/clay.html"><img align="right" width="460" src="https://raw.githubusercontent.com/todor-rusev/viberoom/main/docs/screenshots/readme/card-clay.webp" alt="Clay"></a>
|
|
55
|
+
|
|
56
|
+
**Clay** — sand, terracotta and teal on warm paper; matte objects, soft corners.<br>
|
|
57
|
+
<a href="https://todor-rusev.github.io/viberoom/demo/clay.html">Open the demo in Clay →</a>
|
|
58
|
+
|
|
59
|
+
<br clear="all">
|
|
60
|
+
|
|
61
|
+
<a href="https://todor-rusev.github.io/viberoom/demo/eye-comfort.html"><img align="left" width="460" src="https://raw.githubusercontent.com/todor-rusev/viberoom/main/docs/screenshots/readme/card-eye-comfort.webp" alt="Eye Comfort"></a>
|
|
62
|
+
|
|
63
|
+
**Eye Comfort** — cream instead of white, dark grey instead of black, air between the lines: made for long evenings.<br>
|
|
64
|
+
<a href="https://todor-rusev.github.io/viberoom/demo/eye-comfort.html">Open the demo in Eye Comfort →</a>
|
|
65
|
+
|
|
66
|
+
<br clear="all">
|
|
67
|
+
|
|
68
|
+
<a href="https://todor-rusev.github.io/viberoom/demo/3d-clayful.html"><img align="right" width="460" src="https://raw.githubusercontent.com/todor-rusev/viberoom/main/docs/screenshots/readme/card-3d-clayful.webp" alt="3D clayful"></a>
|
|
69
|
+
|
|
70
|
+
**3D clayful** — cream, lavender, pink and mint with real volume: light from the top left, a wall under every object.<br>
|
|
71
|
+
<a href="https://todor-rusev.github.io/viberoom/demo/3d-clayful.html">Open the demo in 3D clayful →</a>
|
|
72
|
+
|
|
73
|
+
<br clear="all">
|
|
74
|
+
|
|
75
|
+
<a href="https://todor-rusev.github.io/viberoom/demo/terminal.html"><img align="left" width="460" src="https://raw.githubusercontent.com/todor-rusev/viberoom/main/docs/screenshots/readme/card-terminal.webp" alt="Terminal"></a>
|
|
76
|
+
|
|
77
|
+
**Terminal** — phosphor green on black, one monospace face, hairlines instead of shadows.<br>
|
|
78
|
+
<a href="https://todor-rusev.github.io/viberoom/demo/terminal.html">Open the demo in Terminal →</a>
|
|
79
|
+
|
|
80
|
+
<br clear="all">
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
## Six looks, or your own
|
|
84
|
+
|
|
85
|
+
A look is how the window is drawn: colours, light, corners, fonts. Pick one under Settings, fine-tune it,
|
|
86
|
+
or ask a vibemate for "a warm paper look for long evenings": it reads every design token with what it
|
|
87
|
+
means, checks its draft (the hub measures whether the words read on their paper and reports the numbers,
|
|
88
|
+
since a vibemate cannot see colours), saves the look among your own with your name on it, and proposes
|
|
89
|
+
wearing it as a card you can try on first. Your looks are files: export one to share it, import one
|
|
90
|
+
someone sent you.
|
|
91
|
+
|
|
92
|
+
<p align="center">
|
|
93
|
+
<img src="https://raw.githubusercontent.com/todor-rusev/viberoom/main/docs/screenshots/looks-picker.png" width="820" alt="Settings: the look picker with the six shipped looks">
|
|
94
|
+
</p>
|
|
95
|
+
|
|
96
|
+
<br>
|
|
97
|
+
|
|
27
98
|
## The idea
|
|
28
99
|
|
|
29
100
|
One agent is a conversation. Three agents in one room are a **team**: one decides what "done" means,
|
|
@@ -44,7 +115,7 @@ effort and how much it may do on its own. Three vibemates from the same agent wi
|
|
|
44
115
|
vibios behave like three different people, which is exactly the point.
|
|
45
116
|
|
|
46
117
|
<p align="center">
|
|
47
|
-
<img src="https://raw.githubusercontent.com/todor-rusev/viberoom/main/docs/screenshots/summon.png" width="960" alt="Summon a vibemate:
|
|
118
|
+
<img src="https://raw.githubusercontent.com/todor-rusev/viberoom/main/docs/screenshots/summon.png" width="960" alt="Summon a vibemate: eight agents to pick from, then a name, a face and a private brief">
|
|
48
119
|
</p>
|
|
49
120
|
|
|
50
121
|
<br>
|
|
@@ -59,8 +130,7 @@ vibemate gets the rules as instructions, so the room runs the way you wrote it d
|
|
|
59
130
|
<img src="https://raw.githubusercontent.com/todor-rusev/viberoom/main/docs/screenshots/rules.png" width="820" alt="Room rules with live @references to the vibemates">
|
|
60
131
|
</p>
|
|
61
132
|
|
|
62
|
-
|
|
63
|
-
both ends.
|
|
133
|
+
Five lines of rules buy you a handoff chain that runs on its own, with the human at both ends.
|
|
64
134
|
|
|
65
135
|
<p align="center">
|
|
66
136
|
<img src="https://raw.githubusercontent.com/todor-rusev/viberoom/main/docs/art/handoff.svg" width="960" alt="One task, four hands: you to Pip to Nova to Rex, review loop, report back">
|
|
@@ -82,22 +152,10 @@ library marks it as theirs until you have read it.
|
|
|
82
152
|
|
|
83
153
|
## Let them design the room
|
|
84
154
|
|
|
85
|
-
A vibemate can design a room as well as work in it
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
Apply, and the room is told what you decided.
|
|
90
|
-
|
|
91
|
-
## Let them design the look
|
|
92
|
-
|
|
93
|
-
A look (how the window is drawn: colours, light, corners, fonts) is data, not code: which of the shipped
|
|
94
|
-
looks it starts from, a few hues, and what it wants otherwise. Ask a vibemate for "a warm paper look for
|
|
95
|
-
long evenings" and it has a built-in skill on what makes a look good and four tools: read every token
|
|
96
|
-
with what it means, check a draft (the hub measures whether the words read on their paper and reports
|
|
97
|
-
the numbers, since a vibemate cannot see colours), save the look among your own (Settings → Appearance,
|
|
98
|
-
after the shipped ones, with your name on it), or propose wearing it as a card you can try on first. Your
|
|
99
|
-
own looks are files under your data folder: export one to share it, import one someone sent you, delete
|
|
100
|
-
one you are done with. A shipped look can only be fine-tuned, never rewritten.
|
|
155
|
+
A vibemate can design a room as well as work in it: read the room's settings and rules, check a design
|
|
156
|
+
and preview the brief the others would receive, save a template for you to pick under New room, or propose
|
|
157
|
+
a change to the room you are in. A proposal is a card in the chat with the diff; nothing changes until you
|
|
158
|
+
click Apply, and the room is told what you decided.
|
|
101
159
|
|
|
102
160
|
<br>
|
|
103
161
|
|
|
@@ -109,8 +167,8 @@ one you are done with. A shipped look can only be fine-tuned, never rewritten.
|
|
|
109
167
|
|
|
110
168
|
Write to the room and every vibemate answers, each after a short pause so replies do not trip over
|
|
111
169
|
each other. `@Name` one of them and the rest read along. Vibemates talk to each other the same way:
|
|
112
|
-
a reply wakes the others, `@Name` picks one, and a hop limit keeps an argument from running all night.
|
|
113
|
-
at once; the room stays quiet until you write again.
|
|
170
|
+
a reply wakes the others, `@Name` picks one, and a hop limit keeps an argument from running all night.
|
|
171
|
+
**Hush** stops every running reply at once; the room stays quiet until you write again.
|
|
114
172
|
|
|
115
173
|
<br>
|
|
116
174
|
|
|
@@ -122,7 +180,7 @@ at that line. A diagram block becomes a diagram. Change one of your own messages
|
|
|
122
180
|
the vibemates are told, or the conversation rewinds to that point.
|
|
123
181
|
|
|
124
182
|
<p align="center">
|
|
125
|
-
<img src="https://raw.githubusercontent.com/todor-rusev/viberoom/main/docs/screenshots/
|
|
183
|
+
<img src="https://raw.githubusercontent.com/todor-rusev/viberoom/main/docs/screenshots/conversation.png" width="960" alt="A room with two vibemates: one explains a git command from a screenshot, the other adds the part that changes whose problem it is">
|
|
126
184
|
</p>
|
|
127
185
|
|
|
128
186
|
<br>
|
|
@@ -152,10 +210,11 @@ it in a browser tab. Later, `npm install -g viberoom@latest` and the next start
|
|
|
152
210
|
sees your keys; every agent keeps its own login.
|
|
153
211
|
- **Edit a message.** Fix what you said; the vibemates get the memo, or the conversation rewinds.
|
|
154
212
|
- **Quote a message.** Select a fragment of a bubble, or copy it with Ctrl+C, and it goes into your next
|
|
155
|
-
message as a quote
|
|
213
|
+
message as a quote, with who said it and when.
|
|
156
214
|
- **Your messages on a timeline.** A thin strip on the chat's right edge, one mark per message of yours:
|
|
157
215
|
hover for the message with its neighbours, click to jump there.
|
|
158
|
-
- **
|
|
216
|
+
- **Nothing is cut in silence.** A persona or a rule longer than the room's limit is refused with the
|
|
217
|
+
numbers, and the window offers to cut it, raise the limit, or go back.
|
|
159
218
|
- **Settings save themselves.** Change a setting and it is saved: on Enter, on leaving the field, on a pick.
|
|
160
219
|
- **Search the room.** Everything anyone said, one search box.
|
|
161
220
|
- **For geeks.** Every panel folds its technical settings behind a toggle. You never have to open it.
|
|
@@ -165,11 +224,12 @@ it in a browser tab. Later, `npm install -g viberoom@latest` and the next start
|
|
|
165
224
|
## What you need
|
|
166
225
|
|
|
167
226
|
- Node.js 22 or newer.
|
|
168
|
-
- At least one coding agent installed and logged in: Claude Code, Codex, Gemini CLI, Cursor, OpenCode
|
|
169
|
-
or
|
|
227
|
+
- At least one coding agent installed and logged in: Claude Code, Codex, Gemini CLI, Cursor, OpenCode,
|
|
228
|
+
GitHub Copilot, Grok Build or Hermes Agent. viberoom finds the ones you have and offers only those. It
|
|
229
|
+
installs none of them.
|
|
170
230
|
- A browser. Chrome, Edge or Brave for the app window; anything modern for a tab.
|
|
171
231
|
|
|
172
|
-
Something does not start? `viberoom doctor` checks these
|
|
232
|
+
Something does not start? `viberoom doctor` checks these things and says which one is missing.
|
|
173
233
|
|
|
174
234
|
<br>
|
|
175
235
|
|
|
@@ -186,7 +246,7 @@ viberoom logs the background hub's log
|
|
|
186
246
|
|
|
187
247
|
`Enter` sends, `Shift+Enter` is a new line, `@Name` addresses one vibemate, `/name` invokes a skill.
|
|
188
248
|
Your data lives in `~/.viberoom` (or `$VIBEROOM_DATA_DIR`): settings, rooms with their history,
|
|
189
|
-
skills and the log.
|
|
249
|
+
skills, looks and the log.
|
|
190
250
|
|
|
191
251
|
<br>
|
|
192
252
|
|
|
@@ -195,6 +255,7 @@ skills and the log.
|
|
|
195
255
|
- A question ("how do I ...?") goes to [Discussions → Q&A](https://github.com/todor-rusev/viberoom/discussions/categories/q-a).
|
|
196
256
|
- An idea goes to [Discussions → Ideas](https://github.com/todor-rusev/viberoom/discussions/categories/ideas).
|
|
197
257
|
- A bug goes to [Issues](https://github.com/todor-rusev/viberoom/issues/new/choose); the form asks for what a fix needs.
|
|
258
|
+
- A change you would like to make: read [CONTRIBUTING.md](CONTRIBUTING.md) first. Pull requests are credited, not merged as they are.
|
|
198
259
|
|
|
199
260
|
<br>
|
|
200
261
|
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
|
2
|
+
<path fill="currentColor" d="M0.58392448,14.9254204 L0.8326,14.66295 C2.004465,13.42985 3.1678,12.2082 2.45814,10.48105 C1.50813,8.1701 2.061355,5.4619 3.820575,3.70057 C5.6495,1.8709 8.3431,1.40957 10.59295,2.336505 C11.0907,2.52161 11.5245,2.785025 11.86295,3.02993 L9.98425,3.89849 C8.235,3.163775 6.23115,3.66355 5.0081,4.88809 C3.354105,6.5426 3.019895,9.4117 4.95835,11.2656 L-0.335,15.99995 C-0.066496,15.62975 0.2538896,15.275934 0.58392448,14.9254204 Z M14.0391,2.288155 L16.33165,2.57749377e-12 L16.20795,0.172288 C14.4658,2.574355 13.6153,3.749045 14.29795,6.6879 C14.76445,8.68415 14.261,10.90255 12.63545,12.53005 C10.5861,14.58325 7.3066,15.0403 4.6059,13.19215 L6.48885,12.3193 C8.2125,12.99705 10.0983,12.69945 11.4536,11.34255 C12.80895,9.9856 13.1133,8.00925 12.4321,6.3647 C12.30265,6.05285 11.9144,5.97455 11.64275,6.1753 L6.102,10.27035 L14.0391,2.288155 Z"/>
|
|
3
|
+
</svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="-1 0 63 63"><path fill="currentColor" d="M25 0h10v1h-10zM28 1h10v1h-10zM19 2h4v1h-4zM30 2h10v1h-10zM16 3h10v1h-10zM32 3h10v1h-10zM14 4h14v1h-14zM33 4h10v1h-10zM12 5h17v1h-17zM34 5h10v1h-10zM11 6h19v1h-19zM35 6h10v1h-10zM9 7h22v1h-22zM35 7h11v1h-11zM8 8h21v1h-21zM30 8h2v1h-2zM36 8h10v1h-10zM7 9h20v1h-20zM31 9h1v1h-1zM37 9h7v1h-7zM6 10h19v1h-19zM26 10h1v1h-1zM30 10h3v1h-3zM37 10h6v1h-6zM47 10h1v1h-1zM5 11h7v1h-7zM13 11h11v1h-11zM30 11h2v1h-2zM38 11h1v1h-1zM41 11h1v1h-1zM47 11h2v1h-2zM8 12h4v1h-4zM13 12h4v1h-4zM21 12h1v1h-1zM27 12h6v1h-6zM38 12h1v1h-1zM41 12h1v1h-1zM43 12h2v1h-2zM46 12h3v1h-3zM9 13h2v1h-2zM27 13h7v1h-7zM38 13h11v1h-11zM12 14h1v1h-1zM15 14h8v1h-8zM27 14h7v1h-7zM39 14h11v1h-11zM3 15h4v1h-4zM9 15h26v1h-26zM39 15h11v1h-11zM3 16h32v1h-32zM36 16h15v1h-15zM3 17h32v1h-32zM36 17h15v1h-15zM2 18h49v1h-49zM2 19h50v1h-50zM2 20h17v1h-17zM20 20h32v1h-32zM2 21h17v1h-17zM20 21h32v1h-32zM2 22h18v1h-18zM21 22h31v1h-31zM2 23h11v1h-11zM14 23h6v1h-6zM21 23h32v1h-32zM2 24h11v1h-11zM17 24h2v1h-2zM29 24h24v1h-24zM3 25h8v1h-8zM19 25h3v1h-3zM30 25h23v1h-23zM3 26h7v1h-7zM18 26h6v1h-6zM30 26h23v1h-23zM4 27h6v1h-6zM11 27h1v1h-1zM20 27h3v1h-3zM24 27h1v1h-1zM30 27h7v1h-7zM41 27h12v1h-12zM5 28h2v1h-2zM8 28h5v1h-5zM20 28h2v1h-2zM24 28h1v1h-1zM30 28h24v1h-24zM9 29h4v1h-4zM21 29h1v1h-1zM30 29h24v1h-24zM9 30h2v1h-2zM12 30h1v1h-1zM30 30h24v1h-24zM9 31h3v1h-3zM30 31h24v1h-24zM9 32h2v1h-2zM30 32h25v1h-25zM9 33h2v1h-2zM30 33h25v1h-25zM9 34h1v1h-1zM29 34h26v1h-26zM8 35h2v1h-2zM29 35h26v1h-26zM8 36h2v1h-2zM29 36h27v1h-27zM8 37h2v1h-2zM29 37h27v1h-27zM8 38h3v1h-3zM12 38h4v1h-4zM29 38h27v1h-27zM8 39h3v1h-3zM13 39h1v1h-1zM16 39h1v1h-1zM29 39h27v1h-27zM8 40h4v1h-4zM28 40h6v1h-6zM35 40h22v1h-22zM8 41h4v1h-4zM28 41h6v1h-6zM35 41h18v1h-18zM54 41h3v1h-3zM8 42h5v1h-5zM28 42h6v1h-6zM35 42h18v1h-18zM54 42h3v1h-3zM7 43h6v1h-6zM28 43h5v1h-5zM35 43h18v1h-18zM55 43h2v1h-2zM0 44h1v1h-1zM7 44h7v1h-7zM28 44h5v1h-5zM34 44h19v1h-19zM55 44h3v1h-3zM0 45h1v1h-1zM7 45h7v1h-7zM28 45h25v1h-25zM55 45h3v1h-3zM0 46h1v1h-1zM7 46h9v1h-9zM19 46h4v1h-4zM28 46h6v1h-6zM36 46h17v1h-17zM55 46h3v1h-3zM0 47h2v1h-2zM6 47h18v1h-18zM28 47h5v1h-5zM36 47h17v1h-17zM55 47h3v1h-3zM60 47h1v1h-1zM0 48h3v1h-3zM6 48h19v1h-19zM28 48h4v1h-4zM36 48h17v1h-17zM54 48h4v1h-4zM60 48h1v1h-1zM0 49h9v1h-9zM10 49h16v1h-16zM28 49h25v1h-25zM54 49h4v1h-4zM60 49h1v1h-1zM1 50h7v1h-7zM9 50h43v1h-43zM53 50h5v1h-5zM60 50h1v1h-1zM0 51h1v1h-1zM2 51h5v1h-5zM9 51h18v1h-18zM29 51h29v1h-29zM59 51h2v1h-2zM0 52h2v1h-2zM8 52h18v1h-18zM30 52h27v1h-27zM59 52h2v1h-2zM1 53h3v1h-3zM6 53h19v1h-19zM27 53h1v1h-1zM31 53h26v1h-26zM59 53h2v1h-2zM2 54h9v1h-9zM12 54h11v1h-11zM27 54h1v1h-1zM32 54h25v1h-25zM58 54h2v1h-2zM4 55h6v1h-6zM12 55h12v1h-12zM25 55h2v1h-2zM28 55h1v1h-1zM33 55h23v1h-23zM57 55h3v1h-3zM6 56h1v1h-1zM11 56h13v1h-13zM26 56h1v1h-1zM28 56h1v1h-1zM32 56h4v1h-4zM38 56h3v1h-3zM46 56h1v1h-1zM49 56h6v1h-6zM56 56h3v1h-3zM6 57h2v1h-2zM9 57h16v1h-16zM27 57h1v1h-1zM30 57h3v1h-3zM36 57h3v1h-3zM47 57h2v1h-2zM50 57h8v1h-8zM7 58h6v1h-6zM14 58h11v1h-11zM28 58h3v1h-3zM35 58h3v1h-3zM49 58h1v1h-1zM51 58h6v1h-6zM8 59h4v1h-4zM14 59h15v1h-15zM34 59h3v1h-3zM50 59h1v1h-1zM52 59h3v1h-3zM13 60h15v1h-15zM34 60h2v1h-2zM50 60h4v1h-4zM12 61h11v1h-11zM24 61h1v1h-1zM33 61h2v1h-2zM51 61h2v1h-2zM12 62h10v1h-10zM23 62h2v1h-2zM30 62h1v1h-1zM33 62h1v1h-1zM52 62h1v1h-1z"/></svg>
|
package/dist/agent-health.js
CHANGED
|
@@ -10,7 +10,7 @@ export function loginState(probe, evidence) {
|
|
|
10
10
|
return "unknown";
|
|
11
11
|
return probe.files.length ? "missing" : "unknown";
|
|
12
12
|
}
|
|
13
|
-
const LOGIN_WORDS = /\b(not logged in|log ?in required|login required|please log ?in|unauthori[sz]ed|authentication (failed|required|error)|invalid api key|api key (is )?(missing|not set|invalid)|no credentials|credentials not found|401|403|oauth|token (expired|invalid))\b/i;
|
|
13
|
+
const LOGIN_WORDS = /\b(not logged in|log ?in required|login required|please log ?in|unauthori[sz]ed|authentication (failed|required|error)|invalid api key|api key (is )?(missing|not set|invalid)|no credentials|credentials not found|no (llm )?provider (configured|available)|401|403|oauth|token (expired|invalid))\b/i;
|
|
14
14
|
const MISSING_WORDS = /\b(enoent|not found|no such file|is not recognized|command not found|spawn\w* (failed|error))\b/i;
|
|
15
15
|
const TIMEOUT_WORDS = /\b(timed out|timeout|took too long|did not answer|no response)\b/i;
|
|
16
16
|
export function classifyStartFailure(input) {
|
package/dist/hub.js
CHANGED
|
@@ -431,7 +431,7 @@ export class Hub extends EventEmitter {
|
|
|
431
431
|
});
|
|
432
432
|
if (!choice.agentType) {
|
|
433
433
|
try {
|
|
434
|
-
room.addUnstaffed({ name: choice.name || tv.name, tagline: tv.tagline, role: tv.role, avatar: tv.avatar, skills });
|
|
434
|
+
room.addUnstaffed({ name: choice.name || tv.name, tagline: tv.tagline, role: tv.role, avatar: tv.avatar, skills, textCheck: "notice" });
|
|
435
435
|
}
|
|
436
436
|
catch (error) {
|
|
437
437
|
notices.push(`${choice.name || tv.name} could not be added: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -450,12 +450,15 @@ export class Hub extends EventEmitter {
|
|
|
450
450
|
model: choice.model ?? tv.model,
|
|
451
451
|
effort: choice.effort ?? tv.effort,
|
|
452
452
|
mode: choice.mode ?? tv.mode,
|
|
453
|
+
textCheck: "notice",
|
|
453
454
|
});
|
|
454
455
|
}
|
|
455
456
|
catch (error) {
|
|
456
457
|
notices.push(`${choice.name || tv.name} could not be summoned: ${error instanceof Error ? error.message : String(error)}`);
|
|
457
458
|
}
|
|
458
459
|
}
|
|
460
|
+
if (room.settings.customRules.length > room.settings.briefTextLimit)
|
|
461
|
+
notices.push(`The room rules from the template are ${room.settings.customRules.length} characters, over this room's limit of ${room.settings.briefTextLimit}; kept as they are, but the next edit has to fit.`);
|
|
459
462
|
this.saveRooms();
|
|
460
463
|
return { room, notices };
|
|
461
464
|
}
|
package/dist/jsonrpc.js
CHANGED
|
@@ -11,7 +11,9 @@ export class RemoteError extends Error {
|
|
|
11
11
|
rpc;
|
|
12
12
|
method;
|
|
13
13
|
constructor(rpc, method) {
|
|
14
|
-
|
|
14
|
+
const data = rpc.data;
|
|
15
|
+
const detail = typeof data === "string" ? data : data && typeof data === "object" && typeof data.details === "string" ? data.details : "";
|
|
16
|
+
super(`${method} failed: ${rpc.message}${detail ? `: ${detail}` : ""} (code ${rpc.code})`);
|
|
15
17
|
this.rpc = rpc;
|
|
16
18
|
this.method = method;
|
|
17
19
|
}
|
package/dist/main.js
CHANGED
|
@@ -19,6 +19,7 @@ function parseArgs(argv) {
|
|
|
19
19
|
port: 4810,
|
|
20
20
|
portGiven: false,
|
|
21
21
|
dataDir: process.env.VIBEROOM_DATA_DIR ? resolve(process.env.VIBEROOM_DATA_DIR) : resolve(homedir(), ".viberoom"),
|
|
22
|
+
dataDirGiven: !!process.env.VIBEROOM_DATA_DIR,
|
|
22
23
|
name: undefined,
|
|
23
24
|
open: true,
|
|
24
25
|
browser: false,
|
|
@@ -43,6 +44,7 @@ function parseArgs(argv) {
|
|
|
43
44
|
break;
|
|
44
45
|
case "--data-dir":
|
|
45
46
|
options.dataDir = resolve(next());
|
|
47
|
+
options.dataDirGiven = true;
|
|
46
48
|
break;
|
|
47
49
|
case "--open":
|
|
48
50
|
options.open = true;
|
|
@@ -271,7 +273,8 @@ async function runHub(options, log, info) {
|
|
|
271
273
|
}
|
|
272
274
|
}
|
|
273
275
|
}
|
|
274
|
-
|
|
276
|
+
if (!options.dataDirGiven)
|
|
277
|
+
migrateLegacyData(options.dataDir, log);
|
|
275
278
|
const hub = new Hub(options.dataDir, log, options.name);
|
|
276
279
|
if (options.name && hub.settings.humanName !== options.name)
|
|
277
280
|
hub.updateSettings({ humanName: options.name });
|
package/dist/persona.js
CHANGED
|
@@ -29,7 +29,8 @@ export const ROOM_SETTINGS_SPEC = {
|
|
|
29
29
|
replayAfterRestart: { kind: "integer", min: 0, max: 200, default: 10, brief: false, agent: true, doc: "Messages replayed to a vibemate whose session restarts." },
|
|
30
30
|
backlogCap: { kind: "integer", min: 1, max: 1000, default: 50, brief: false, agent: true, doc: "Most missed messages a vibemate reads on its next turn; older ones are dropped with a note." },
|
|
31
31
|
showVendorInRoster: { kind: "boolean", default: false, brief: true, agent: true, doc: "The roster in the brief names each vibemate's vendor (Claude, Codex, ...)." },
|
|
32
|
-
|
|
32
|
+
briefTextLimit: { kind: "integer", min: 500, max: 32_000, default: 8000, brief: false, agent: true, doc: "Most characters a vibio (a vibemate's role) or the room rules may have; both go into every brief. Text over it is refused with the numbers, never cut." },
|
|
33
|
+
customRules: { kind: "text", max: 32_000, default: "", brief: true, agent: true, doc: "The room rules, one per line, at most briefTextLimit characters; every vibemate gets them under 'Room rules (set by the human)'. @Name inside a rule is a live reference." },
|
|
33
34
|
refereeAction: { kind: "enum", values: ["next-header", "retry-hidden"], default: "next-header", brief: false, agent: true, doc: "On a mechanical violation (wrong language, too long): remind in the next header, or hold the reply and ask for a corrected one in a hidden turn." },
|
|
34
35
|
turnTaking: { kind: "enum", values: ["parallel", "one-at-a-time"], default: "parallel", brief: false, agent: true, doc: "parallel: every addressed vibemate answers at once; one-at-a-time: one speaks, the others queue and see the earlier replies first." },
|
|
35
36
|
waitWhileHumanTypes: { kind: "boolean", default: true, brief: false, agent: true, doc: "A vibemate about to start a turn waits while the human is typing." },
|
|
@@ -77,8 +78,12 @@ export function coerceSetting(key, raw) {
|
|
|
77
78
|
throw new Error(`${key} must be ${spec.values.join(" or ")}`);
|
|
78
79
|
return value;
|
|
79
80
|
}
|
|
80
|
-
case "text":
|
|
81
|
-
|
|
81
|
+
case "text": {
|
|
82
|
+
const value = String(raw);
|
|
83
|
+
if (value.length > spec.max)
|
|
84
|
+
throw new Error(`${key} is ${value.length} characters; at most ${spec.max}`);
|
|
85
|
+
return value;
|
|
86
|
+
}
|
|
82
87
|
case "language": {
|
|
83
88
|
if (raw && typeof raw === "object") {
|
|
84
89
|
const o = raw;
|
package/dist/recipes.js
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
// viberoom - Copyright (c) 2026 Todor Rusev - AGPL-3.0-or-later; see LICENSE
|
|
2
2
|
import { execSync } from "node:child_process";
|
|
3
|
-
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
3
|
+
import { existsSync, readdirSync, readFileSync, realpathSync } from "node:fs";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
|
-
import { dirname, join } from "node:path";
|
|
6
|
+
import { basename, dirname, join } from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { loginState } from "./agent-health.js";
|
|
9
9
|
const isWindows = process.platform === "win32";
|
|
10
|
+
const ICON_VERSION = (() => {
|
|
11
|
+
try {
|
|
12
|
+
return String(createRequire(import.meta.url)("../package.json").version ?? "0");
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return "0";
|
|
16
|
+
}
|
|
17
|
+
})();
|
|
18
|
+
const iconUrl = (id) => `/vendor-icons/${id}.svg?v=${ICON_VERSION}`;
|
|
10
19
|
function resolvePackageEntry(packageName, relativeEntry) {
|
|
11
20
|
try {
|
|
12
21
|
const require = createRequire(import.meta.url);
|
|
@@ -141,6 +150,46 @@ function resolveClaudeCode() {
|
|
|
141
150
|
function resolveCodex() {
|
|
142
151
|
return resolveGlobalNpmBin("codex") ?? resolveOnPath(["codex"]);
|
|
143
152
|
}
|
|
153
|
+
function realHome() {
|
|
154
|
+
try {
|
|
155
|
+
return realpathSync(homedir());
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function resolveGrok() {
|
|
162
|
+
const exe = isWindows ? "grok.exe" : "grok";
|
|
163
|
+
const home = realHome();
|
|
164
|
+
const dirs = [process.env.GROK_BIN_DIR, process.env.GROK_HOME && join(process.env.GROK_HOME, "bin"), join(homedir(), ".grok", "bin"), home && join(home, ".grok", "bin")];
|
|
165
|
+
for (const dir of dirs)
|
|
166
|
+
if (dir && existsSync(join(dir, exe)))
|
|
167
|
+
return join(dir, exe);
|
|
168
|
+
const onPath = resolveOnPath(["grok"]);
|
|
169
|
+
return onPath && !/\.(cmd|bat|ps1)$/i.test(onPath) ? onPath : null;
|
|
170
|
+
}
|
|
171
|
+
function resolveHermes() {
|
|
172
|
+
const hermesHome = process.env.HERMES_HOME || (isWindows ? process.env.LOCALAPPDATA && join(process.env.LOCALAPPDATA, "hermes") : join(homedir(), ".hermes"));
|
|
173
|
+
const scripts = isWindows ? "Scripts" : "bin";
|
|
174
|
+
const exe = (name) => (isWindows ? `${name}.exe` : name);
|
|
175
|
+
const candidates = [];
|
|
176
|
+
if (hermesHome) {
|
|
177
|
+
const venv = join(hermesHome, "hermes-agent", "venv");
|
|
178
|
+
candidates.push({ command: join(venv, scripts, exe("hermes-acp")), args: [] }, { command: join(venv, scripts, exe("hermes")), args: ["acp"] });
|
|
179
|
+
if (isWindows)
|
|
180
|
+
candidates.push({ command: join(hermesHome, "bin", "hermes-acp.exe"), args: [] }, { command: join(hermesHome, "bin", "hermes.exe"), args: ["acp"] });
|
|
181
|
+
}
|
|
182
|
+
if (!isWindows) {
|
|
183
|
+
candidates.push({ command: join(homedir(), ".local", "bin", "hermes"), args: ["acp"] }, { command: "/usr/local/lib/hermes-agent/venv/bin/hermes-acp", args: [] }, { command: "/usr/local/bin/hermes", args: ["acp"] });
|
|
184
|
+
}
|
|
185
|
+
for (const candidate of candidates)
|
|
186
|
+
if (existsSync(candidate.command))
|
|
187
|
+
return candidate;
|
|
188
|
+
const onPath = resolveOnPath(["hermes-acp", "hermes"]);
|
|
189
|
+
if (onPath && !/\.(cmd|bat|ps1)$/i.test(onPath))
|
|
190
|
+
return { command: onPath, args: /^hermes-acp/i.test(basename(onPath)) ? [] : ["acp"] };
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
144
193
|
const vendorDir = join(dirname(fileURLToPath(import.meta.url)), "..", "vendor", "acp");
|
|
145
194
|
const claudeAdapter = join(vendorDir, "claude-agent-acp", "dist", "index.js");
|
|
146
195
|
const codexAdapter = join(vendorDir, "codex-acp", "dist", "index.js");
|
|
@@ -151,12 +200,14 @@ const geminiEntry = resolvePackageEntry("@google/gemini-cli", join("bundle", "ge
|
|
|
151
200
|
const cursorAgent = resolveCursorAgent();
|
|
152
201
|
const openCodeExe = resolveOpenCode();
|
|
153
202
|
const copilotExe = resolveCopilot();
|
|
203
|
+
const grokExe = resolveGrok();
|
|
204
|
+
const hermesLaunch = resolveHermes();
|
|
154
205
|
const recipes = [
|
|
155
206
|
{
|
|
156
207
|
id: "claude",
|
|
157
208
|
label: "Claude (claude-agent-acp)",
|
|
158
209
|
vendor: "Claude",
|
|
159
|
-
icon: "
|
|
210
|
+
icon: iconUrl("claude"),
|
|
160
211
|
tested: true,
|
|
161
212
|
note: "Adapter around the Claude Agent SDK, driving the Claude Code installed on this machine with its login and settings.",
|
|
162
213
|
modelPresets: ["haiku", "sonnet", "opus", "default"],
|
|
@@ -182,7 +233,7 @@ const recipes = [
|
|
|
182
233
|
id: "codex",
|
|
183
234
|
label: "Codex (codex-acp)",
|
|
184
235
|
vendor: "Codex",
|
|
185
|
-
icon: "
|
|
236
|
+
icon: iconUrl("codex"),
|
|
186
237
|
tested: true,
|
|
187
238
|
note: "Adapter around the Codex App Server of the Codex CLI installed on this machine; uses its login (~/.codex) or CODEX_API_KEY. Mode 'agent' edits the working directory without asking; 'read-only' for a chat-only participant.",
|
|
188
239
|
modelPresets: ["gpt-5.4-mini", "gpt-5.5", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"],
|
|
@@ -208,7 +259,7 @@ const recipes = [
|
|
|
208
259
|
id: "gemini",
|
|
209
260
|
label: "Gemini CLI (gemini --acp)",
|
|
210
261
|
vendor: "Gemini",
|
|
211
|
-
icon: "
|
|
262
|
+
icon: iconUrl("gemini"),
|
|
212
263
|
tested: true,
|
|
213
264
|
note: "Native ACP mode of the globally installed Gemini CLI; uses the machine's Gemini login / API key. Exposes no config options over ACP: the model is fixed at launch (--model).",
|
|
214
265
|
modelPresets: ["gemini-3.8-flash", "gemini-3.7-flash"],
|
|
@@ -235,7 +286,7 @@ const recipes = [
|
|
|
235
286
|
id: "cursor",
|
|
236
287
|
label: "Cursor (cursor-agent acp)",
|
|
237
288
|
vendor: "Cursor",
|
|
238
|
-
icon: "
|
|
289
|
+
icon: iconUrl("cursor"),
|
|
239
290
|
tested: true,
|
|
240
291
|
note: "Cursor's CLI agent in native ACP mode; uses the machine's Cursor login (agent login) or CURSOR_API_KEY. Mode 'agent' edits without asking; 'ask' is read-only Q&A. Models: see the session settings after joining.",
|
|
241
292
|
modelPresets: [],
|
|
@@ -261,7 +312,7 @@ const recipes = [
|
|
|
261
312
|
id: "opencode",
|
|
262
313
|
label: "OpenCode (opencode acp)",
|
|
263
314
|
vendor: "OpenCode",
|
|
264
|
-
icon: "
|
|
315
|
+
icon: iconUrl("opencode"),
|
|
265
316
|
tested: false,
|
|
266
317
|
note: "The open-source coding agent in native ACP mode; the model list comes from the providers configured in OpenCode (opencode providers). Mode 'plan' is read-only; 'build' edits the working directory (OpenCode's own permission config decides what still asks; the questions arrive here).",
|
|
267
318
|
modelPresets: [],
|
|
@@ -286,7 +337,7 @@ const recipes = [
|
|
|
286
337
|
id: "copilot",
|
|
287
338
|
label: "GitHub Copilot (copilot --acp)",
|
|
288
339
|
vendor: "Copilot",
|
|
289
|
-
icon: "
|
|
340
|
+
icon: iconUrl("copilot"),
|
|
290
341
|
tested: false,
|
|
291
342
|
note: "GitHub Copilot CLI in native ACP mode; uses the machine's Copilot login (copilot login). Session modes agent / plan / autopilot; the 'allow_all' option decides whether tool calls ask for permission. Exposes no model option over ACP: the model is fixed at launch (--model, e.g. auto).",
|
|
292
343
|
modelPresets: ["auto"],
|
|
@@ -312,6 +363,57 @@ const recipes = [
|
|
|
312
363
|
args: ["--acp", ...(model ? ["--model", model] : [])],
|
|
313
364
|
}),
|
|
314
365
|
},
|
|
366
|
+
{
|
|
367
|
+
id: "grok",
|
|
368
|
+
label: "Grok Build (grok agent stdio)",
|
|
369
|
+
vendor: "Grok",
|
|
370
|
+
icon: iconUrl("grok"),
|
|
371
|
+
tested: true,
|
|
372
|
+
note: "xAI's coding agent in its native ACP mode; uses this machine's Grok login (grok login) or XAI_API_KEY. It reports no session modes over ACP, so the mode is a launch flag: 'ask-first' asks before every tool call, 'always-approve' never does; a change restarts the session with its notes kept. Model and reasoning effort are session options (the model also goes on the launch command).",
|
|
373
|
+
modelPresets: ["grok-4.6", "grok-4.5"],
|
|
374
|
+
defaultModel: null,
|
|
375
|
+
effortPresets: ["xhigh", "high", "medium", "low"],
|
|
376
|
+
defaultEffort: null,
|
|
377
|
+
modePresets: ["ask-first", "always-approve"],
|
|
378
|
+
defaultMode: "ask-first",
|
|
379
|
+
bypassMode: "always-approve",
|
|
380
|
+
unavailableReason: grokExe ? null : "Grok Build not found",
|
|
381
|
+
installedAt: grokExe,
|
|
382
|
+
installHint: "curl -fsSL https://x.ai/cli/install.sh | bash (Windows: irm https://x.ai/cli/install.ps1 | iex), or npm install -g @xai-official/grok; then grok login",
|
|
383
|
+
loginCommand: "grok login",
|
|
384
|
+
login: { env: ["XAI_API_KEY", "GROK_DEPLOYMENT_KEY"], files: [".grok/auth.json"], command: "grok login" },
|
|
385
|
+
loginState: "unknown",
|
|
386
|
+
modeAtLaunch: true,
|
|
387
|
+
build: ({ model, mode }) => ({
|
|
388
|
+
command: grokExe ?? "",
|
|
389
|
+
args: ["agent", ...(model ? ["-m", model] : []), ...(mode === "always-approve" ? ["--always-approve"] : []), "stdio"],
|
|
390
|
+
}),
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
id: "hermes",
|
|
394
|
+
label: "Hermes Agent (hermes acp)",
|
|
395
|
+
vendor: "Hermes",
|
|
396
|
+
icon: iconUrl("hermes"),
|
|
397
|
+
tested: true,
|
|
398
|
+
note: "Nous Research's open-source agent in its native ACP mode; the provider and model are the ones configured in Hermes (hermes model). Mode 'default' asks before edits, 'accept_edits' auto-allows workspace and /tmp edits, 'dont_ask' auto-allows file edits except sensitive paths; a tool call that still needs permission arrives here with Hermes' own choices: once, this session, or always.",
|
|
399
|
+
modelPresets: [],
|
|
400
|
+
defaultModel: null,
|
|
401
|
+
effortPresets: [],
|
|
402
|
+
defaultEffort: null,
|
|
403
|
+
modePresets: ["default", "accept_edits", "dont_ask"],
|
|
404
|
+
defaultMode: "default",
|
|
405
|
+
bypassMode: "dont_ask",
|
|
406
|
+
unavailableReason: hermesLaunch ? null : "Hermes Agent not found",
|
|
407
|
+
installedAt: hermesLaunch?.command ?? null,
|
|
408
|
+
installHint: "curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash (Windows: iex (irm https://hermes-agent.nousresearch.com/install.ps1)), then hermes model",
|
|
409
|
+
loginCommand: "hermes model",
|
|
410
|
+
login: { env: [], files: [], command: "hermes model" },
|
|
411
|
+
loginState: "unknown",
|
|
412
|
+
build: () => ({
|
|
413
|
+
command: hermesLaunch?.command ?? "",
|
|
414
|
+
args: hermesLaunch?.args ?? [],
|
|
415
|
+
}),
|
|
416
|
+
},
|
|
315
417
|
];
|
|
316
418
|
const fakeAgent = process.env.VIBEROOM_FAKE_AGENT;
|
|
317
419
|
if (fakeAgent) {
|
package/dist/room-design.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// viberoom - Copyright (c) 2026 Todor Rusev - AGPL-3.0-or-later; see LICENSE
|
|
2
2
|
import { AGENT_SETTINGS, DEFAULT_ROOM_SETTINGS, NAME_PATTERN, ROOM_SETTINGS_SPEC, SILENT_MARKER, REQUEST_BRIEF_MARKER, buildBrief, coerceSetting } from "./persona.js";
|
|
3
3
|
export const TAGLINE_MAX = 80;
|
|
4
|
-
export const ROLE_MAX =
|
|
4
|
+
export const ROLE_MAX = 8000;
|
|
5
5
|
export const AVATAR_MAX = 8;
|
|
6
6
|
export const RULES_SOFT_MAX = 12;
|
|
7
7
|
export const ROLE_SOFT_MAX = 1200;
|
|
8
|
-
export const RULES_NEAR_LIMIT =
|
|
8
|
+
export const RULES_NEAR_LIMIT = 0.75;
|
|
9
9
|
export function ruleLines(customRules) {
|
|
10
10
|
return customRules
|
|
11
11
|
.split(/\r?\n/)
|
|
@@ -57,6 +57,7 @@ export function lintRoomDesign(design, context) {
|
|
|
57
57
|
if (!vibemates.length)
|
|
58
58
|
error("template-no-vibemates", "a template needs at least one vibemate");
|
|
59
59
|
}
|
|
60
|
+
const roleMax = raw.briefTextLimit !== undefined ? settings.briefTextLimit : context.briefTextLimit ?? settings.briefTextLimit ?? ROLE_MAX;
|
|
60
61
|
const seen = new Map();
|
|
61
62
|
const touched = context.changedVibemates ? new Set(context.changedVibemates.map((n) => n.trim().toLowerCase())) : null;
|
|
62
63
|
for (const [i, v] of vibemates.entries()) {
|
|
@@ -79,8 +80,8 @@ export function lintRoomDesign(design, context) {
|
|
|
79
80
|
error("tagline-too-long", `${who}: the tagline is at most ${TAGLINE_MAX} characters (it is the one line the others see)`);
|
|
80
81
|
else if (soft && !(v?.tagline ?? "").trim())
|
|
81
82
|
warn("vibemate-no-tagline", `${who} has no tagline: the others read it in the roster to know what this one leans to`);
|
|
82
|
-
if ((v?.role ?? "").length >
|
|
83
|
-
error("role-too-long", `${who}: the role is
|
|
83
|
+
if ((v?.role ?? "").length > roleMax)
|
|
84
|
+
error("role-too-long", `${who}: the role is ${v.role.length} characters; the limit is ${roleMax} (the briefTextLimit setting)`);
|
|
84
85
|
else if (soft && !(v?.role ?? "").trim())
|
|
85
86
|
warn("vibemate-no-role", `${who} has no role: without one it is the agent's default self, not a character`);
|
|
86
87
|
else if (soft && (v.role ?? "").length > ROLE_SOFT_MAX)
|
|
@@ -109,8 +110,10 @@ export function lintRoomDesign(design, context) {
|
|
|
109
110
|
const rules = ruleLines(settings.customRules);
|
|
110
111
|
if (rules.length > RULES_SOFT_MAX)
|
|
111
112
|
warn("too-many-rules", `${rules.length} rules: every vibemate carries them on every turn; ${RULES_SOFT_MAX} is a full protocol, longer belongs in a skill`);
|
|
112
|
-
if (settings.customRules.length >
|
|
113
|
-
|
|
113
|
+
if (settings.customRules.length > roleMax)
|
|
114
|
+
error("rules-too-long", `the rules are ${settings.customRules.length} characters; the limit is ${roleMax} (the briefTextLimit setting)`);
|
|
115
|
+
else if (settings.customRules.length > roleMax * RULES_NEAR_LIMIT)
|
|
116
|
+
warn("rules-near-limit", `the rules are ${settings.customRules.length} characters; the limit is ${roleMax}`);
|
|
114
117
|
for (const rule of rules) {
|
|
115
118
|
for (const m of BRIEF_MECHANICS)
|
|
116
119
|
if (m.pattern.test(rule))
|
package/dist/room.js
CHANGED
|
@@ -21,6 +21,7 @@ import { applyVibemateChanges, diffSettings, lintRoomDesign, ruleLines } from ".
|
|
|
21
21
|
import { BRIEF_AFFECTING_SETTINGS, AGENT_SETTINGS, coerceSetting, describeSettings, DEFAULT_ROOM_SETTINGS, ROOM_SETTINGS_SPEC, REQUEST_BRIEF_MARKER, NAME_PATTERN, SILENT_MARKER, buildBrief, buildHeader, composeCorrectionPrompt, composePrompt, countSentences, ensureDir, } from "./persona.js";
|
|
22
22
|
import { Transcript } from "./log.js";
|
|
23
23
|
const SKILL_TOOL_READY_MS = 5000;
|
|
24
|
+
const AUTH_WAIT_MS = 8000;
|
|
24
25
|
const COLORS = ["#6d5dfc", "#16a34a", "#d97706", "#dc2626", "#0891b2", "#be185d", "#4d7c0f", "#7c3aed"];
|
|
25
26
|
const MENTION_PATTERN = /@([\p{L}\p{N}][\p{L}\p{N}_-]*)/gu;
|
|
26
27
|
const RULE_REF_TOKEN = /@\{p:([^}]+)\}/g;
|
|
@@ -113,7 +114,7 @@ export class Room extends EventEmitter {
|
|
|
113
114
|
restore(stored) {
|
|
114
115
|
for (const s of stored) {
|
|
115
116
|
if (!s.agentType) {
|
|
116
|
-
this.addUnstaffed({ name: s.name, tagline: s.tagline, role: s.role, avatar: s.avatar, skills: s.skills, color: s.color, id: s.id });
|
|
117
|
+
this.addUnstaffed({ name: s.name, tagline: s.tagline, role: s.role, avatar: s.avatar, skills: s.skills, color: s.color, id: s.id, textCheck: "keep" });
|
|
117
118
|
continue;
|
|
118
119
|
}
|
|
119
120
|
const recipe = getRecipe(s.agentType);
|
|
@@ -505,14 +506,8 @@ export class Room extends EventEmitter {
|
|
|
505
506
|
participant.sessionId = undefined;
|
|
506
507
|
this.restoredSeen.set(id, this.seq);
|
|
507
508
|
this.push({ type: "participant", participant });
|
|
508
|
-
if (!online)
|
|
509
|
-
|
|
510
|
-
const comesBack = withNotes ? "it comes back with its notes" : "it comes back knowing nothing from before";
|
|
511
|
-
this.postSystem(options.reason ? `${participant.name} was respawned while offline (${why}): ${comesBack}.` : `${participant.name} was respawned while offline: ${comesBack}.`);
|
|
512
|
-
this.push({ type: "participant", participant });
|
|
513
|
-
this.log.info(`respawn of ${participant.name} (offline): stored session dropped`);
|
|
514
|
-
return participant;
|
|
515
|
-
}
|
|
509
|
+
if (!online)
|
|
510
|
+
this.log.info(`respawn of ${participant.name} (offline): stored session dropped, starting it now`);
|
|
516
511
|
await this.reconnect(id, memory
|
|
517
512
|
? { mode: "replay", replay, memory: withNotes, reason: `${why}; it comes back with ${withNotes ? "its notes and " : ""}the last ${replay} messages` }
|
|
518
513
|
: { mode: "replay", replay: 0, reason: `${why}, it remembers nothing from before` });
|
|
@@ -676,7 +671,7 @@ export class Room extends EventEmitter {
|
|
|
676
671
|
continue;
|
|
677
672
|
let value = coerceSetting(key, patch[key]);
|
|
678
673
|
if (key === "customRules") {
|
|
679
|
-
const resolved = this.resolveRuleReferences(value);
|
|
674
|
+
const resolved = this.resolveRuleReferences(this.guardBriefText("The room rules text", value, "refuse", next.briefTextLimit));
|
|
680
675
|
unknownRefs = resolved.unknown;
|
|
681
676
|
value = resolved.stored;
|
|
682
677
|
}
|
|
@@ -699,6 +694,14 @@ export class Room extends EventEmitter {
|
|
|
699
694
|
}
|
|
700
695
|
return this.settings;
|
|
701
696
|
}
|
|
697
|
+
guardBriefText(what, text, check = "refuse", limit = this.settings.briefTextLimit) {
|
|
698
|
+
if (check === "keep" || text.length <= limit)
|
|
699
|
+
return text;
|
|
700
|
+
if (check === "refuse")
|
|
701
|
+
throw new Error(`${what} is ${text.length} characters; this room's limit is ${limit} (the briefTextLimit setting). Shorten it, move the instructions into a skill, or raise the limit.`);
|
|
702
|
+
this.postSystem(`${what} is ${text.length} characters, over this room's limit of ${limit}; kept as it is, but the next edit has to fit (shorten it or raise briefTextLimit).`, "human", false, { tone: "attention" });
|
|
703
|
+
return text;
|
|
704
|
+
}
|
|
702
705
|
updatePersona(id, patch) {
|
|
703
706
|
const participant = this.participants.get(id);
|
|
704
707
|
const runtime = this.runtimes.get(id);
|
|
@@ -727,7 +730,7 @@ export class Room extends EventEmitter {
|
|
|
727
730
|
changed.push("tagline");
|
|
728
731
|
}
|
|
729
732
|
if (patch.role !== undefined && patch.role.trim() !== (participant.role ?? "")) {
|
|
730
|
-
participant.role = patch.role.trim()
|
|
733
|
+
participant.role = this.guardBriefText(`${participant.name}'s vibio`, patch.role.trim());
|
|
731
734
|
changed.push("role");
|
|
732
735
|
}
|
|
733
736
|
if (patch.avatar !== undefined) {
|
|
@@ -786,7 +789,7 @@ export class Room extends EventEmitter {
|
|
|
786
789
|
return cached;
|
|
787
790
|
const cwd = ensureDir(join(this.dataDir, ".probe"));
|
|
788
791
|
const log = this.log.child(`probe:${recipeId}`);
|
|
789
|
-
const launch = recipe.build({ model: null });
|
|
792
|
+
const launch = recipe.build({ model: null, mode: null });
|
|
790
793
|
const agent = new AcpAgent({ ...launch, cwd }, {
|
|
791
794
|
onSessionUpdate: () => undefined,
|
|
792
795
|
onPermissionRequest: async () => ({ outcome: { outcome: "cancelled" } }),
|
|
@@ -798,7 +801,7 @@ export class Room extends EventEmitter {
|
|
|
798
801
|
const info = await Promise.race([
|
|
799
802
|
(async () => {
|
|
800
803
|
const init = await agent.initialize({ name: "viberoom", version: "0.2.0" });
|
|
801
|
-
const session = await this.openSession(agent, cwd, log);
|
|
804
|
+
const session = await this.openSession(agent, cwd, log, [], recipe);
|
|
802
805
|
const result = {
|
|
803
806
|
recipeId,
|
|
804
807
|
agentInfo: { name: init.agentInfo?.name ?? null, version: init.agentInfo?.version ?? null },
|
|
@@ -806,6 +809,7 @@ export class Room extends EventEmitter {
|
|
|
806
809
|
modes: session.modes ?? null,
|
|
807
810
|
configOptions: session.configOptions ?? [],
|
|
808
811
|
modelAtLaunch: !!recipe.modelAtLaunch,
|
|
812
|
+
modeAtLaunch: !!recipe.modeAtLaunch,
|
|
809
813
|
discoveredAt: Date.now(),
|
|
810
814
|
durationMs: 0,
|
|
811
815
|
};
|
|
@@ -858,7 +862,7 @@ export class Room extends EventEmitter {
|
|
|
858
862
|
turns: 0,
|
|
859
863
|
color: options.color ?? COLORS[this.colorIndex++ % COLORS.length],
|
|
860
864
|
tagline: (options.tagline ?? "").trim().slice(0, 80),
|
|
861
|
-
role: (options.role ?? "").trim()
|
|
865
|
+
role: this.guardBriefText(`${name}'s vibio`, (options.role ?? "").trim(), options.textCheck),
|
|
862
866
|
avatar: (options.avatar ?? "").trim().slice(0, 8) || undefined,
|
|
863
867
|
replyDelay: options.replyDelay === undefined || options.replyDelay === null ? undefined : Math.max(0, Math.min(120, Number(options.replyDelay) || 0)),
|
|
864
868
|
skills: normalizeSkillList(options.skills ?? undefined),
|
|
@@ -888,7 +892,7 @@ export class Room extends EventEmitter {
|
|
|
888
892
|
turns: 0,
|
|
889
893
|
color: input.color ?? COLORS[this.colorIndex++ % COLORS.length],
|
|
890
894
|
tagline: (input.tagline ?? "").trim().slice(0, 80),
|
|
891
|
-
role: (input.role ?? "").trim()
|
|
895
|
+
role: this.guardBriefText(`${name}'s vibio`, (input.role ?? "").trim(), input.textCheck),
|
|
892
896
|
avatar: (input.avatar ?? "").trim().slice(0, 8) || undefined,
|
|
893
897
|
skills: normalizeSkillList(input.skills),
|
|
894
898
|
violations: 0,
|
|
@@ -938,7 +942,7 @@ export class Room extends EventEmitter {
|
|
|
938
942
|
const name = participant.name;
|
|
939
943
|
const log = this.log.child(name);
|
|
940
944
|
const cwd = ensureDir(this.dir);
|
|
941
|
-
const spec = recipe.build({ model: launch.model });
|
|
945
|
+
const spec = recipe.build({ model: launch.model, mode: launch.mode });
|
|
942
946
|
const transcript = new Transcript(join(this.dataDir, "transcripts"), name);
|
|
943
947
|
log.info(`spawning ${spec.command} ${spec.args.join(" ")} (cwd ${cwd}); transcript ${transcript.path}`);
|
|
944
948
|
const stderrTail = [];
|
|
@@ -990,7 +994,7 @@ export class Room extends EventEmitter {
|
|
|
990
994
|
}
|
|
991
995
|
}
|
|
992
996
|
if (!session)
|
|
993
|
-
session = await this.openSession(agent, cwd, log, mcpServers);
|
|
997
|
+
session = await this.openSession(agent, cwd, log, mcpServers, recipe);
|
|
994
998
|
participant.sessionId = session.sessionId;
|
|
995
999
|
participant.sessionOrigin = origin;
|
|
996
1000
|
const storedSeen = this.restoredSeen.get(id);
|
|
@@ -1044,10 +1048,15 @@ export class Room extends EventEmitter {
|
|
|
1044
1048
|
const warnings = await this.applyConfig(runtime, participant, {
|
|
1045
1049
|
model: recipe.modelAtLaunch ? null : launch.model,
|
|
1046
1050
|
effort: launch.effort,
|
|
1047
|
-
mode: launch.mode,
|
|
1051
|
+
mode: recipe.modeAtLaunch ? null : launch.mode,
|
|
1048
1052
|
});
|
|
1049
1053
|
if (recipe.modelAtLaunch && launch.model)
|
|
1050
1054
|
participant.model = launch.model;
|
|
1055
|
+
if (recipe.modeAtLaunch) {
|
|
1056
|
+
if (!participant.modes?.length)
|
|
1057
|
+
participant.modes = recipe.modePresets.map((id) => ({ id, name: id }));
|
|
1058
|
+
participant.mode = launch.mode ?? recipe.defaultMode ?? participant.mode;
|
|
1059
|
+
}
|
|
1051
1060
|
for (const w of warnings)
|
|
1052
1061
|
this.notice(`${name}: ${w}`, "warn");
|
|
1053
1062
|
participant.status = "idle";
|
|
@@ -1148,6 +1157,16 @@ export class Room extends EventEmitter {
|
|
|
1148
1157
|
const participant = this.participants.get(id);
|
|
1149
1158
|
if (!runtime || !participant)
|
|
1150
1159
|
throw new Error("no such agent (offline?)");
|
|
1160
|
+
const recipe = getRecipe(participant.agentType ?? "");
|
|
1161
|
+
if (recipe?.modeAtLaunch && configId === "mode") {
|
|
1162
|
+
if (!recipe.modePresets.includes(String(value)))
|
|
1163
|
+
throw new Error(`${participant.name}: no mode "${value}" (${recipe.modePresets.join(", ")})`);
|
|
1164
|
+
participant.launch = { model: participant.launch?.model ?? null, effort: participant.launch?.effort ?? null, mode: String(value) };
|
|
1165
|
+
participant.mode = String(value);
|
|
1166
|
+
this.push({ type: "participant", participant });
|
|
1167
|
+
await this.respawnAgent(id, { memory: true, reason: "its mode changed" });
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
1151
1170
|
const hasOption = participant.configOptions?.some((o) => o.id === configId);
|
|
1152
1171
|
if (!hasOption && configId === "mode" && participant.modes?.some((m) => m.id === value)) {
|
|
1153
1172
|
await runtime.agent.setMode(runtime.sessionId, String(value));
|
|
@@ -1349,6 +1368,7 @@ export class Room extends EventEmitter {
|
|
|
1349
1368
|
humanName: this.settings.humanName,
|
|
1350
1369
|
roomName: this.settings.name,
|
|
1351
1370
|
base: kind === "room" ? { ...this.settings, customRules: this.renderRuleReferences(this.settings.customRules) } : undefined,
|
|
1371
|
+
briefTextLimit: this.settings.briefTextLimit,
|
|
1352
1372
|
knownSkills: this.skills ? this.skills.library.list().map((s) => s.name) : undefined,
|
|
1353
1373
|
};
|
|
1354
1374
|
}
|
|
@@ -2577,7 +2597,7 @@ export class Room extends EventEmitter {
|
|
|
2577
2597
|
this.notice(`${participant.name}: agent process exited.`, "error");
|
|
2578
2598
|
}
|
|
2579
2599
|
}
|
|
2580
|
-
async openSession(agent, cwd, log, mcpServers = []) {
|
|
2600
|
+
async openSession(agent, cwd, log, mcpServers = [], recipe) {
|
|
2581
2601
|
try {
|
|
2582
2602
|
return await agent.newSession(cwd, mcpServers);
|
|
2583
2603
|
}
|
|
@@ -2585,12 +2605,22 @@ export class Room extends EventEmitter {
|
|
|
2585
2605
|
if (!isAuthRequired(error) || !agent.authMethods.length)
|
|
2586
2606
|
throw error;
|
|
2587
2607
|
log.info(`session/new needs authentication: ${describeError(error)}`);
|
|
2608
|
+
if (recipe?.loginState === "missing")
|
|
2609
|
+
throw new Error(`authentication required; log in with the agent's own CLI first${recipe.loginCommand ? ` (${recipe.loginCommand})` : ""}`);
|
|
2588
2610
|
}
|
|
2589
2611
|
const failures = [];
|
|
2590
2612
|
for (const method of agent.authMethods) {
|
|
2613
|
+
if (method.type === "terminal") {
|
|
2614
|
+
failures.push(`${method.id}: needs a terminal`);
|
|
2615
|
+
continue;
|
|
2616
|
+
}
|
|
2591
2617
|
log.info(`authenticate with "${method.id}"${method.name ? ` (${method.name})` : ""}`);
|
|
2592
2618
|
try {
|
|
2593
|
-
await agent.authenticate(method.id);
|
|
2619
|
+
const done = await Promise.race([agent.authenticate(method.id).then(() => true), delay(AUTH_WAIT_MS).then(() => false)]);
|
|
2620
|
+
if (!done) {
|
|
2621
|
+
failures.push(`${method.id}: no answer in ${AUTH_WAIT_MS / 1000} s (a sign-in that needs you)`);
|
|
2622
|
+
continue;
|
|
2623
|
+
}
|
|
2594
2624
|
return await agent.newSession(cwd, mcpServers);
|
|
2595
2625
|
}
|
|
2596
2626
|
catch (error) {
|
|
@@ -2667,7 +2697,7 @@ export class Room extends EventEmitter {
|
|
|
2667
2697
|
participant.mode = pick("mode") ?? participant.mode;
|
|
2668
2698
|
}
|
|
2669
2699
|
failStart(participant, error, fresh, stderr = []) {
|
|
2670
|
-
participant.statusDetail =
|
|
2700
|
+
participant.statusDetail = describeError(error);
|
|
2671
2701
|
const recipe = getRecipe(participant.agentType ?? "");
|
|
2672
2702
|
participant.trouble = classifyStartFailure({
|
|
2673
2703
|
error: participant.statusDetail,
|
|
@@ -2826,7 +2856,11 @@ function isAuthRequired(error) {
|
|
|
2826
2856
|
return error instanceof Error && /auth/i.test(error.message);
|
|
2827
2857
|
}
|
|
2828
2858
|
function describeError(error) {
|
|
2829
|
-
|
|
2859
|
+
if (!(error instanceof Error))
|
|
2860
|
+
return String(error);
|
|
2861
|
+
const data = error.rpc?.data ?? error.data;
|
|
2862
|
+
const detail = typeof data === "string" ? data : data && typeof data === "object" && typeof data.details === "string" ? data.details : "";
|
|
2863
|
+
return detail && !error.message.includes(detail) ? `${error.message}: ${detail}` : error.message;
|
|
2830
2864
|
}
|
|
2831
2865
|
function looksSilent(text) {
|
|
2832
2866
|
const t = text.trim().toLowerCase();
|
package/dist/server.js
CHANGED
|
@@ -34,6 +34,8 @@ const STATIC_FILES = {
|
|
|
34
34
|
"/vendor-icons/cursor.svg": { file: "vendors/cursor.svg", type: "image/svg+xml", dir: "assets" },
|
|
35
35
|
"/vendor-icons/opencode.svg": { file: "vendors/opencode.svg", type: "image/svg+xml", dir: "assets" },
|
|
36
36
|
"/vendor-icons/copilot.svg": { file: "vendors/copilot.svg", type: "image/svg+xml", dir: "assets" },
|
|
37
|
+
"/vendor-icons/grok.svg": { file: "vendors/grok.svg", type: "image/svg+xml", dir: "assets" },
|
|
38
|
+
"/vendor-icons/hermes.svg": { file: "vendors/hermes.svg", type: "image/svg+xml", dir: "assets" },
|
|
37
39
|
"/vendor/mermaid.min.js": { file: "mermaid/dist/mermaid.min.js", type: "text/javascript; charset=utf-8", dir: "node_modules" },
|
|
38
40
|
"/vendor/marked.umd.js": { file: "marked/lib/marked.umd.js", type: "text/javascript; charset=utf-8", dir: "node_modules" },
|
|
39
41
|
"/vendor/prism.js": { file: "prismjs/prism.js", type: "text/javascript; charset=utf-8", dir: "node_modules" },
|
|
@@ -146,7 +148,7 @@ export function startServer(hub, port, log, info, onShutdownRequest, onRestartRe
|
|
|
146
148
|
if (req.method === "GET" && STATIC_FILES[path]) {
|
|
147
149
|
const entry = STATIC_FILES[path];
|
|
148
150
|
const body = await readFile(staticPath(entry));
|
|
149
|
-
res.writeHead(200, { "Content-Type": entry.type, "Cache-Control": entry.dir === "
|
|
151
|
+
res.writeHead(200, { "Content-Type": entry.type, "Cache-Control": entry.dir === "node_modules" ? "public, max-age=3600" : "no-cache" });
|
|
150
152
|
res.end(body);
|
|
151
153
|
return;
|
|
152
154
|
}
|
package/package.json
CHANGED
package/ui/app.css
CHANGED
|
@@ -712,6 +712,9 @@ table.csv tbody tr:nth-child(even) td { background: var(--table-stripe); }
|
|
|
712
712
|
.section textarea { resize: vertical; min-height: 76px; height: auto; padding: 10px 14px; line-height: 1.5; }
|
|
713
713
|
.section input::placeholder, .section textarea::placeholder { color: var(--placeholder); font-weight: 600; }
|
|
714
714
|
.section .hint { margin: -4px 0 10px; }
|
|
715
|
+
.field .count { font-variant-numeric: tabular-nums; color: var(--muted); }
|
|
716
|
+
.field .count::before { content: " · "; }
|
|
717
|
+
.field .count.over { color: var(--danger); font-weight: 800; }
|
|
715
718
|
.three-inline { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 6px; }
|
|
716
719
|
.three-inline label { margin-bottom: 4px; }
|
|
717
720
|
.rc-num { width: 76px; display: inline-flex; }
|
package/ui/app.js
CHANGED
|
@@ -685,28 +685,34 @@
|
|
|
685
685
|
}
|
|
686
686
|
if (!found.size) return;
|
|
687
687
|
const paths = await Promise.all([...found.keys()].map((token) => resolveInRoom(room.id, token)));
|
|
688
|
-
|
|
688
|
+
const resolved = new Map();
|
|
689
689
|
[...found.keys()].forEach((token, i) => {
|
|
690
690
|
const full = paths[i] && paths[i].path;
|
|
691
|
-
if (
|
|
692
|
-
const byNode = new Map();
|
|
693
|
-
for (const hit of found.get(token)) byNode.set(hit.node, [...(byNode.get(hit.node) || []), hit]);
|
|
694
|
-
for (const [node, hits] of byNode) {
|
|
695
|
-
if (!node.isConnected) continue;
|
|
696
|
-
for (const hit of hits.sort((a, b) => b.index - a.index)) {
|
|
697
|
-
const after = node.splitText(hit.index);
|
|
698
|
-
after.nodeValue = after.nodeValue.slice(hit.length);
|
|
699
|
-
const link = document.createElement("a");
|
|
700
|
-
link.className = "open-link";
|
|
701
|
-
link.href = "#";
|
|
702
|
-
link.dataset.open = `${full}${hit.line}`;
|
|
703
|
-
link.title = `${full} (relative to the room's folder)`;
|
|
704
|
-
link.textContent = `${token}${hit.line}`;
|
|
705
|
-
node.parentNode.insertBefore(link, after);
|
|
706
|
-
linked = true;
|
|
707
|
-
}
|
|
708
|
-
}
|
|
691
|
+
if (full) resolved.set(token, full);
|
|
709
692
|
});
|
|
693
|
+
if (!resolved.size) return;
|
|
694
|
+
const byNode = new Map();
|
|
695
|
+
for (const [token, hits] of found) {
|
|
696
|
+
if (!resolved.has(token)) continue;
|
|
697
|
+
for (const hit of hits) byNode.set(hit.node, [...(byNode.get(hit.node) || []), { ...hit, token }]);
|
|
698
|
+
}
|
|
699
|
+
let linked = false;
|
|
700
|
+
for (const [node, hits] of byNode) {
|
|
701
|
+
if (!node.isConnected) continue;
|
|
702
|
+
for (const hit of hits.sort((a, b) => b.index - a.index)) {
|
|
703
|
+
const full = resolved.get(hit.token);
|
|
704
|
+
const after = node.splitText(hit.index);
|
|
705
|
+
after.nodeValue = after.nodeValue.slice(hit.length);
|
|
706
|
+
const link = document.createElement("a");
|
|
707
|
+
link.className = "open-link";
|
|
708
|
+
link.href = "#";
|
|
709
|
+
link.dataset.open = `${full}${hit.line}`;
|
|
710
|
+
link.title = `${full} (relative to the room's folder)`;
|
|
711
|
+
link.textContent = `${hit.token}${hit.line}`;
|
|
712
|
+
node.parentNode.insertBefore(link, after);
|
|
713
|
+
linked = true;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
710
716
|
if (linked) renderPreviews(textEl, m);
|
|
711
717
|
}
|
|
712
718
|
function csvTable(rows) {
|
|
@@ -1106,8 +1112,8 @@
|
|
|
1106
1112
|
const field = lastField;
|
|
1107
1113
|
lastField = null;
|
|
1108
1114
|
try {
|
|
1109
|
-
await onSave();
|
|
1110
|
-
if (field) {
|
|
1115
|
+
const outcome = await onSave();
|
|
1116
|
+
if (field && outcome !== false) {
|
|
1111
1117
|
recentlySaved.set(field, Date.now());
|
|
1112
1118
|
markSaved(field);
|
|
1113
1119
|
}
|
|
@@ -1309,24 +1315,60 @@
|
|
|
1309
1315
|
}, 190);
|
|
1310
1316
|
}
|
|
1311
1317
|
function confirmDialog(text, opts) {
|
|
1318
|
+
return choiceDialog(text, opts).then((choice) => choice === "ok");
|
|
1319
|
+
}
|
|
1320
|
+
function choiceDialog(text, opts) {
|
|
1312
1321
|
const o = opts || {};
|
|
1313
1322
|
const dialog = $("#confirm-dialog");
|
|
1314
1323
|
$("#cf-title").textContent = o.title || "Are you sure?";
|
|
1315
1324
|
$("#cf-text").textContent = text;
|
|
1316
|
-
const
|
|
1317
|
-
ok.textContent = o.okLabel || "OK";
|
|
1318
|
-
|
|
1325
|
+
const buttons = { ok: $("#cf-ok"), alt: $("#cf-alt"), cancel: $("#cf-cancel") };
|
|
1326
|
+
buttons.ok.textContent = o.okLabel || "OK";
|
|
1327
|
+
buttons.cancel.textContent = o.cancelLabel || "Cancel";
|
|
1328
|
+
buttons.alt.textContent = o.altLabel || "";
|
|
1329
|
+
buttons.alt.hidden = !o.altLabel;
|
|
1330
|
+
dialog.classList.toggle("three-way", !!o.altLabel);
|
|
1331
|
+
const primary = o.primary || "ok";
|
|
1332
|
+
for (const [name, button] of Object.entries(buttons)) button.dataset.kind = name === primary ? (o.danger ? "danger" : "primary") : "ghost";
|
|
1319
1333
|
return new Promise((resolve) => {
|
|
1320
1334
|
const done = () => {
|
|
1321
1335
|
dialog.removeEventListener("close", done);
|
|
1322
|
-
resolve(dialog.returnValue === "ok");
|
|
1336
|
+
resolve(dialog.returnValue === "ok" ? "ok" : dialog.returnValue === "alt" ? "alt" : "cancel");
|
|
1323
1337
|
};
|
|
1324
1338
|
dialog.addEventListener("close", done);
|
|
1325
1339
|
dialog.returnValue = "";
|
|
1326
1340
|
openDialog(dialog);
|
|
1327
|
-
|
|
1341
|
+
buttons[primary].focus();
|
|
1328
1342
|
});
|
|
1329
1343
|
}
|
|
1344
|
+
|
|
1345
|
+
const BRIEF_TEXT_MAX = 32000;
|
|
1346
|
+
function briefTextLimit(room) {
|
|
1347
|
+
const r = room || currentRoom();
|
|
1348
|
+
return (r && r.settings && Number(r.settings.briefTextLimit)) || 8000;
|
|
1349
|
+
}
|
|
1350
|
+
function bindCount(el, countEl, limitOf) {
|
|
1351
|
+
if (!el || !countEl) return;
|
|
1352
|
+
const tick = () => {
|
|
1353
|
+
const n = (el.isContentEditable ? rulesText(el) : el.value).length;
|
|
1354
|
+
const limit = limitOf();
|
|
1355
|
+
countEl.textContent = `${n} / ${limit}`;
|
|
1356
|
+
countEl.classList.toggle("over", n > limit);
|
|
1357
|
+
};
|
|
1358
|
+
el.addEventListener("input", tick);
|
|
1359
|
+
tick();
|
|
1360
|
+
}
|
|
1361
|
+
async function fitBriefText(text, what, limit, limitInput) {
|
|
1362
|
+
if (text.length <= limit) return text;
|
|
1363
|
+
const needed = Math.min(BRIEF_TEXT_MAX, Math.ceil(text.length / 500) * 500);
|
|
1364
|
+
const canRaise = text.length <= BRIEF_TEXT_MAX;
|
|
1365
|
+
const choice = await choiceDialog(`${what} is ${text.length} characters; this room's limit is ${limit}. Cut it at the limit, or raise the limit for this room?${canRaise ? "" : ` ${BRIEF_TEXT_MAX} is the most a room can allow.`}`, { title: "Over the room's limit", okLabel: `Cut at ${limit}`, altLabel: canRaise ? `Raise the limit to ${needed}` : "", cancelLabel: "Go back", primary: "cancel" });
|
|
1366
|
+
if (choice === "ok") return text.slice(0, limit);
|
|
1367
|
+
if (choice !== "alt") return null;
|
|
1368
|
+
if (limitInput) limitInput.value = String(needed);
|
|
1369
|
+
else await post(roomApi("/settings"), { briefTextLimit: needed });
|
|
1370
|
+
return text;
|
|
1371
|
+
}
|
|
1330
1372
|
function remember(key, value) {
|
|
1331
1373
|
try {
|
|
1332
1374
|
localStorage.setItem(`viberoom.${key}`, String(value));
|
|
@@ -2733,7 +2775,7 @@
|
|
|
2733
2775
|
${field("Vibename", `<input type="text" id="pp-name" maxlength="24" value="${esc(p.name)}">`)}
|
|
2734
2776
|
${field("Vibersona", `<input type="text" id="pp-tagline" maxlength="80" value="${esc(p.tagline || "")}" placeholder="a few words under the vibename">`, "Shown under the vibename.", "Everyone in the room sees it: you, and the other vibemates in their roster.")}
|
|
2735
2777
|
${field("Vibeface", `<div id="pp-avatar-picker"></div><input type="text" id="pp-avatar" maxlength="8" value="${esc(p.avatar || "")}" placeholder="custom emoji (optional)">`)}
|
|
2736
|
-
${field("Vibio", `<textarea id="pp-role" rows="5"
|
|
2778
|
+
${field("Vibio", `<textarea id="pp-role" rows="5" placeholder="who it is, how it speaks, what it cares about">${esc(p.role || "")}</textarea>`, `Only this vibemate reads it.<span class="count" id="pp-role-count"></span>`, "Reaches the vibemate as refreshed instructions in its brief on its next turn; its memory is kept. The other participants never see it. A vibio and the room rules go into every brief, so the room has a limit for them (the room's settings, for geeks); text over it is never cut in silence.")}
|
|
2737
2779
|
</div>
|
|
2738
2780
|
${p.trouble ? `<div class="trouble"><b>${esc(p.trouble.what)}</b><span>${esc(p.trouble.advice)}</span></div>` : ""}
|
|
2739
2781
|
${p.statusDetail && (p.status === "offline" || p.status === "error" || p.failedTurns) ? `<p class="hint" style="color:var(--danger);margin:0 4px 10px">${esc(p.statusDetail)}</p>` : ""}
|
|
@@ -2806,7 +2848,13 @@
|
|
|
2806
2848
|
renderSkillChecks($("#pp-skills"), p.skills || []);
|
|
2807
2849
|
bindSave($("#pp-skills-section"), () => post(roomApi(`/participants/${encodeURIComponent(p.id)}/persona`), { skills: checkedSkills($("#pp-skills")) }));
|
|
2808
2850
|
bindSave($("#pp-timing"), () => post(roomApi(`/participants/${encodeURIComponent(p.id)}/persona`), { replyDelay: $("#pp-delay").value === "" ? null : Number($("#pp-delay").value) }));
|
|
2809
|
-
|
|
2851
|
+
bindCount($("#pp-role"), $("#pp-role-count"), () => briefTextLimit(currentRoom()));
|
|
2852
|
+
bindSave($("#pp-persona"), async () => {
|
|
2853
|
+
const role = await fitBriefText($("#pp-role").value, `${p.name}'s vibio`, briefTextLimit(currentRoom()));
|
|
2854
|
+
if (role === null) return false;
|
|
2855
|
+
await post(roomApi(`/participants/${encodeURIComponent(p.id)}/persona`), { name: $("#pp-name").value, tagline: $("#pp-tagline").value, role, avatar: $("#pp-avatar").value });
|
|
2856
|
+
return true;
|
|
2857
|
+
});
|
|
2810
2858
|
renderConfig($("#pp-config"), p, offline);
|
|
2811
2859
|
}
|
|
2812
2860
|
|
|
@@ -2907,7 +2955,7 @@
|
|
|
2907
2955
|
${field("Emoji", `<div id="rp-emoji-picker"></div><input type="text" id="rp-emoji" maxlength="8" value="${esc(rs.emoji || "")}" placeholder="custom emoji (optional)">`, "A face for the room, next to its name.")}
|
|
2908
2956
|
${field("Topic", `<input type="text" id="rp-topic" maxlength="2000" value="${esc(rs.topic || "")}" placeholder="what this room is about (optional)">`)}
|
|
2909
2957
|
${field("Folder", `<span class="dir-row"><input type="text" id="rp-dir" maxlength="1000" value="${esc(room.dir)}" spellcheck="false">${UI.html("button", { label: "Browse", icon: "folder", kind: "ghost", id: "rp-dir-browse", title: "Choose a folder", hook: "browse-btn" })}</span>`, "Where the vibemates read and write. Changing it restarts them in the new folder; they replay the last messages.")}
|
|
2910
|
-
<div class="field mention-host"><span class="label">Room rules${geekTip("References follow renames and note when a participant has left. Rules go into every vibemate's brief as instructions, not as routing.")}</span><div id="rp-rules" class="rules-editor" contenteditable="true" spellcheck="true" data-placeholder="e.g. Everyone listens to @Pesho, he is the manager. Keep answers under 3 sentences."></div><span class="hint">One rule per line; type @ to reference a participant
|
|
2958
|
+
<div class="field mention-host"><span class="label">Room rules${geekTip("References follow renames and note when a participant has left. Rules go into every vibemate's brief as instructions, not as routing.")}</span><div id="rp-rules" class="rules-editor" contenteditable="true" spellcheck="true" data-placeholder="e.g. Everyone listens to @Pesho, he is the manager. Keep answers under 3 sentences."></div><span class="hint">One rule per line; type @ to reference a participant.<span class="count" id="rp-rules-count"></span></span><div class="mention-menu inline" id="rp-rules-menu" hidden></div></div>
|
|
2911
2959
|
${field("Language", `<input type="text" id="rp-lang" value="${esc(lang)}" placeholder="follow the human (default), or e.g. English">`)}
|
|
2912
2960
|
</div>
|
|
2913
2961
|
<div class="section">
|
|
@@ -2950,6 +2998,7 @@
|
|
|
2950
2998
|
<label class="switch"><span class="label">Show vendor and model to other vibemates</span><input type="checkbox" id="rp-vendor" ${rs.showVendorInRoster ? "checked" : ""}></label>
|
|
2951
2999
|
${field("Replay last N chat messages after a reconnect", `${UI.html("number-field", { id: "rp-replay", value: String(rs.replayAfterRestart), min: 0, max: 200 })}`)}
|
|
2952
3000
|
${field("Missed messages a vibemate reads at most on its next turn", `${UI.html("number-field", { id: "rp-backlog", value: String(rs.backlogCap), min: 1, max: 1000 })}`, "Everything posted since its last turn counts, including while it was muted; older messages are dropped with a note in its prompt.")}
|
|
3001
|
+
${field("Most characters in a vibio or in the room rules", `${UI.html("number-field", { id: "rp-text-limit", value: String(rs.briefTextLimit ?? 8000), min: 500, max: 32000, step: 500 })}`, "Both go into every brief. Text over the limit is refused with the numbers, never cut.")}
|
|
2953
3002
|
</div>`,
|
|
2954
3003
|
"tools, hops, referee, briefs",
|
|
2955
3004
|
)}
|
|
@@ -2963,6 +3012,8 @@
|
|
|
2963
3012
|
wireDetailsClose();
|
|
2964
3013
|
rulesToNodes($("#rp-rules"), room.customRulesText != null ? room.customRulesText : rs.customRules || "", room);
|
|
2965
3014
|
attachRichMentions($("#rp-rules"), $("#rp-rules-menu"));
|
|
3015
|
+
bindCount($("#rp-rules"), $("#rp-rules-count"), () => Number($("#rp-text-limit").value) || briefTextLimit(room));
|
|
3016
|
+
$("#rp-text-limit").addEventListener("input", () => $("#rp-rules").dispatchEvent(new Event("input")));
|
|
2966
3017
|
$("#rp-emoji-picker").appendChild(
|
|
2967
3018
|
emojiGrid(ROOM_EMOJI, rs.emoji || "", (emoji) => {
|
|
2968
3019
|
$("#rp-emoji").value = emoji;
|
|
@@ -2979,10 +3030,13 @@
|
|
|
2979
3030
|
if (name.trim() !== room.name) await post(roomApi("/rename"), { name });
|
|
2980
3031
|
const dir = $("#rp-dir").value.trim();
|
|
2981
3032
|
if (dir && dir !== room.dir) await post(roomApi("/dir"), { dir });
|
|
3033
|
+
const rules = await fitBriefText(rulesText($("#rp-rules")), "The room rules text", Number($("#rp-text-limit").value) || briefTextLimit(room), $("#rp-text-limit"));
|
|
3034
|
+
if (rules === null) return false;
|
|
2982
3035
|
await post(roomApi("/settings"), {
|
|
2983
3036
|
emoji: $("#rp-emoji").value,
|
|
2984
3037
|
topic: $("#rp-topic").value,
|
|
2985
|
-
customRules:
|
|
3038
|
+
customRules: rules,
|
|
3039
|
+
briefTextLimit: Number($("#rp-text-limit").value),
|
|
2986
3040
|
language: $("#rp-lang").value.trim() || "follow-human",
|
|
2987
3041
|
tools: $("#rp-tools").value,
|
|
2988
3042
|
maxSentences: $("#rp-maxlen").value === "" ? null : Number($("#rp-maxlen").value),
|
|
@@ -3131,6 +3185,7 @@
|
|
|
3131
3185
|
${field("Hop limit", `${UI.html("number-field", { id: "sp-hops", value: String(d.hopLimit), min: 0, max: 10000 })}`, "How many vibemate-to-vibemate replies may follow one message of yours before the room waits for you again.")}
|
|
3132
3186
|
${field("Full brief every N turns", `${UI.html("number-field", { id: "sp-brief-turns", value: String(d.fullBriefEveryTurns), min: 1, max: 10000 })}`, "How often a vibemate gets the whole room brief again instead of the short header.")}
|
|
3133
3187
|
${field("Full brief every N tokens", `${UI.html("number-field", { id: "sp-brief-tokens", value: String(d.fullBriefEveryTokens), min: 1000, max: 10000000, step: 1000 })}`, "…or after this much new context since its last full brief, whichever comes first.")}
|
|
3188
|
+
${field("Most characters in a vibio or in the room rules", `${UI.html("number-field", { id: "sp-text-limit", value: String(d.briefTextLimit ?? 8000), min: 500, max: 32000, step: 500 })}`, "Both go into every brief. Over the limit, the text is refused with the numbers, never cut; each room can raise or lower its own.")}
|
|
3134
3189
|
<label class="switch"><span class="label">Repeat core rules in every header<span class="hint">The short header before each turn repeats the room's core rules (who is here, how to address, how long to write).</span></span><input type="checkbox" id="sp-header-rules" ${d.headerRules ? "checked" : ""}></label>
|
|
3135
3190
|
${field("Tools", `<select id="sp-tools"><option value="on-request"${d.tools === "on-request" ? " selected" : ""}>Only when asked</option><option value="never"${d.tools === "never" ? " selected" : ""}>Never</option></select>`, "Whether vibemates may use their own tools (files, shell, web) without being asked to.")}
|
|
3136
3191
|
</div>
|
|
@@ -3386,6 +3441,7 @@
|
|
|
3386
3441
|
hopLimit: Number($("#sp-hops").value),
|
|
3387
3442
|
fullBriefEveryTurns: Number($("#sp-brief-turns").value),
|
|
3388
3443
|
fullBriefEveryTokens: Number($("#sp-brief-tokens").value),
|
|
3444
|
+
briefTextLimit: Number($("#sp-text-limit").value),
|
|
3389
3445
|
headerRules: $("#sp-header-rules").checked,
|
|
3390
3446
|
tools: $("#sp-tools").value,
|
|
3391
3447
|
},
|
|
@@ -3722,7 +3778,7 @@
|
|
|
3722
3778
|
}
|
|
3723
3779
|
els.invModelCustom.hidden = !info.modelAtLaunch;
|
|
3724
3780
|
const who = info.agentInfo && info.agentInfo.name ? `${info.agentInfo.name} ${info.agentInfo.version || ""}`.trim() : recipe.vendor;
|
|
3725
|
-
els.invStatus.textContent = parts.length ? `Options from ${who} (${parts.join(", ")}; ${(info.durationMs / 1000).toFixed(1)} s)` : `${who} exposes no config options over ACP${info.modelAtLaunch ? "; the model is a launch flag (built-in list, or type one)" : ""}.`;
|
|
3781
|
+
els.invStatus.textContent = parts.length ? `Options from ${who} (${parts.join(", ")}; ${(info.durationMs / 1000).toFixed(1)} s)` : `${who} exposes no config options over ACP${info.modelAtLaunch ? "; the model is a launch flag (built-in list, or type one)" : ""}${info.modeAtLaunch ? "; the mode is a launch flag (a change restarts the session)" : ""}.`;
|
|
3726
3782
|
} catch (error) {
|
|
3727
3783
|
if (requestId !== optionsRequest) return;
|
|
3728
3784
|
els.invStatus.textContent = `Could not read the vibemate's options (${error.message}); showing the built-in list.`;
|
|
@@ -4093,6 +4149,7 @@
|
|
|
4093
4149
|
${stpField("…or every N tokens", `${UI.html("number-field", { id: "stp-brief-tokens", value: String(st.fullBriefEveryTokens ?? 20000), min: 1000, max: 10000000, step: 1000 })}`)}
|
|
4094
4150
|
${stpField("Replay after a reconnect", `${UI.html("number-field", { id: "stp-replay", value: String(st.replayAfterRestart ?? 10), min: 0, max: 200 })}`)}
|
|
4095
4151
|
${stpField("Missed messages read at most", `${UI.html("number-field", { id: "stp-backlog", value: String(st.backlogCap ?? 50), min: 1, max: 1000 })}`)}
|
|
4152
|
+
${stpField("Most characters in a vibio or the rules", `${UI.html("number-field", { id: "stp-text-limit", value: String(st.briefTextLimit ?? 8000), min: 500, max: 32000, step: 500 })}`)}
|
|
4096
4153
|
${stpSwitch("Core rules in every header", "stp-header-rules", st.headerRules !== false)}
|
|
4097
4154
|
${stpSwitch("Show vendor and model to other vibemates", "stp-vendor", !!st.showVendorInRoster)}
|
|
4098
4155
|
</div>`;
|
|
@@ -4114,7 +4171,7 @@
|
|
|
4114
4171
|
</div></div>`).join("");
|
|
4115
4172
|
return `
|
|
4116
4173
|
<div class="stp-section"><h5>Room</h5>${room}</div>
|
|
4117
|
-
<div class="stp-section"><h5>Room rules</h5><textarea id="stp-rules" rows="5"
|
|
4174
|
+
<div class="stp-section"><h5>Room rules</h5><textarea id="stp-rules" rows="5" placeholder="one rule per line">${esc(st.customRules || "")}</textarea></div>
|
|
4118
4175
|
<div class="stp-section"><h5>Folder</h5><span class="dir-row"><input type="text" id="stp-dir" maxlength="1000" value="${esc(t.dir || "")}" spellcheck="false">${UI.html("button", { label: "Browse", icon: "folder", kind: "ghost", hook: "browse-btn", data: { stpBrowse: true } })}</span></div>
|
|
4119
4176
|
<div class="stp-section"><h5>Vibemates · <span id="stp-vm-count">${(t.vibemates || []).length}</span></h5>${vms || '<span class="hint">none</span>'}</div>`;
|
|
4120
4177
|
}
|
|
@@ -4139,7 +4196,8 @@
|
|
|
4139
4196
|
backlogCap: num("#stp-backlog"),
|
|
4140
4197
|
headerRules: $("#stp-header-rules").checked,
|
|
4141
4198
|
showVendorInRoster: $("#stp-vendor").checked,
|
|
4142
|
-
customRules: $("#stp-rules").value
|
|
4199
|
+
customRules: $("#stp-rules").value,
|
|
4200
|
+
briefTextLimit: num("#stp-text-limit"),
|
|
4143
4201
|
};
|
|
4144
4202
|
const vibemates = [...stpEls.preview.querySelectorAll(".stp-vm")].map((row) => {
|
|
4145
4203
|
const v = {};
|
package/ui/index.html
CHANGED
|
@@ -147,7 +147,7 @@
|
|
|
147
147
|
<form method="dialog">
|
|
148
148
|
<h3 id="cf-title">Are you sure?</h3>
|
|
149
149
|
<p class="lead" id="cf-text"></p>
|
|
150
|
-
<div class="actions"><button data-ui="button" data-kind="ghost" value="cancel" type="submit">Cancel</button><button data-ui="button" data-kind="primary" id="cf-ok" value="ok" type="submit">OK</button></div>
|
|
150
|
+
<div class="actions"><button data-ui="button" data-kind="ghost" id="cf-cancel" value="cancel" type="submit">Cancel</button><button data-ui="button" data-kind="ghost" id="cf-alt" value="alt" type="submit" hidden>Alt</button><button data-ui="button" data-kind="primary" id="cf-ok" value="ok" type="submit">OK</button></div>
|
|
151
151
|
</form>
|
|
152
152
|
</dialog>
|
|
153
153
|
<dialog id="room-dialog" class="dialog">
|
|
@@ -200,7 +200,7 @@
|
|
|
200
200
|
<div class="choice-row"><span class="label">Mode</span><div class="chips" data-for="inv-mode"></div><select id="inv-mode" hidden></select></div>
|
|
201
201
|
</div>
|
|
202
202
|
<div class="two-col">
|
|
203
|
-
<label>Vibename <span class="hint">(required)</span><input id="inv-name" type="text" maxlength="24" required autocomplete="off" placeholder="letters, digits, _ or -"></label>
|
|
203
|
+
<label>Vibename <span class="hint req">(required)</span><input id="inv-name" type="text" maxlength="24" required autocomplete="off" placeholder="letters, digits, _ or -"></label>
|
|
204
204
|
<label>Vibersona <span class="hint">(optional; a few words under the vibename)</span><input id="inv-tagline" type="text" maxlength="80" placeholder="e.g. curious analyst"></label>
|
|
205
205
|
</div>
|
|
206
206
|
<label>Vibeface <span class="hint">(pick one, or type any emoji)</span>
|
package/ui/theme.css
CHANGED
|
@@ -615,6 +615,7 @@ pre { background: var(--code-bg); color: var(--code-ink); padding: 10px 12px; bo
|
|
|
615
615
|
.hint { color: var(--faint); font-size: var(--fs-xs); font-weight: 600; line-height: 1.45; }
|
|
616
616
|
.hint.accent { color: var(--primary); }
|
|
617
617
|
.hint.warn { color: var(--warm-ink); }
|
|
618
|
+
.hint.req { color: var(--rose-ink); font-weight: 700; }
|
|
618
619
|
.hint.error, .error { color: var(--rose-ink); font-size: var(--fs-sm); font-weight: 700; }
|
|
619
620
|
.eyebrow { font-size: calc(11px * var(--fs-scale)); text-transform: uppercase; letter-spacing: 0.1em; color: var(--faint); font-weight: 800; }
|
|
620
621
|
.muted { color: var(--muted); }
|
|
@@ -820,6 +821,9 @@ code .open-link { color: inherit; }
|
|
|
820
821
|
.dialog .actions .spacer { flex: 1; }
|
|
821
822
|
.dialog.wide { width: 720px; }
|
|
822
823
|
.dialog.narrow { width: 440px; }
|
|
824
|
+
.dialog.narrow.three-way { width: 580px; }
|
|
825
|
+
.dialog.narrow.three-way .actions { flex-wrap: nowrap; }
|
|
826
|
+
.dialog.narrow.three-way .actions [data-ui="button"] { white-space: nowrap; }
|
|
823
827
|
.toasts { position: fixed; right: 18px; bottom: 20px; display: flex; flex-direction: column; gap: 8px; max-width: 400px; z-index: 50; pointer-events: none; }
|
|
824
828
|
.toast { pointer-events: auto; background: var(--toast-bg); color: var(--toast-ink); padding: 10px 14px; border-radius: calc(12px * var(--r-scale)); font-size: var(--fs-sm); font-weight: 700; box-shadow: var(--shadow-pop); cursor: pointer; animation: slide-in-right var(--t-slow) var(--ease-pop); line-height: 1.4; display: flex; gap: 8px; align-items: flex-start; }
|
|
825
829
|
.toast .i { width: 15px; height: 15px; margin-top: 1px; opacity: 0.85; }
|