privateboard 0.1.37 → 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 (76) hide show
  1. package/dist/boot.js +1415 -91
  2. package/dist/boot.js.map +1 -1
  3. package/dist/cli.js +1415 -91
  4. package/dist/cli.js.map +1 -1
  5. package/dist/server.js +1271 -81
  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/adjourn-overlay.css +2 -2
  13. package/public/agent-overlay.css +27 -15
  14. package/public/agent-overlay.js +3 -1
  15. package/public/agent-profile.css +331 -41
  16. package/public/agent-profile.js +499 -75
  17. package/public/app-updater.css +1 -1
  18. package/public/app.js +2090 -547
  19. package/public/avatar-3d-snap.js +205 -0
  20. package/public/avatar-3d.js +792 -0
  21. package/public/avatar-customizer.html +274 -0
  22. package/public/avatar3d-editor.css +240 -0
  23. package/public/avatar3d-editor.js +481 -0
  24. package/public/avatars/3d/chair.png +0 -0
  25. package/public/avatars/3d/first-principles.png +0 -0
  26. package/public/avatars/3d/historian.png +0 -0
  27. package/public/avatars/3d/long-horizon.png +0 -0
  28. package/public/avatars/3d/phenomenologist.png +0 -0
  29. package/public/avatars/3d/socrates.png +0 -0
  30. package/public/avatars/3d/user-empathy.png +0 -0
  31. package/public/avatars/3d/value-investor.png +0 -0
  32. package/public/core-avatars.js +86 -0
  33. package/public/home-3d-loader.js +15 -4
  34. package/public/home-3d-mock.js +18 -7
  35. package/public/home.html +80 -18
  36. package/public/i18n.js +279 -4
  37. package/public/icons/avatar_1779855104027.glb +0 -0
  38. package/public/icons/logo.png +0 -0
  39. package/public/icons/new-style.glb +0 -0
  40. package/public/icons/new-style2.glb +0 -0
  41. package/public/icons/new-style3.glb +0 -0
  42. package/public/icons/new-style4.glb +0 -0
  43. package/public/icons/new-style5.glb +0 -0
  44. package/public/icons/office.glb +0 -0
  45. package/public/icons/stuff.glb +0 -0
  46. package/public/index.html +203 -182
  47. package/public/mention-picker.js +1 -1
  48. package/public/new-agent.css +7 -7
  49. package/public/new-agent.js +46 -20
  50. package/public/office-viewer.html +340 -0
  51. package/public/onboarding.css +5 -5
  52. package/public/quote-cta.css +5 -4
  53. package/public/quote-cta.js +50 -5
  54. package/public/room-settings.css +24 -9
  55. package/public/stuff-viewer.html +330 -0
  56. package/public/thread.css +1211 -0
  57. package/public/user-settings.css +16 -19
  58. package/public/user-settings.js +86 -78
  59. package/public/vendor/BufferGeometryUtils.js +1434 -0
  60. package/public/vendor/DRACOLoader.js +739 -0
  61. package/public/vendor/GLTFLoader.js +4860 -0
  62. package/public/vendor/RoomEnvironment.js +185 -0
  63. package/public/vendor/SkeletonUtils.js +496 -0
  64. package/public/vendor/draco/draco_decoder.js +34 -0
  65. package/public/vendor/draco/draco_decoder.wasm +0 -0
  66. package/public/vendor/draco/draco_encoder.js +33 -0
  67. package/public/vendor/draco/draco_wasm_wrapper.js +117 -0
  68. package/public/vendor/meshopt_decoder.module.js +196 -0
  69. package/public/voice-3d-banner.js +12 -0
  70. package/public/voice-3d.js +1407 -432
  71. package/public/voice-clone.css +875 -0
  72. package/public/voice-clone.js +1351 -0
  73. package/public/voice-replay.css +3 -3
  74. package/public/voice-replay.js +21 -0
  75. package/public/avatar-skill.js +0 -629
  76. package/public/icons/folded-sidebar.png +0 -0
package/public/index.html CHANGED
@@ -87,7 +87,7 @@
87
87
  background: var(--bg);
