privateboard 0.1.38 → 0.1.40

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 (63) hide show
  1. package/dist/boot.js +323 -50
  2. package/dist/boot.js.map +1 -1
  3. package/dist/cli.js +323 -50
  4. package/dist/cli.js.map +1 -1
  5. package/dist/server.js +200 -40
  6. package/dist/server.js.map +1 -1
  7. package/dist/version.d.ts +1 -1
  8. package/dist/version.js +1 -1
  9. package/dist/version.js.map +1 -1
  10. package/package.json +1 -1
  11. package/public/__avatar3d_test.html +156 -0
  12. package/public/agent-profile.css +3 -9
  13. package/public/agent-profile.js +85 -32
  14. package/public/app.js +469 -526
  15. package/public/avatar-3d-snap.js +205 -0
  16. package/public/avatar-3d.js +792 -0
  17. package/public/avatar-customizer.html +274 -0
  18. package/public/avatar3d-editor.css +240 -0
  19. package/public/avatar3d-editor.js +481 -0
  20. package/public/avatars/3d/chair.png +0 -0
  21. package/public/avatars/3d/first-principles.png +0 -0
  22. package/public/avatars/3d/historian.png +0 -0
  23. package/public/avatars/3d/long-horizon.png +0 -0
  24. package/public/avatars/3d/phenomenologist.png +0 -0
  25. package/public/avatars/3d/socrates.png +0 -0
  26. package/public/avatars/3d/user-empathy.png +0 -0
  27. package/public/avatars/3d/value-investor.png +0 -0
  28. package/public/core-avatars.js +86 -0
  29. package/public/home-3d-loader.js +15 -4
  30. package/public/home-3d-mock.js +18 -7
  31. package/public/home.html +78 -16
  32. package/public/i18n.js +12 -4
  33. package/public/icons/avatar_1779855104027.glb +0 -0
  34. package/public/icons/logo.png +0 -0
  35. package/public/icons/new-style.glb +0 -0
  36. package/public/icons/new-style2.glb +0 -0
  37. package/public/icons/new-style3.glb +0 -0
  38. package/public/icons/new-style4.glb +0 -0
  39. package/public/icons/new-style5.glb +0 -0
  40. package/public/icons/office.glb +0 -0
  41. package/public/icons/stuff.glb +0 -0
  42. package/public/index.html +148 -121
  43. package/public/new-agent.js +46 -20
  44. package/public/office-viewer.html +340 -0
  45. package/public/stuff-viewer.html +330 -0
  46. package/public/thread.css +8 -8
  47. package/public/user-settings.css +10 -13
  48. package/public/user-settings.js +86 -78
  49. package/public/vendor/BufferGeometryUtils.js +1434 -0
  50. package/public/vendor/DRACOLoader.js +739 -0
  51. package/public/vendor/GLTFLoader.js +4860 -0
  52. package/public/vendor/RoomEnvironment.js +185 -0
  53. package/public/vendor/SkeletonUtils.js +496 -0
  54. package/public/vendor/draco/draco_decoder.js +34 -0
  55. package/public/vendor/draco/draco_decoder.wasm +0 -0
  56. package/public/vendor/draco/draco_encoder.js +33 -0
  57. package/public/vendor/draco/draco_wasm_wrapper.js +117 -0
  58. package/public/vendor/meshopt_decoder.module.js +196 -0
  59. package/public/voice-3d-banner.js +12 -0
  60. package/public/voice-3d.js +1407 -432
  61. package/public/voice-replay.js +21 -0
  62. package/public/avatar-skill.js +0 -629
  63. package/public/icons/folded-sidebar.png +0 -0
package/public/index.html CHANGED
@@ -1107,68 +1107,19 @@
1107
1107
  mask here also overrides the generic .mini-btn::before, which —
1108
1108
  lacking an --icon — would otherwise paint a solid square. */
1109
1109
  .mini-logo { position: relative; }
1110
- /* The chair avatar is a two-layer stack: open-eye frame always
1111
- visible, closed-eye frame flashed in by the blink keyframe. The
1112
- wrapper carries an occasional idle hop. */
1113
- .mini-logo-av {
1114
- position: relative;
1115
- width: 26px;
1116
- height: 26px;
1117
- transition: opacity 0.12s;
1118
- animation: chair-hop 7s ease-in-out infinite;
1119
- }
1120
- .mini-logo-av img {
1121
- position: absolute;
1122
- inset: 0;
1123
- width: 100%;
1124
- height: 100%;
1125
- image-rendering: pixelated;
1126
- image-rendering: crisp-edges;
1127
- }
1128
- /* Closed-eye frame · hidden at rest, briefly opaque on the blink
1129
- keyframe. A different cycle length from the hop keeps the two from
1130
- locking into a robotic sync. */
1131
- .mini-logo-av .cl-blink {
1132
- opacity: 0;
1133
- animation: chair-blink 5.7s ease-in-out infinite;
1134
- }
1135
- .mini-logo:hover .mini-logo-av { opacity: 0; }
1136
- /* Occasional double-bounce hop · still ~85% of the cycle, then a
1137
- quick two-step hop so the chair reads as "alive" without being
1138
- busy. */
1139
- @keyframes chair-hop {
1140
- 0%, 60%, 100% { transform: translateY(0); }
1141
- 66% { transform: translateY(-4px); }
1142
- 72% { transform: translateY(0); }
1143
- 77% { transform: translateY(-2px); }
1144
- 82% { transform: translateY(0); }
1145
- }
1146
- /* Single crisp blink near the end of the cycle (~170ms closed). */
1147
- @keyframes chair-blink {
1148
- 0%, 93.5%, 100% { opacity: 0; }
1149
- 94%, 96.5% { opacity: 1; }
1150
- 97% { opacity: 0; }
1151
- }
1152
- /* Respect reduced-motion · hold the open-eye frame, no hop / blink. */
1153
- @media (prefers-reduced-motion: reduce) {
1154
- .mini-logo-av,
1155
- .mini-logo-av .cl-blink { animation: none; }
1156
- }
1110
+ /* Fold glyph · same Lucide PanelLeft mark the in-sidebar collapse
1111
+ button uses. Always visible (no chair avatar / hover swap),
1112
+ mirrored on X so the rows flip to the right edge — telegraphs
1113
+ "the panel expands out from here". */
1157
1114
  .mini-logo::before {
1158
1115
  content: "";
1159
1116
  position: absolute;
1160
1117
  top: 50%;
1161
1118
  left: 50%;
1162
- /* scaleX(-1) mirrors the fold so the rows flip to the right edge —
1163
- the same direction the in-sidebar collapse button uses in its
1164
- collapsed state to telegraph "the panel expands out from here". */
1165
1119
  transform: translate(-50%, -50%) scaleX(-1);
1166
1120
  -webkit-mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><rect x='3' y='3' width='18' height='18' rx='2'/><path d='M9 3v18'/><path d='M5 8h2'/><path d='M5 12h2'/><path d='M5 16h2'/></svg>");
1167
1121
  mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><rect x='3' y='3' width='18' height='18' rx='2'/><path d='M9 3v18'/><path d='M5 8h2'/><path d='M5 12h2'/><path d='M5 16h2'/></svg>");
1168
- opacity: 0;
1169
- transition: opacity 0.12s;
1170
1122
  }
1171
- .mini-logo:hover::before { opacity: 1; }
1172
1123
  /* Divider between the action group and the rooms/agents switchers. */
1173
1124
  .mini-sep {
1174
1125
  width: 20px;
@@ -2059,11 +2010,11 @@
2059
2010
  position: relative;
2060
2011
  overflow: hidden;
2061
2012
  }
2062
- /* Pixel-art variant · when prefs.avatarSeed exists, app.renderUserBlock
2063
- swaps the initial-letter chip for an AvatarSkill SVG. The pixel SVG
2064
- is transparent, so we drop the fill entirely (was --lime, then
2065
- --bg) and let it sit directly on the surface behind it no boxed
2066
- square around the character. */
2013
+ /* 3D portrait variant · when prefs.avatarSeed exists,
2014
+ app.renderUserBlock swaps the initial-letter chip for the
2015
+ Avatar3DSnap rendered PNG. The image fills the chip; we drop the
2016
+ filled background so the portrait reads cleanly against the
2017
+ surface behind it. */
2067
2018
  .user-av.has-pixel-av {
2068
2019
  background: transparent;
2069
2020
  }
@@ -2229,6 +2180,17 @@
2229
2180
  and leaving an empty band below. */
2230
2181
  .main-view[hidden] { display: none !important; }
2231
2182
 