88
88
  color: var(--text);
89
89
  font-family: var(--mono);
90
- font-size: 13px;
90
+ font-size: 14px;
91
91
  line-height: 1.5;
92
92
  -webkit-font-smoothing: antialiased;
93
93
  height: 100vh;
@@ -327,7 +327,7 @@
327
327
  .migrate-deck {
328
328
  margin: 0;
329
329
  font-family: var(--font-human, system-ui, sans-serif);
330
- font-size: 13px;
330
+ font-size: 14px;
331
331
  line-height: 1.55;
332
332
  color: var(--text-soft);
333
333
  }
@@ -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;
@@ -1234,7 +1185,7 @@
1234
1185
  justify-content: center;
1235
1186
  gap: 7px;
1236
1187
  font-family: var(--sans);
1237
- font-size: 13px;
1188
+ font-size: 14px;
1238
1189
  font-weight: 500;
1239
1190
  color: var(--text-dim);
1240
1191
  cursor: pointer;
@@ -1554,7 +1505,7 @@
1554
1505
  position: relative;
1555
1506
  z-index: 1;
1556
1507
  font-family: var(--mono);
1557
- font-size: 13px;
1508
+ font-size: 14px;
1558
1509
  color: var(--lime);
1559
1510
  line-height: 1;
1560
1511
  }
@@ -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 /
@@ -2372,7 +2334,7 @@
2372
2334
  background: var(--panel-2);
2373
2335
  }
2374
2336
  .reports-load-more-arrow {
2375
- font-size: 13px;
2337
+ font-size: 14px;
2376
2338
  line-height: 1;
2377
2339
  }
2378
2340
 
@@ -2883,7 +2845,7 @@
2883
2845
  }
2884
2846
  .search-overlay-status-msg {
2885
2847
  font-family: var(--font-human);
2886
- font-size: 13px;
2848
+ font-size: 14px;
2887
2849
  color: var(--text-soft);
2888
2850
  line-height: 1.5;
2889
2851
  max-width: 360px;
@@ -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,
@@ -4715,7 +4705,7 @@
4715
4705
  .brief-active-substage {
4716
4706
  font-family: var(--font-headline, "Charter", "Songti SC", Georgia, serif);
4717
4707
  font-style: italic;
4718
- font-size: 13px;
4708
+ font-size: 14px;
4719
4709
  line-height: 1.55;
4720
4710
  color: var(--text-soft);
4721
4711
  margin-top: 6px;
@@ -5022,7 +5012,7 @@
5022
5012
  color: var(--bg);
5023
5013
  }
5024
5014
  .brief-retry-mark {
5025
- font-size: 13px;
5015
+ font-size: 14px;
5026
5016
  line-height: 1;
5027
5017
  }
5028
5018
 
@@ -5086,7 +5076,7 @@
5086
5076
  border: none;
5087
5077
  color: var(--text-faint);
5088
5078
  cursor: pointer;
5089
- font-size: 13px;
5079
+ font-size: 14px;
5090
5080
  line-height: 1;
5091
5081
  padding: 2px 4px;
5092
5082
  }
@@ -5121,7 +5111,7 @@
5121
5111
  color: var(--text-faint);
5122
5112
  width: 18px;
5123
5113
  padding: 0 4px 0 0;
5124
- font-size: 13px;
5114
+ font-size: 14px;
5125
5115
  line-height: 1;
5126
5116
  cursor: pointer;
5127
5117
  opacity: 0;
@@ -5458,7 +5448,7 @@
5458
5448
  what the user is looking at before they get to the verdict. */
5459
5449
  .dv-purpose {
5460
5450
  font-family: var(--font-human);
5461
- font-size: 13px;
5451
+ font-size: 14px;
5462
5452
  line-height: 1.55;
5463
5453
  color: var(--text-soft);
5464
5454
  margin: 0;
@@ -5511,7 +5501,7 @@
5511
5501
  }