2183
+ /* Boot guard · on refresh, the sidebar restore tick has to wait for
2184
+ the agent rows to mount before openAgentProfile fires, but app.js's
2185
+ handleRoute paints the new-room composer into the room view straight
2186
+ away on an empty hash. Mask the room view while a saved agent profile
2187
+ is being restored so the composer doesn't flash under the profile.
2188
+ The guard class is set SYNCHRONOUSLY during parse (top of the sidebar
2189
+ IIFE, before app.js's deferred init runs) and cleared the moment the
2190
+ profile opens or the restore gives up. `!important` beats app.js's
2191
+ own removeAttribute("hidden") on the room view during that window. */
2192
+ html.bb-restoring-agent .main-view[data-main-view="room"] { display: none !important; }
2193
+
2232
2194
  /* ─── All Reports · clean vertical reading list ──────────────────
2233
2195
  Single-hierarchy archive view · header → filter chips → date-
2234
2196
  grouped reading rows. Each row uses pure typography (kicker /
@@ -4202,6 +4164,34 @@
4202
4164
  letter-spacing: 0.1em;
4203
4165
  }
4204
4166
  .resume-btn:hover { background: var(--bg); color: var(--lime); }
4167
+ /* Stop voice replay · same compact mono chrome as pause/resume, but in
4168
+ red so it reads as a distinct "terminate playback" action. Rendered
4169
+ by app.js only while a replay is active in this room, and it sits in
4170
+ the pause/resume slot (those are hidden on adjourned rooms, where
4171
+ replay runs) so it lands exactly where the eye looks for transport. */
4172
+ .replay-stop-btn {
4173
+ padding: 4px 10px;
4174
+ background: transparent;
4175
+ color: var(--text);
4176
+ border: 0.5px solid var(--red, #B5706A);
4177
+ font-family: var(--mono);
4178
+ font-size: 10px;
4179
+ font-weight: 700;
4180
+ cursor: pointer;
4181
+ text-decoration: none;
4182
+ text-transform: uppercase;
4183
+ letter-spacing: 0.1em;
4184
+ transition: all 0.12s;
4185
+ }
4186
+ .replay-stop-btn .replay-stop-icon {
4187
+ color: var(--red, #B5706A);
4188
+ margin: 0 1px;
4189
+ letter-spacing: -0.05em;
4190
+ }
4191
+ .replay-stop-btn:hover {
4192
+ background: rgba(181, 112, 106, 0.12);
4193
+ color: var(--red, #B5706A);
4194
+ }
4205
4195
  /* ─── Empty state ───
4206
4196
  No room loaded (zero-room first-run, or after a navigation). The
4207
4197
  starter panel renders inside .chat; the input bar, speaking queue,
@@ -7357,11 +7347,10 @@
7357
7347
  border: 0.5px solid var(--line-strong);
7358
7348
  font-family: var(--mono);
7359
7349
  }
7360
- /* Pixel-art variant · when prefs.avatarSeed is set, renderChat swaps
7361
- the initial-letter chip for the AvatarSkill SVG (same seed the
7362
- sidebar foot + preference overlay use). The SVG provides its own
7363
- surface, so drop the border + bg + flex centering and let it fill
7364
- the chip edge-to-edge. */
7350
+ /* Legacy pixel-av variant · the renderChat path no longer mounts
7351
+ `msg-av-pixel` (Avatar3DSnap PNG fills `<img class="msg-av">`
7352
+ directly), but the CSS is kept for any third-party plugins that
7353
+ still write into that node. */
7365
7354
  .msg.user .msg-av.msg-av-pixel {
7366
7355
  border: none;
7367
7356
  background: var(--bg);
@@ -7547,7 +7536,10 @@
7547
7536
  -webkit-box-orient: vertical;
7548
7537
  overflow: hidden;
7549
7538
  }
7550
- .msg-time { color: var(--text-faint); margin-left: auto; }
7539
+ /* Flows inline right after the badges (no longer pinned far-right via
7540
+ margin-left:auto) so the far-right end of the meta line is free for
7541
+ the hover thread toolbar to sit at the row's end. */
7542
+ .msg-time { color: var(--text-faint); }
7551
7543
 
7552
7544
  .msg-bubble {
7553
7545
  padding: 0;
@@ -8480,23 +8472,16 @@
8480
8472
  --rt-bottom-reserve: 130px;
8481
8473
  }
8482
8474
 
8483
- /* ─── brainstorm · SOIL + GRASS floor ────────────────────────
8484
- 128×128 tileable terrain · grass base with two irregular dirt
8485
- patches placed inside the tile bounds so when the pattern
8486
- repeats the patches read as organic "islands" of bare earth in
8487
- a grass field rather than a gridded indoor layout. Subtle
8488
- tonal blobs in the grass (darker / lighter) break the flat
8489
- fill; scattered 1×2 lighter pixels read as grass blades
8490
- catching light; pebbles + dust spots inside the dirt patches
8491
- add ground-texture detail. JRPG outdoor-tile vocabulary for
8492
- the "yes-and" creative tone — open ground, room to grow.
8493
-
8494
- Replaces the earlier muted-earth grid mosaic which still read
8495
- as an indoor floor pattern even at the larger tile size. */
8475
+ /* ─── brainstorm · WARM-OAK PLANK floor ───────────────────────
8476
+ 128×64 light warm-oak planks · alternating plank tones, darker
8477
+ seams, staggered plank-end joints, a soft catch-light under each
8478
+ seam, and a few faint grain dashes. Pairs with the 3D room's cozy
8479
+ modern interior (voice-3d.js · brainstorm = window / chest / sage
8480
+ sofa) so the directors sit on a warm wood floor. Bright + cozy. */
8496
8481
  .roundtable-stage[data-floor="brainstorm"] {
8497
- --floor-bg: #5E6B47;
8482
+ --floor-bg: #B0976E;
8498
8483
  --rt-chair-name: var(--lime, #6FB572);
8499
- --floor-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='128' height='128' shape-rendering='crispEdges'><rect width='128' height='128' fill='%235E6B47'/><rect x='0' y='8' width='20' height='16' fill='%234F5C3D'/><rect x='104' y='20' width='20' height='20' fill='%234F5C3D'/><rect x='88' y='64' width='24' height='16' fill='%234F5C3D'/><rect x='0' y='64' width='12' height='20' fill='%234F5C3D'/><rect x='44' y='64' width='16' height='8' fill='%234F5C3D'/><rect x='72' y='8' width='12' height='8' fill='%236E7A52'/><rect x='120' y='80' width='8' height='12' fill='%236E7A52'/><rect x='48' y='84' width='8' height='8' fill='%236E7A52'/><rect x='48' y='24' width='32' height='32' fill='%235C4838'/><rect x='44' y='28' width='4' height='24' fill='%235C4838'/><rect x='80' y='28' width='4' height='24' fill='%235C4838'/><rect x='52' y='20' width='24' height='4' fill='%235C4838'/><rect x='52' y='56' width='20' height='4' fill='%235C4838'/><rect x='56' y='16' width='12' height='4' fill='%235C4838'/><rect x='56' y='32' width='8' height='8' fill='%234A3A28'/><rect x='68' y='40' width='4' height='8' fill='%234A3A28'/><rect x='60' y='24' width='4' height='4' fill='%236E5A48'/><rect x='68' y='48' width='4' height='4' fill='%236E5A48'/><rect x='58' y='44' width='2' height='2' fill='%236B6258'/><rect x='72' y='32' width='2' height='2' fill='%236B6258'/><rect x='50' y='38' width='2' height='2' fill='%236B6258'/><rect x='16' y='96' width='24' height='20' fill='%235C4838'/><rect x='12' y='100' width='4' height='12' fill='%235C4838'/><rect x='40' y='100' width='4' height='12' fill='%235C4838'/><rect x='20' y='92' width='16' height='4' fill='%235C4838'/><rect x='22' y='104' width='8' height='4' fill='%234A3A28'/><rect x='20' y='100' width='4' height='4' fill='%236E5A48'/><rect x='24' y='108' width='2' height='2' fill='%236B6258'/><rect x='32' y='98' width='2' height='2' fill='%236B6258'/><rect x='8' y='40' width='1' height='2' fill='%238FA068'/><rect x='24' y='72' width='1' height='2' fill='%238FA068'/><rect x='104' y='56' width='1' height='2' fill='%238FA068'/><rect x='120' y='104' width='1' height='2' fill='%238FA068'/><rect x='88' y='44' width='1' height='2' fill='%238FA068'/><rect x='4' y='88' width='1' height='2' fill='%238FA068'/><rect x='64' y='88' width='1' height='2' fill='%238FA068'/><rect x='92' y='24' width='1' height='2' fill='%238FA068'/><rect x='44' y='62' width='1' height='2' fill='%238FA068'/><rect x='80' y='120' width='1' height='2' fill='%238FA068'/><rect x='12' y='12' width='1' height='2' fill='%238FA068'/><rect x='112' y='60' width='1' height='2' fill='%238FA068'/></svg>");
8484
+ --floor-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='128' height='64' shape-rendering='crispEdges'><rect width='128' height='64' fill='%23B0976E'/><rect y='16' width='128' height='16' fill='%23A68C62'/><rect y='48' width='128' height='16' fill='%23A68C62'/><rect y='0' width='128' height='1' fill='%23C2A87E'/><rect y='16' width='128' height='1' fill='%23C2A87E'/><rect y='32' width='128' height='1' fill='%23C2A87E'/><rect y='48' width='128' height='1' fill='%23C2A87E'/><rect y='15' width='128' height='1' fill='%23877045'/><rect y='31' width='128' height='1' fill='%23877045'/><rect y='47' width='128' height='1' fill='%23877045'/><rect y='63' width='128' height='1' fill='%23877045'/><rect x='40' y='0' width='1' height='16' fill='%23877045'/><rect x='96' y='16' width='1' height='16' fill='%23877045'/><rect x='20' y='32' width='1' height='16' fill='%23877045'/><rect x='72' y='48' width='1' height='16' fill='%23877045'/><rect x='10' y='6' width='14' height='1' fill='%23A38755'/><rect x='60' y='22' width='18' height='1' fill='%23A38755'/><rect x='100' y='40' width='14' height='1' fill='%23A38755'/><rect x='30' y='56' width='16' height='1' fill='%23A38755'/></svg>");
8500
8485
  }
8501
8486
  /* ─── constructive · BLACK-WALNUT STAGGERED PLANK ─────────────
8502
8487
  192×192 tileable boardroom floor · 8 rows of horizontal black-
@@ -8555,10 +8540,16 @@
8555
8540
  arms in antique brass, polished-gold corner dots. Smaller tile
8556
8541
  than the burgundy carpet so the pattern reads as dense metallic
8557
8542
  weave at room scale (executive boardroom polish). */
8543
+ /* ─── critique · DEEP-GREEN WOOL CARPET ───────────────────────
8544
+ 64×64 near-solid deep forest-green carpet · only a whisper of
8545
+ low-contrast wool flecks (texture, not pattern). Replaced the
8546
+ busy brass-on-gunmetal grid, which read too loud. Deep green is
8547
+ the classic gentleman's-study pairing for the mahogany +
8548
+ brass executive panel walls — calm, grounding, high-end. */
8558
8549
  .roundtable-stage[data-floor="critique"] {
8559
- --floor-bg: #3E362C;
8550
+ --floor-bg: #2F3A30;
8560
8551
  --rt-chair-name: #E8C788;
8561
- --floor-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' shape-rendering='crispEdges'><rect width='16' height='16' fill='%233E362C'/><rect x='6' y='7' width='4' height='2' fill='%23BA9050'/><rect x='7' y='6' width='2' height='4' fill='%23BA9050'/><rect x='7' y='2' width='2' height='1' fill='%239F7C42'/><rect x='7' y='13' width='2' height='1' fill='%239F7C42'/><rect x='2' y='7' width='1' height='2' fill='%239F7C42'/><rect x='13' y='7' width='1' height='2' fill='%239F7C42'/><rect x='1' y='1' width='1' height='1' fill='%23E8C788'/><rect x='14' y='1' width='1' height='1' fill='%23E8C788'/><rect x='1' y='14' width='1' height='1' fill='%23E8C788'/><rect x='14' y='14' width='1' height='1' fill='%23E8C788'/></svg>");
8552
+ --floor-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='64' height='64' shape-rendering='crispEdges'><rect width='64' height='64' fill='%232F3A30'/><rect x='6' y='8' width='1' height='1' fill='%233C493C'/><rect x='24' y='5' width='1' height='1' fill='%233C493C'/><rect x='42' y='12' width='1' height='1' fill='%233C493C'/><rect x='58' y='7' width='1' height='1' fill='%233C493C'/><rect x='14' y='26' width='1' height='1' fill='%233C493C'/><rect x='34' y='22' width='1' height='1' fill='%233C493C'/><rect x='50' y='30' width='1' height='1' fill='%233C493C'/><rect x='9' y='44' width='1' height='1' fill='%233C493C'/><rect x='28' y='50' width='1' height='1' fill='%233C493C'/><rect x='46' y='46' width='1' height='1' fill='%233C493C'/><rect x='56' y='56' width='1' height='1' fill='%233C493C'/><rect x='18' y='60' width='1' height='1' fill='%233C493C'/><rect x='12' y='14' width='1' height='1' fill='%23283227'/><rect x='30' y='9' width='1' height='1' fill='%23283227'/><rect x='48' y='6' width='1' height='1' fill='%23283227'/><rect x='60' y='18' width='1' height='1' fill='%23283227'/><rect x='8' y='34' width='1' height='1' fill='%23283227'/><rect x='36' y='30' width='1' height='1' fill='%23283227'/><rect x='52' y='40' width='1' height='1' fill='%23283227'/><rect x='20' y='52' width='1' height='1' fill='%23283227'/><rect x='40' y='58' width='1' height='1' fill='%23283227'/><rect x='26' y='36' width='1' height='1' fill='%23283227'/><rect x='54' y='24' width='1' height='1' fill='%23283227'/><rect x='15' y='42' width='1' height='1' fill='%23283227'/></svg>");
8562
8553
  }
8563
8554
 
8564
8555
  /* 8-bit pixel-art conference table · sized to occupy the
@@ -9360,8 +9351,8 @@
9360
9351
 
9361
9352
  /* Initial-letter avatar chip · fallback when the user has no
9362
9353
  `prefs.avatarSeed` configured. Mirrors the sidebar foot's
9363
- pre-AvatarSkill chip so the seat reads as "the user, no
9364
- custom avatar" rather than empty space. */
9354
+ placeholder chip so the seat reads as "the user, no custom
9355
+ avatar" rather than empty space. */
9365
9356
  .rt-avatar-user.rt-avatar-initial {
9366
9357
  display: inline-flex;
9367
9358
  align-items: center;
@@ -9374,14 +9365,14 @@
9374
9365
  font-weight: 700;
9375
9366
  text-transform: uppercase;
9376
9367
  }
9377
- /* Pixel-art avatar wrapper · when an avatar seed IS present, the
9378
- SVG is injected by AvatarSkill; this wrapper just constrains it
9379
- to the same box as a regular `<img class="rt-avatar">`. The
9380
- wrapper itself stays transparent — the AvatarSkill SVG carries
9381
- its own surface, and the chair sprite shows through any
9382
- letterboxed area (the SVG is square, the seat-avatar box is
9383
- portrait 48×56, so without transparent bg a dark block paints
9384
- above + below the SVG). */
9368
+ /* Portrait avatar wrapper · when an avatar seed IS present, an
9369
+ Avatar3DSnap PNG is injected by app.renderRoundTable; this
9370
+ wrapper constrains it to the same box as a regular
9371
+ `<img class="rt-avatar">`. The wrapper itself stays transparent
9372
+ — the rendered PNG carries its own surface, and the chair
9373
+ sprite shows through any letterboxed area (the snap is square,
9374
+ the seat-avatar box is portrait 48×56, so without transparent
9375
+ bg a dark block paints above + below the image). */
9385
9376
  .rt-avatar-user.has-pixel-av {
9386
9377
  display: flex;
9387
9378
  align-items: center;
@@ -11422,14 +11413,14 @@
11422
11413
  display: grid !important;
11423
11414
  align-content: center !important;
11424
11415
  justify-items: stretch !important;
11425
- /* `.chat` reserves 140 px of bottom padding for the floating
11426
- `.ib-stack` (input-bar + speaking-queue strips). The
11427
- new-room / new-agent composers don't mount an `.ib-stack`
11428
- the composer's own `.cmp-toolbar` IS the input row — so
11429
- inheriting 140 px just pushes the composer off-centre by
11430
- ~70 px. Reset to a balanced gutter that matches the top
11431
- padding for visual symmetry. */
11432
- padding-bottom: 14px !important;
11416
+ /* `.chat` carries gutter padding meant for the room view (top
11417
+ clears the frosted `.room-head`, bottom reserves space for
11418
+ the floating `.ib-stack`). Neither applies to the new-room /
11419
+ new-agent composers `.room-head:empty` and there's no
11420
+ `.ib-stack`, so the inherited padding just throws off centring
11421
+ and steals horizontal width from the cmp card. Zero it out;
11422
+ `.cmp` carries its own breathing room. */
11423
+ padding: 0 !important;
11433
11424
  }
11434
11425
  .chat.chat--composer > [data-chat-messages] {
11435
11426
  width: 100% !important;
@@ -11450,8 +11441,11 @@
11450
11441
  `50vh - 110px` puts the hero+input vertical midpoint at 50vh.
11451
11442
  `max(32px, …)` keeps the original padding floor on tiny viewports
11452
11443
  where the calc would go negative.
11453
- Toggled by JS (updateComposerOverflow) on render + resize. */
11454
- .chat.chat--composer.chat--composer-overflow {
11444
+ Toggled by JS (updateComposerOverflow) on render + resize.
11445
+ Scoped to NOT apply when the new-agent composer (`.ag-cmp`) is
11446
+ mounted — the agent composer keeps the default grid-centre
11447
+ layout regardless of natural height (per user spec). */
11448
+ .chat.chat--composer.chat--composer-overflow:not(:has(.ag-cmp)) {
11455
11449
  display: block !important;
11456
11450
  align-content: initial !important;
11457
11451
  overflow-y: auto;
@@ -12322,7 +12316,7 @@
12322
12316
  .cmp-celeb-card {
12323
12317
  position: relative;
12324
12318
  display: grid;
12325
- grid-template-columns: 56px 1fr;
12319
+ grid-template-columns: 64px 1fr;
12326
12320
  gap: 14px;
12327
12321
  align-items: stretch;
12328
12322
  width: 100%;
@@ -12343,12 +12337,8 @@
12343
12337
  display: flex;
12344
12338
  align-items: center;
12345
12339
  justify-content: center;
12346
- width: 56px;
12347
- height: 56px;
12348
- border-radius: 4px;
12349
- overflow: hidden;
12350
- background: var(--panel);
12351
- border: 1px solid var(--line);
12340
+ width: 64px;
12341
+ height: 64px;
12352
12342
  }
12353
12343
  .cmp-celeb-img {
12354
12344
  width: 100%;
@@ -15764,11 +15754,18 @@
15764
15754
  <script src="room-settings.js" defer></script>
15765
15755
  <link rel="stylesheet" href="adjourn-overlay.css">
15766
15756
  <!-- Avatar skill must load before any UI that calls it (new-agent, user-settings). -->
15767
- <script src="avatar-skill.js" defer></script>
15757
+ <!-- avatar-3d-snap · shared head-and-shoulders 3D portrait helper
15758
+ (replaces the retired AvatarSkill 8-bit SVG generator). Loads as
15759
+ a classic script so window.Avatar3DSnap is available before app.js
15760
+ init(). Internally lazy-loads three.js + avatar-3d.js only when a
15761
+ portrait is actually requested. -->
15762
+ <script src="avatar-3d-snap.js" defer></script>
15768
15763
  <link rel="stylesheet" href="agent-profile.css">
15769
15764
  <script src="agent-profile.js" defer></script>
15770
15765
  <link rel="stylesheet" href="new-agent.css">
15771
15766
  <script src="new-agent.js" defer></script>
15767
+ <link rel="stylesheet" href="avatar3d-editor.css">
15768
+ <script type="module" src="avatar3d-editor.js"></script>
15772
15769
  <link rel="stylesheet" href="user-settings.css">
15773
15770
  <script type="module" src="keys-store.js"></script>
15774
15771
  <script src="key-validators.js" defer></script>
@@ -15778,6 +15775,7 @@
15778
15775
  <script src="mention-picker.js" defer></script>
15779
15776
  <script src="auto-hide-scroll.js" defer></script>
15780
15777
  <link rel="stylesheet" href="onboarding.css">
15778
+ <script src="core-avatars.js" defer></script>
15781
15779
  <script src="voice-3d-banner.js" defer></script>
15782
15780
  <script src="onboarding.js" defer></script>
15783
15781
  <link rel="stylesheet" href="voice-onboarding.css">
@@ -15795,9 +15793,9 @@
15795
15793
  <!-- voice-3d · voxel pixel-art 3D round-table renderer. ES module
15796
15794
  (imports three.js from /vendor/three.module.min.js). Loads in
15797
15795
  parallel with app.js · no ordering dependency, app.js feature-
15798
- detects window.VoiceStage3D at render time. Toggle in user-
15799
- settings (or localStorage["boardroom.stage3d"]) decides whether
15800
- to use it · default on, falls back to the legacy 2D SVG path. -->
15796
+ detects window.VoiceStage3D at render time. The voice room is
15797
+ 3D-only as of 2026-05; the legacy 2D SVG stage + the user-
15798
+ settings toggle have been retired. -->
15801
15799
  <script type="module" src="voice-3d.js"></script>
15802
15800
  <!-- voice-recorder · meeting capture. Lazy module · only allocates
15803
15801
  anything on first start(). Loaded BEFORE app.js so the global
@@ -16078,12 +16076,7 @@
16078
16076
  sidebar uses); only the logo + rooms + agents expand. -->
16079
16077
  <aside class="mini-sidebar" aria-label="Collapsed sidebar navigation">
16080
16078
  <div class="mini-top">
16081
- <button type="button" class="mini-btn mini-logo" data-sidebar-expand data-i18n-tip="sidebar_expand" data-tip="Expand sidebar" aria-label="Expand sidebar">
16082
- <span class="mini-logo-av">
16083
- <img class="cl-open" src="/avatars/chair.svg" alt="" aria-hidden="true">
16084
- <img class="cl-blink" src="/avatars/chair-blink.svg" alt="" aria-hidden="true">
16085
- </span>
16086
- </button>
16079
+ <button type="button" class="mini-btn mini-logo" data-sidebar-expand data-i18n-tip="sidebar_expand" data-tip="Expand sidebar" aria-label="Expand sidebar"></button>
16087
16080
  <button type="button" class="mini-btn mini-new-room" data-convene-trigger data-i18n-tip="sidebar_new_room" data-tip="New room" aria-label="New room"></button>
16088
16081
  <button type="button" class="mini-btn mini-new-agent" data-agent-composer-trigger data-i18n-tip="sidebar_new_agent" data-tip="New agent" aria-label="New agent"></button>
16089
16082
  <button type="button" class="mini-btn mini-search" data-search-trigger data-tip="Search" aria-label="Search"></button>
@@ -17018,6 +17011,22 @@
17018
17011
  const lsGet = (k) => { try { return localStorage.getItem(k); } catch (_) { return null; } };
17019
17012
  const lsSet = (k, v) => { try { localStorage.setItem(k, v); } catch (_) {} };
17020
17013
 
17014
+ /* Anti-flash boot guard · runs SYNCHRONOUSLY during parse, before
17015
+ app.js's deferred init paints the new-room composer. If this
17016
+ refresh will restore a saved agent profile, mask the room view
17017
+ now (see the `.bb-restoring-agent` CSS rule) so the composer never
17018
+ flashes under the profile. Cleared by clearAgentRestoreGuard once
17019
+ the profile opens or the restore gives up. */
17020
+ if (lsGet(TAB_KEY) === "agents") {
17021
+ const _savedAgent = lsGet(AGENTS_KEY);
17022
+ if (_savedAgent && _savedAgent !== "new") {
17023
+ document.documentElement.classList.add("bb-restoring-agent");
17024
+ }
17025
+ }
17026
+ function clearAgentRestoreGuard() {
17027
+ document.documentElement.classList.remove("bb-restoring-agent");
17028
+ }
17029
+
17021
17030
  /* Truth source for "does this room exist?" is `window.app.rooms`,
17022
17031
  not the DOM. The sidebar may not have rendered yet on first
17023
17032
  restore (app.init's loadInitial is async and runs concurrently
@@ -17124,14 +17133,25 @@
17124
17133
 
17125
17134
  function applyAgentsSubState(sub) {
17126
17135
  if (!sub || sub === "new") {
17136
+ clearAgentRestoreGuard();
17127
17137
  if (window.app && typeof window.app.setComposerMode === "function") {
17128
17138
  window.app.setComposerMode("agent");
17129
17139
  }
17130
17140
  return;
17131
17141
  }
17132
17142
  if (typeof window.openAgentProfile === "function") {
17133
- try { window.openAgentProfile(sub); }
17143
+ try {
17144
+ window.openAgentProfile(sub);
17145
+ // Drop the boot guard only once the profile view is actually
17146
+ // visible. open() early-returns (leaving the agent view
17147
+ // hidden) when the agent record hasn't loaded yet — in that
17148
+ // case the retry tick calls us again and the room view stays
17149
+ // masked until the profile genuinely opens.
17150
+ const agentView = document.querySelector('[data-main-view="agent"]');
17151
+ if (agentView && !agentView.hasAttribute("hidden")) clearAgentRestoreGuard();
17152
+ }
17134
17153
  catch (_) {
17154
+ clearAgentRestoreGuard();
17135
17155
  lsSet(AGENTS_KEY, "new");
17136
17156
  if (window.app && typeof window.app.setComposerMode === "function") {
17137
17157
  window.app.setComposerMode("agent");
@@ -17402,10 +17422,17 @@
17402
17422
  const tick = () => {
17403
17423
  attempts += 1;
17404
17424
  const id = lsGet(AGENTS_KEY);
17405
- if (!id || id === "new") return;
17425
+ if (!id || id === "new") { clearAgentRestoreGuard(); return; }
17406
17426
  const haveAny = document.querySelector(".agents-scroll .agent-row[data-agent-profile]");
17407
- if (haveAny) { applyAgentsSubState(id); return; }
17427
+ if (haveAny) {
17428
+ applyAgentsSubState(id);
17429
+ // If the profile opened, the guard is already cleared and we
17430
+ // stop. If open() couldn't resolve the agent yet (guard still
17431
+ // set), keep retrying instead of leaving the room view masked.
17432
+ if (!document.documentElement.classList.contains("bb-restoring-agent")) return;
17433
+ }
17408
17434
  if (attempts < 10) setTimeout(tick, 250);
17435
+ else clearAgentRestoreGuard(); // give up · reveal the composer
17409
17436
  };
17410
17437
  setTimeout(tick, 250);
17411
17438
  }
@@ -141,12 +141,15 @@
141
141
  const rng = makeRng(seed);
142
142
  return [0,0,0,0].map(() => Math.floor(rng() * 16).toString(16)).join("");
143
143
  }
144
- // Avatar generation delegates to the shared AvatarSkill
145
- // (see public/avatar-skill.js). One source of truth for the
146
- // 8-bit pixel-art look used here, in user settings, and anywhere
147
- // else that wants a director-style avatar.
148
- function generateAvatar(seed, opts) {
149
- return window.AvatarSkill.generate(seed, opts);
144
+ // Avatar generation delegates to the shared Avatar3DSnap helper
145
+ // (public/avatar-3d-snap.js). One source of truth for the 3D
146
+ // voxel head-and-shoulders portrait used everywhere a director
147
+ // face appears. The legacy AvatarSkill 8-bit SVG generator was
148
+ // retired in favour of this 3D pipeline.
149
+ async function generateAvatarDataUrl(seed) {
150
+ const snap = window.Avatar3DSnap;
151
+ if (!snap || typeof snap.generate !== "function") return "";
152
+ return snap.generate(seed);
150
153
  }
151
154
 
152
155
  function modalHTML() {
@@ -612,16 +615,37 @@
612
615
  if (!frame) return;
613
616
  if (avatarState.placeholder) {
614
617
  frame.classList.add("placeholder");
615
- frame.innerHTML = generateAvatar("__placeholder__", { placeholder: true });
618
+ // Empty frame · the wrapper's CSS placeholder (initial letter
619
+ // / texture) is the resting state until the user clicks
620
+ // regenerate. The legacy 8-bit SVG silhouette was retired.
621
+ frame.innerHTML = "";
616
622
  if (seedEl) seedEl.textContent = "—";
617
623
  if (rollEl) rollEl.textContent = "";
618
- } else {
619
- frame.classList.remove("placeholder");
620
- const seedKey = avatarState.seed + "::" + avatarState.roll;
621
- frame.innerHTML = generateAvatar(seedKey);
622
- if (seedEl) seedEl.textContent = shortHash(avatarState.seed);
623
- if (rollEl) rollEl.textContent = " · #" + avatarState.roll;
624
+ return;
625
+ }
626
+ frame.classList.remove("placeholder");
627
+ const seedKey = avatarState.seed + "::" + avatarState.roll;
628
+ if (seedEl) seedEl.textContent = shortHash(avatarState.seed);
629
+ if (rollEl) rollEl.textContent = " · #" + avatarState.roll;
630
+ const snap = window.Avatar3DSnap;
631
+ const cached = snap && typeof snap.cacheGet === "function" ? snap.cacheGet(seedKey) : null;
632
+ if (cached) {
633
+ frame.innerHTML = `<img src="${cached}" alt="">`;
634
+ return;
624
635
  }
636
+ frame.innerHTML = '<div class="na-avatar-loading" aria-hidden="true">…</div>';
637
+ if (!snap || typeof snap.generate !== "function") return;
638
+ const expected = seedKey;
639
+ snap.generate(seedKey).then((dataUrl) => {
640
+ if (!dataUrl) return;
641
+ // Bail if the user has already rolled again — only the latest
642
+ // seed's render should land in the frame.
643
+ const cur = avatarState.placeholder ? null : (avatarState.seed + "::" + avatarState.roll);
644
+ if (cur !== expected) return;
645
+ const f2 = modal.querySelector("[data-na-avatar]");
646
+ if (!f2) return;
647
+ f2.innerHTML = `<img src="${dataUrl}" alt="">`;
648
+ }).catch(() => { /* */ });
625
649
  }
626
650
 
627
651
  function positionDropdown() {
@@ -665,10 +689,10 @@
665
689
  }
666
690
 
667
691
  /** Regenerate the avatar by asking the LLM for a "vibe seed" derived
668
- * from the director's name + bio, then painting the SVG via the
669
- * shared AvatarSkill. Falls back to a local random seed if the
670
- * endpoint errors (no key, network, etc.) so the button always
671
- * produces a fresh face. */
692
+ * from the director's name + bio, then painting the 3D portrait
693
+ * via the shared Avatar3DSnap. Falls back to a local random seed
694
+ * if the endpoint errors (no key, network, etc.) so the button
695
+ * always produces a fresh face. */
672
696
  async function regenerateAvatar() {
673
697
  const name = modal.querySelector(".na-name-input").value.trim();
674
698
  const desc = modal.querySelector(".na-desc-input").value.trim();
@@ -682,7 +706,7 @@
682
706
  // Without a name, just produce a random seed locally — no point
683
707
  // burning an LLM call on an empty form.
684
708
  if (!name) {
685
- avatarState.seed = (window.AvatarSkill?.randomSeed?.() || ("anon|" + Date.now()));
709
+ avatarState.seed = (window.Avatar3DSnap?.randomSeed?.() || ("anon|" + Date.now()));
686
710
  if (vibeEl) vibeEl.textContent = "";
687
711
  paintAvatar();
688
712
  return;
@@ -800,14 +824,16 @@
800
824
 
801
825
  // Avatar → data URL. If the user never clicked "regenerate", we
802
826
  // build one off the form values now so the agent has a real face.
827
+ // The render is async (3D portrait via Avatar3DSnap); falls back
828
+ // to an empty string when WebGL isn't available — the backend
829
+ // already tolerates a missing avatarPath.
803
830
  let avatarSeed = avatarState.seed;
804
831
  let avatarRoll = avatarState.roll || 1;
805
832
  if (avatarState.placeholder) {
806
833
  avatarSeed = (name + "|" + bio) || "anon";
807
834
  avatarRoll = 1;
808
835
  }
809
- const svg = generateAvatar(avatarSeed + "::" + avatarRoll);
810
- const avatarPath = "data:image/svg+xml;utf8," + encodeURIComponent(svg);
836
+ const avatarPath = (await generateAvatarDataUrl(avatarSeed + "::" + avatarRoll)) || "";
811
837
 
812
838
  // Lock the button while the request is in flight.
813
839
  create.disabled = true;