5512
5502
  .dv-verdict-body {
5513
5503
  font-family: var(--font-human);
5514
- font-size: 13px;
5504
+ font-size: 14px;
5515
5505
  line-height: 1.55;
5516
5506
  color: var(--text-soft);
5517
5507
  margin: 0;
@@ -5532,7 +5522,7 @@
5532
5522
  }
5533
5523
  .dv-unexplored-row {
5534
5524
  font-family: var(--font-human);
5535
- font-size: 13px;
5525
+ font-size: 14px;
5536
5526
  line-height: 1.5;
5537
5527
  color: var(--text);
5538
5528
  padding: 8px 12px;
@@ -5710,7 +5700,7 @@
5710
5700
  overflows. Mirrors the adjourn-overlay / room-settings clamp
5711
5701
  pattern so all three modals read identically. */
5712
5702
  .followup-parent-subject {
5713
- font-size: 13px;
5703
+ font-size: 14px;
5714
5704
  font-weight: 600;
5715
5705
  color: var(--text);
5716
5706
  line-height: 1.4;
@@ -6396,7 +6386,7 @@
6396
6386
  common case (textarea + one strip); tall textarea +
6397
6387
  multiple strips can still cover the bottom message, the
6398
6388
  user scrolls a touch — acceptable. */
6399
- padding: calc(14px + var(--room-head-h, 56px)) 50px 140px 40px;
6389
+ padding: calc(14px + var(--room-head-h, 56px)) 40px 140px 40px;
6400
6390
  transition: opacity 0.18s ease-out;
6401
6391
  }
6402
6392
  /* Note-jump loading state · the user clicked a note in the All
@@ -6715,7 +6705,7 @@
6715
6705
  color: var(--text);
6716
6706
  text-decoration: none;
6717
6707
  font-family: var(--font-human);
6718
- font-size: 13px;
6708
+ font-size: 14px;
6719
6709
  font-weight: 600;
6720
6710
  letter-spacing: -0.005em;
6721
6711
  line-height: 1.4;
@@ -6727,6 +6717,7 @@
6727
6717
  text-overflow: ellipsis;
6728
6718
  white-space: nowrap;
6729
6719
  min-width: 0;
6720
+ font-weight: 400;
6730
6721
  }
6731
6722
  .msg-tool-sources-title:hover {
6732
6723
  color: var(--lime);
@@ -7352,15 +7343,14 @@
7352
7343
  align-items: center;
7353
7344
  justify-content: center;
7354
7345
  font-weight: 700;
7355
- font-size: 13px;
7346
+ font-size: 14px;
7356
7347
  border: 0.5px solid var(--line-strong);
7357
7348
  font-family: var(--mono);
7358
7349
  }
7359
- /* Pixel-art variant · when prefs.avatarSeed is set, renderChat swaps
7360
- the initial-letter chip for the AvatarSkill SVG (same seed the
7361
- sidebar foot + preference overlay use). The SVG provides its own
7362
- surface, so drop the border + bg + flex centering and let it fill
7363
- 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. */
7364
7354
  .msg.user .msg-av.msg-av-pixel {
7365
7355
  border: none;
7366
7356
  background: var(--bg);
@@ -7546,7 +7536,10 @@
7546
7536
  -webkit-box-orient: vertical;
7547
7537
  overflow: hidden;
7548
7538
  }
7549
- .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); }
7550
7543
 
7551
7544
  .msg-bubble {
7552
7545
  padding: 0;
@@ -7608,12 +7601,12 @@
7608
7601
  Used by quote-cta probe / second messages and any plain
7609
7602
  markdown blockquote a director / chair might emit. */
7610
7603
  .msg-bubble .msg-quote {
7611
- margin: 0 0 12px;
7612
- padding: 10px 14px;
7604
+ margin: 0;
7605
+ padding: 10px 0;
7613
7606
  background: var(--panel-2);
7614
7607
  font-family: var(--font-agent);
7615
7608
  font-style: italic;
7616
- font-size: 13px;
7609
+ font-size: 14px;
7617
7610
  line-height: 1.55;
7618
7611
  color: var(--text-soft);
7619
7612
  }
@@ -7629,7 +7622,6 @@
7629
7622
  font-style: normal;
7630
7623
  margin-bottom: 6px;
7631
7624
  }
7632
- .msg-bubble .msg-quote:last-child { margin-bottom: 0; }
7633
7625
  /* Markdown tables · editorial style. No outer box, no zebra, no
7634
7626
  uppercase data-grid headers — those read as "spreadsheet pasted
7635
7627
  into a conversation" and broke the bubble's literary register.
@@ -7646,7 +7638,7 @@
7646
7638
  width: 100%;
7647
7639
  font-family: var(--font-human);
7648
7640
  font-style: normal;
7649
- font-size: 13px;
7641
+ font-size: 14px;
7650
7642
  line-height: 1.55;
7651
7643
  color: var(--text-soft);
7652
7644
  letter-spacing: -0.003em;
@@ -7982,7 +7974,7 @@
7982
7974
  .kp-row:hover { border-color: var(--line-strong); }
7983
7975
  .kp-body {
7984
7976
  font-family: var(--font-human);
7985
- font-size: 13px;
7977
+ font-size: 14px;
7986
7978
  color: var(--text);
7987
7979
  line-height: 1.4;
7988
7980
  }
@@ -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;
@@ -9721,7 +9712,7 @@
9721
9712
  height: 16px;
9722
9713
  line-height: 1;
9723
9714
  font-family: var(--mono);
9724
- font-size: 13px;
9715
+ font-size: 14px;
9725
9716
  font-weight: 700;
9726
9717
  padding: 0;
9727
9718
  cursor: pointer;
@@ -11204,7 +11195,7 @@
11204
11195
  }
11205
11196
  .starter-deck {
11206
11197
  font-family: var(--font-human);
11207
- font-size: 13px;
11198
+ font-size: 14px;
11208
11199
  color: var(--text-soft);
11209
11200
  line-height: 1.5;
11210
11201
  margin: 0;
@@ -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,21 +11441,15 @@
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;
11458
11452
  }
11459
- .chat.chat--composer.chat--composer-overflow .cmp {
11460
- /* Overflow mode · the starters tray (now augmented with
11461
- topic recommendations + the "+ N more" button) can push
11462
- total content past one viewport, at which point the
11463
- composer can't stay centred. Pin the hero a fixed 120px
11464
- from the top so it sits comfortably above the fold and
11465
- the tray scrolls underneath. */
11466
- padding-top: 120px;
11467
- }
11468
11453
  .cmp-hero {
11469
11454
  text-align: center;
11470
11455
  margin-bottom: 22px;
@@ -12051,7 +12036,7 @@
12051
12036
  background: transparent;
12052
12037
  border: none;
12053
12038
  color: var(--text-faint);
12054
- font-size: 13px;
12039
+ font-size: 14px;
12055
12040
  line-height: 1;
12056
12041
  cursor: pointer;
12057
12042
  transition: color 0.1s;
@@ -12331,7 +12316,7 @@
12331
12316
  .cmp-celeb-card {
12332
12317
  position: relative;
12333
12318
  display: grid;
12334
- grid-template-columns: 56px 1fr;
12319
+ grid-template-columns: 64px 1fr;
12335
12320
  gap: 14px;
12336
12321
  align-items: stretch;
12337
12322
  width: 100%;
@@ -12352,12 +12337,8 @@
12352
12337
  display: flex;
12353
12338
  align-items: center;
12354
12339
  justify-content: center;
12355
- width: 56px;
12356
- height: 56px;
12357
- border-radius: 4px;
12358
- overflow: hidden;
12359
- background: var(--panel);
12360
- border: 1px solid var(--line);
12340
+ width: 64px;
12341
+ height: 64px;
12361
12342
  }
12362
12343
  .cmp-celeb-img {
12363
12344
  width: 100%;
@@ -12564,7 +12545,7 @@
12564
12545
  }
12565
12546
  .ag-gen-error-desc-body {
12566
12547
  font-family: var(--font-human);
12567
- font-size: 13px;
12548
+ font-size: 14px;
12568
12549
  line-height: 1.55;
12569
12550
  color: var(--text-soft);
12570
12551
  }
@@ -12595,7 +12576,7 @@
12595
12576
  color: var(--lime);
12596
12577
  }
12597
12578
  .ag-gen-error-retry-mark {
12598
- font-size: 13px;
12579
+ font-size: 14px;
12599
12580
  line-height: 1;
12600
12581
  }
12601
12582
  .ag-gen-error-discard {
@@ -13031,7 +13012,7 @@
13031
13012
  }
13032
13013
  .pb-done-sub {
13033
13014
  font-family: var(--font-human);
13034
- font-size: 13px;
13015
+ font-size: 14px;
13035
13016
  color: var(--text-soft);
13036
13017
  line-height: 1.5;
13037
13018
  margin: 0 auto 18px;
@@ -13073,7 +13054,7 @@
13073
13054
  background: transparent;
13074
13055
  color: var(--lime);
13075
13056
  }
13076
- .pb-done-open-mark { font-size: 13px; line-height: 1; }
13057
+ .pb-done-open-mark { font-size: 14px; line-height: 1; }
13077
13058
 
13078
13059
  /* When the manual overlay is in Full-mode reuse, surface the
13079
13060
  build's footer-meta override in lime (it carries the
@@ -13098,19 +13079,6 @@
13098
13079
  padding: 16px 0 32px;
13099
13080
  color: var(--text);
13100
13081
  }
13101
- /* Persona-builder layout · default behaviour is the SAME as the
13102
- new-room / new-agent hero: vertically centred by
13103
- `.chat.chat--composer { align-content: center }`. When the
13104
- dashboard's natural height exceeds 90% of the viewport (the
13105
- overflow trigger for pb-stage · `updateComposerOverflow` in
13106
- app.js switches its threshold from 70% of chat height to 90%
13107
- of `window.innerHeight` when a `.pb-stage` is present),
13108
- `.chat--composer-overflow` kicks in and we reduce the default
13109
- 120 px top offset to 50 px so the dashboard pins comfortably
13110
- near the top of the panel rather than getting shoved down. */
13111
- .chat.chat--composer.chat--composer-overflow:has(.pb-stage) .cmp {
13112
- padding-top: 50px !important;
13113
- }
13114
13082
  .pb-stage-head {
13115
13083
  display: grid;
13116
13084
  grid-template-columns: 1fr auto;
@@ -13655,7 +13623,7 @@
13655
13623
  background: color-mix(in srgb, var(--red) 5%, transparent);
13656
13624
  }
13657
13625
  .pb-cancel-mark {
13658
- font-size: 13px;
13626
+ font-size: 14px;
13659
13627
  line-height: 1;
13660
13628
  }
13661
13629
  .pb-foot-hint {
@@ -13714,7 +13682,7 @@
13714
13682
  border: 0.5px solid var(--line-bright);
13715
13683
  color: var(--text);
13716
13684
  font-family: var(--font-human);
13717
- font-size: 13px;
13685
+ font-size: 14px;
13718
13686
  padding: 7px 10px;
13719
13687
  width: 100%;
13720
13688
  box-sizing: border-box;
@@ -13849,7 +13817,7 @@
13849
13817
  gap: 12px;
13850
13818
  }
13851
13819
  .ag-persona-error-detail {
13852
- font-size: 13px;
13820
+ font-size: 14px;
13853
13821
  color: var(--text);
13854
13822
  line-height: 1.55;
13855
13823
  }
@@ -14155,7 +14123,7 @@
14155
14123
  border: 0.5px solid var(--line-strong);
14156
14124
  color: var(--text);
14157
14125
  font-family: var(--font-human);
14158
- font-size: 13px;
14126
+ font-size: 14px;
14159
14127
  line-height: 1.5;
14160
14128
  padding: 8px 10px;
14161
14129
  width: 100%;
@@ -14456,7 +14424,7 @@
14456
14424
  .brief-picker-title-head {
14457
14425
  font-family: var(--mono);
14458
14426
  font-size: 14px;
14459
- font-weight: 700;
14427
+ font-weight: 400;
14460
14428
  letter-spacing: 0.04em;
14461
14429
  color: var(--text);
14462
14430
  }
@@ -14474,7 +14442,7 @@
14474
14442
  }
14475
14443
  .brief-picker-row {
14476
14444
  display: grid;
14477
- grid-template-columns: 32px 1fr auto auto;
14445
+ grid-template-columns: 32px 1fr auto;
14478
14446
  gap: 10px;
14479
14447
  align-items: center;
14480
14448
  padding: 10px 12px;
@@ -14501,8 +14469,8 @@
14501
14469
  }
14502
14470
  .brief-picker-title {
14503
14471
  font-family: var(--font-human, system-ui, sans-serif);
14504
- font-size: 13px;
14505
- font-weight: 600;
14472
+ font-size: 14px;
14473
+ font-weight: 400;
14506
14474
  color: var(--text);
14507
14475
  line-height: 1.3;
14508
14476
  overflow: hidden;
@@ -14510,6 +14478,15 @@
14510
14478
  -webkit-line-clamp: 2;
14511
14479
  -webkit-box-orient: vertical;
14512
14480
  }
14481
+ /* Subtitle row · supplement label (left) + filed time (right) share
14482
+ one line beneath the title, so the title row gets the full main
14483
+ column width instead of competing with the time for space. */
14484
+ .brief-picker-subrow {
14485
+ display: flex;
14486
+ align-items: center;
14487
+ gap: 8px;
14488
+ min-width: 0;
14489
+ }
14513
14490
  .brief-picker-sub {
14514
14491
  font-family: var(--mono);
14515
14492
  font-size: 10px;
@@ -14518,6 +14495,8 @@
14518
14495
  overflow: hidden;
14519
14496
  text-overflow: ellipsis;
14520
14497
  white-space: nowrap;
14498
+ flex: 1 1 auto;
14499
+ min-width: 0;
14521
14500
  }
14522
14501
  .brief-picker-time {
14523
14502
  font-family: var(--mono);
@@ -14525,6 +14504,8 @@
14525
14504
  letter-spacing: 0.04em;
14526
14505
  color: var(--text-faint);
14527
14506
  white-space: nowrap;
14507
+ flex: 0 0 auto;
14508
+ margin-left: auto;
14528
14509
  }
14529
14510
  .brief-picker-arrow {
14530
14511
  font-family: var(--mono);
@@ -15620,7 +15601,7 @@
15620
15601
  flex: 1 1 auto;
15621
15602
  min-width: 0;
15622
15603
  font-family: var(--sans);
15623
- font-size: 13px;
15604
+ font-size: 14px;
15624
15605
  font-weight: 600;
15625
15606
  /* Theme accent · `--lime` is the per-theme primary token
15626
15607
  (swapped by every theme in themes.css). Sits on the
@@ -15773,11 +15754,18 @@
15773
15754
  <script src="room-settings.js" defer></script>
15774
15755
  <link rel="stylesheet" href="adjourn-overlay.css">
15775
15756
  <!-- Avatar skill must load before any UI that calls it (new-agent, user-settings). -->
15776
- <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>
15777
15763
  <link rel="stylesheet" href="agent-profile.css">
15778
15764
  <script src="agent-profile.js" defer></script>
15779
15765
  <link rel="stylesheet" href="new-agent.css">
15780
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>
15781
15769
  <link rel="stylesheet" href="user-settings.css">
15782
15770
  <script type="module" src="keys-store.js"></script>
15783
15771
  <script src="key-validators.js" defer></script>
@@ -15787,6 +15775,7 @@
15787
15775
  <script src="mention-picker.js" defer></script>
15788
15776
  <script src="auto-hide-scroll.js" defer></script>
15789
15777
  <link rel="stylesheet" href="onboarding.css">
15778
+ <script src="core-avatars.js" defer></script>
15790
15779
  <script src="voice-3d-banner.js" defer></script>
15791
15780
  <script src="onboarding.js" defer></script>
15792
15781
  <link rel="stylesheet" href="voice-onboarding.css">
@@ -15795,15 +15784,18 @@
15795
15784
  <script src="app-updater.js" defer></script>
15796
15785
  <link rel="stylesheet" href="quote-cta.css">
15797
15786
  <script src="quote-cta.js" defer></script>
15787
+ <link rel="stylesheet" href="thread.css">
15788
+ <link rel="stylesheet" href="voice-clone.css">
15789
+ <script src="voice-clone.js" defer></script>
15798
15790
  <script src="typing-sfx.js" defer></script>
15799
15791
  <script src="agent-build-bgm.js" defer></script>
15800
15792
  <script src="share-cover-svg-creator.js" defer></script>
15801
15793
  <!-- voice-3d · voxel pixel-art 3D round-table renderer. ES module
15802
15794
  (imports three.js from /vendor/three.module.min.js). Loads in
15803
15795
  parallel with app.js · no ordering dependency, app.js feature-
15804
- detects window.VoiceStage3D at render time. Toggle in user-
15805
- settings (or localStorage["boardroom.stage3d"]) decides whether
15806
- 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. -->
15807
15799
  <script type="module" src="voice-3d.js"></script>
15808
15800
  <!-- voice-recorder · meeting capture. Lazy module · only allocates
15809
15801
  anything on first start(). Loaded BEFORE app.js so the global
@@ -16084,12 +16076,7 @@
16084
16076
  sidebar uses); only the logo + rooms + agents expand. -->
16085
16077
  <aside class="mini-sidebar" aria-label="Collapsed sidebar navigation">
16086
16078
  <div class="mini-top">
16087
- <button type="button" class="mini-btn mini-logo" data-sidebar-expand data-i18n-tip="sidebar_expand" data-tip="Expand sidebar" aria-label="Expand sidebar">
16088
- <span class="mini-logo-av">
16089
- <img class="cl-open" src="/avatars/chair.svg" alt="" aria-hidden="true">
16090
- <img class="cl-blink" src="/avatars/chair-blink.svg" alt="" aria-hidden="true">
16091
- </span>
16092
- </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>
16093
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>
16094
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>
16095
16082
  <button type="button" class="mini-btn mini-search" data-search-trigger data-tip="Search" aria-label="Search"></button>
@@ -17024,6 +17011,22 @@
17024
17011
  const lsGet = (k) => { try { return localStorage.getItem(k); } catch (_) { return null; } };
17025
17012
  const lsSet = (k, v) => { try { localStorage.setItem(k, v); } catch (_) {} };
17026
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
+
17027
17030
  /* Truth source for "does this room exist?" is `window.app.rooms`,
17028
17031
  not the DOM. The sidebar may not have rendered yet on first
17029
17032
  restore (app.init's loadInitial is async and runs concurrently
@@ -17130,14 +17133,25 @@
17130
17133
 
17131
17134
  function applyAgentsSubState(sub) {
17132
17135
  if (!sub || sub === "new") {
17136
+ clearAgentRestoreGuard();
17133
17137
  if (window.app && typeof window.app.setComposerMode === "function") {
17134
17138
  window.app.setComposerMode("agent");
17135
17139
  }
17136
17140
  return;
17137
17141
  }
17138
17142
  if (typeof window.openAgentProfile === "function") {
17139
- 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
+ }
17140
17153
  catch (_) {
17154
+ clearAgentRestoreGuard();
17141
17155
  lsSet(AGENTS_KEY, "new");
17142
17156
  if (window.app && typeof window.app.setComposerMode === "function") {
17143
17157
  window.app.setComposerMode("agent");
@@ -17408,10 +17422,17 @@
17408
17422
  const tick = () => {
17409
17423
  attempts += 1;
17410
17424
  const id = lsGet(AGENTS_KEY);
17411
- if (!id || id === "new") return;
17425
+ if (!id || id === "new") { clearAgentRestoreGuard(); return; }
17412
17426
  const haveAny = document.querySelector(".agents-scroll .agent-row[data-agent-profile]");
17413
- 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
+ }
17414
17434
  if (attempts < 10) setTimeout(tick, 250);
17435
+ else clearAgentRestoreGuard(); // give up · reveal the composer
17415
17436
  };
17416
17437
  setTimeout(tick, 250);
17417
17438
  }