privateboard 0.1.22 → 0.1.24

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/public/app.js CHANGED
@@ -19,22 +19,21 @@
19
19
  const MODEL_LABELS = {
20
20
  "sonnet-4-6": "Sonnet 4.6",
21
21
  "opus-4-7": "Opus 4.7",
22
- "opus-4-6": "Opus 4.6",
23
22
  "opus-4-6-fast": "Opus 4.6 Fast",
24
23
  "haiku-4-5": "Haiku 4.5",
25
24
  "gpt-5-5": "GPT-5.5",
26
25
  "gpt-5-4": "GPT-5.4",
27
26
  "gpt-5-4-mini": "GPT-5.4 Mini",
28
- "gpt-5-5-pro": "GPT-5.5 Pro",
29
27
  "codex-5-4": "ChatGPT Codex 5.4",
30
28
  "gemini-3-1": "Gemini 3.1 Pro",
31
29
  "gemini-3-flash": "Gemini 3 Flash",
32
30
  "gemini-3-1-flash": "Gemini 3.1 Flash Lite",
33
- "grok-4-3": "Grok 4.3",
34
- "grok-4-1-fast": "Grok 4.1 Fast",
35
- "grok-4-20": "Grok 4.20",
36
31
  "deepseek-v4-pro": "DeepSeek V4 Pro",
37
32
  "deepseek-v4-flash": "DeepSeek Lite",
33
+ "glm-5-1": "GLM 5.1",
34
+ "kimi-k2-6": "Kimi K2.6",
35
+ "minimax-m2-7": "MiniMax M2.7",
36
+ "minimax-m2-5": "MiniMax M2.5",
38
37
  };
39
38
 
40
39
  /** Full model catalog for the new-agent composer dropdown. Mirrors
@@ -46,9 +45,8 @@
46
45
  const AGENT_COMPOSER_MODELS = [
47
46
  { v: "opus-4-7", label: "Claude Opus 4.7", provider: "Anthropic", deck: "deep reasoning" },
48
47
  { v: "sonnet-4-6", label: "Claude Sonnet 4.6", provider: "Anthropic", deck: "balanced · default" },
49
- { v: "opus-4-6", label: "Claude Opus 4.6", provider: "Anthropic", deck: "prior-gen flagship" },
50
48
  { v: "opus-4-6-fast", label: "Claude Opus 4.6 Fast", provider: "Anthropic", deck: "faster 4.6 · same intelligence" },
51
- { v: "haiku-4-5", label: "Claude Haiku 4.5", provider: "Anthropic", deck: "fast · low-cost" }, { v: "gpt-5-5-pro", label: "GPT-5.5 Pro", provider: "OpenAI", deck: "flagship · 1M ctx" },
49
+ { v: "haiku-4-5", label: "Claude Haiku 4.5", provider: "Anthropic", deck: "fast · low-cost" },
52
50
  { v: "gpt-5-5", label: "GPT-5.5", provider: "OpenAI", deck: "1M ctx" },
53
51
  { v: "gpt-5-4", label: "GPT-5.4", provider: "OpenAI", deck: "general · 1M ctx" },
54
52
  { v: "gpt-5-4-mini", label: "GPT-5.4 Mini", provider: "OpenAI", deck: "fast · 400k ctx" },
@@ -56,11 +54,12 @@
56
54
  { v: "gemini-3-1", label: "Gemini 3.1 Pro", provider: "Google", deck: "flagship · 1M ctx" },
57
55
  { v: "gemini-3-flash", label: "Gemini 3 Flash", provider: "Google", deck: "frontier flash · 1M ctx" },
58
56
  { v: "gemini-3-1-flash", label: "Gemini 3.1 Flash Lite", provider: "Google", deck: "fast · 1M ctx" },
59
- { v: "grok-4-3", label: "Grok 4.3", provider: "xAI", deck: "flagship · 1M ctx" },
60
- { v: "grok-4-1-fast", label: "Grok 4.1 Fast", provider: "xAI", deck: "fast · 256k ctx" },
61
- { v: "grok-4-20", label: "Grok 4.20", provider: "xAI", deck: "2M ctx · big context" },
62
57
  { v: "deepseek-v4-pro", label: "DeepSeek V4 Pro", provider: "DeepSeek", deck: "reasoning · open weights" },
63
58
  { v: "deepseek-v4-flash", label: "DeepSeek Lite", provider: "DeepSeek", deck: "V4 Flash · fast · 1M ctx" },
59
+ { v: "glm-5-1", label: "GLM 5.1", provider: "Zhipu", deck: "Zhipu flagship · 200k ctx" },
60
+ { v: "kimi-k2-6", label: "Kimi K2.6", provider: "Moonshot", deck: "long-context" },
61
+ { v: "minimax-m2-7", label: "MiniMax M2.7", provider: "MiniMax", deck: "flagship · long-context" },
62
+ { v: "minimax-m2-5", label: "MiniMax M2.5", provider: "MiniMax", deck: "prior · long-context" },
64
63
  ];
65
64
 
66
65
  /** Tone tooltips · short, user-readable summary of how each tone
@@ -789,6 +788,12 @@
789
788
 
790
789
  // ── Room lifecycle ────────────────────────────────────────
791
790
  async openRoom(roomId) {
791
+ // Close the @-mention picker if open · its director list is
792
+ // scoped to the previous room and would point at a stale cast
793
+ // mid-navigation. Idempotent · no-op when already closed.
794
+ if (window.MentionPicker && typeof window.MentionPicker.close === "function") {
795
+ window.MentionPicker.close();
796
+ }
792
797
  // Hand off SSE to a background tracker BEFORE closing the
793
798
  // foreground · openRoom is the room-to-room nav path
794
799
  // (closeRoom only fires when going back to "no room") so
@@ -2662,9 +2667,24 @@
2662
2667
  // If a director is mid-turn AND we don't already have a queued
2663
2668
  // message, ask the user how to proceed.
2664
2669
  if (this.isAgentSpeaking() && !this.pendingUserMessage) {
2665
- this.openSendChoiceModal(text);
2670
+ // Drain @-mentions NOW so the choice modal can carry them
2671
+ // along to interrupt / queue · waiting until handleSendChoice
2672
+ // would be too late (the textarea is cleared by then, and
2673
+ // the mention-picker filters by handle-still-present-in-text).
2674
+ const midTurnMentions = (window.MentionPicker && typeof window.MentionPicker.consumePendingMentions === "function")
2675
+ ? window.MentionPicker.consumePendingMentions(text)
2676
+ : [];
2677
+ this.openSendChoiceModal(text, midTurnMentions);
2666
2678
  return true;
2667
2679
  }
2680
+ // Drain any pending @-mentions the user picked via the
2681
+ // mention-picker overlay. The picker filters out handles the
2682
+ // user later backspaced out, so the resulting id list is
2683
+ // already consistent with the text body. Falls back to an
2684
+ // empty array when mention-picker.js hasn't loaded yet.
2685
+ const mentions = (window.MentionPicker && typeof window.MentionPicker.consumePendingMentions === "function")
2686
+ ? window.MentionPicker.consumePendingMentions(text)
2687
+ : [];
2668
2688
  input.value = "";
2669
2689
  // Shrink the textarea back to its single-line baseline ·
2670
2690
  // typed content may have grown the field, and the cleared
@@ -2674,7 +2694,7 @@
2674
2694
  if (input.matches?.(".ib-textarea[data-send-input]")) {
2675
2695
  this.autosizeRoomInputTextarea();
2676
2696
  }
2677
- this.sendMessage(text).catch((err) => alert("Send failed: " + err.message));
2697
+ this.sendMessage(text, mentions).catch((err) => alert("Send failed: " + err.message));
2678
2698
  return true;
2679
2699
  },
2680
2700
 
@@ -2989,8 +3009,12 @@
2989
3009
  /** Modal · "an agent is speaking · interrupt or wait?". Mirrors the
2990
3010
  * pause-choice overlay's chrome (.pc-overlay / .pc-modal) so the
2991
3011
  * visual treatment is consistent across modal prompts. */
2992
- openSendChoiceModal(text) {
3012
+ openSendChoiceModal(text, mentions) {
2993
3013
  this.closeSendChoiceModal();
3014
+ // Stash mentions alongside text so handleSendChoice forwards
3015
+ // them on interrupt / queue. Falls back to [] when called from
3016
+ // legacy / non-mention paths.
3017
+ this._sendChoiceMentions = Array.isArray(mentions) ? mentions.slice() : [];
2994
3018
  const speaker = this.currentQueue[0]
2995
3019
  ? this.agentsById[this.currentQueue[0].agentId]
2996
3020
  : null;
@@ -3037,10 +3061,12 @@
3037
3061
  if (el) el.remove();
3038
3062
  this._sendChoiceText = null;
3039
3063
  this._sendChoiceSpeakerId = null;
3064
+ this._sendChoiceMentions = null;
3040
3065
  },
3041
3066
  handleSendChoice(choice) {
3042
3067
  const text = this._sendChoiceText || "";
3043
3068
  const speakerId = this._sendChoiceSpeakerId;
3069
+ const mentions = Array.isArray(this._sendChoiceMentions) ? this._sendChoiceMentions.slice() : [];
3044
3070
  const input = document.querySelector('.input-bar input, [data-send-input]');
3045
3071
  this.closeSendChoiceModal();
3046
3072
  if (choice === "cancel" || !text.trim()) return;
@@ -3048,7 +3074,7 @@
3048
3074
  input.value = "";
3049
3075
  }
3050
3076
  if (choice === "interrupt") {
3051
- this.sendMessage(text).catch((err) => alert("Send failed: " + err.message));
3077
+ this.sendMessage(text, mentions).catch((err) => alert("Send failed: " + err.message));
3052
3078
  return;
3053
3079
  }
3054
3080
  if (choice === "queue") {
@@ -3064,7 +3090,7 @@
3064
3090
  // turns, AFTER current speaker finishes and BEFORE the next
3065
3091
  // speaker starts. The placeholder clears when the
3066
3092
  // message-appended SSE comes back.
3067
- this.sendMessage(text, [], "after-speaker").catch((err) => {
3093
+ this.sendMessage(text, mentions, "after-speaker").catch((err) => {
3068
3094
  alert("Queue failed: " + err.message);
3069
3095
  this.pendingUserMessage = null;
3070
3096
  this.pendingForSpeakerId = null;
@@ -6251,11 +6277,14 @@
6251
6277
  // base provider colour. Append-only — adding a new variant just
6252
6278
  // requires its modelV at the appropriate position.
6253
6279
  const MODEL_SHADE_ORDER = {
6254
- anthropic: ["opus-4-7", "opus-4-6", "sonnet-4-6", "haiku-4-5"],
6255
- openai: ["gpt-5-5", "gpt-5-4-mini", "codex-5-4"],
6280
+ anthropic: ["opus-4-7", "sonnet-4-6", "opus-4-6-fast", "haiku-4-5"],
6281
+ openai: ["gpt-5-5", "gpt-5-4", "gpt-5-4-mini", "codex-5-4"],
6256
6282
  google: ["gemini-3-1", "gemini-3-flash", "gemini-3-1-flash"],
6257
- xai: ["grok-4", "grok-4-3", "grok-4-mini"],
6258
- deepseek: ["deepseek-v4-pro", "deepseek-v4-flash", "deepseek-v4"],
6283
+ // xai · no Grok models in registry currently.
6284
+ deepseek: ["deepseek-v4-pro", "deepseek-v4-flash"],
6285
+ zhipu: ["glm-5-1"],
6286
+ moonshot: ["kimi-k2-6"],
6287
+ minimax: ["minimax-m2-7", "minimax-m2-5"],
6259
6288
  };
6260
6289
  const providerOf = (modelV) => {
6261
6290
  const hit = reachable.find((m) => m.modelV === modelV);
@@ -13957,8 +13986,24 @@
13957
13986
  source = null;
13958
13987
  }
13959
13988
  }
13960
- const inOtherRoom = !!(source && source.roomId && source.roomId !== this.currentRoomId);
13961
- if (!inOtherRoom) {
13989
+ // Visibility · hide only when the user is actively viewing the
13990
+ // same room as the playing source. "Actively viewing" means BOTH
13991
+ // currentRoomId matches AND the sidebar's active tab is rooms.
13992
+ // When the user switches to the Agents tab (or any non-rooms
13993
+ // tab), currentRoomId stays sticky to the last-opened room, but
13994
+ // the user is no longer looking at the room's stream — the
13995
+ // player is now their only anchor back to the live audio, so it
13996
+ // has to surface even though the ids match.
13997
+ const activeTabEl = document.querySelector(".sidebar-tab.active[data-sidebar-tab]");
13998
+ const activeTab = activeTabEl ? activeTabEl.dataset.sidebarTab : null;
13999
+ const viewingRoomsTab = !activeTab || activeTab === "rooms";
14000
+ const viewingSameRoom = !!(
14001
+ source &&
14002
+ source.roomId &&
14003
+ source.roomId === this.currentRoomId &&
14004
+ viewingRoomsTab
14005
+ );
14006
+ if (!source || viewingSameRoom) {
13962
14007
  el.hidden = true;
13963
14008
  el.removeAttribute("data-mini-player-room-id");
13964
14009
  el.removeAttribute("data-mini-player-kind");
package/public/home.html CHANGED
@@ -1802,7 +1802,7 @@
1802
1802
 
1803
1803
  <div class="hero-actions">
1804
1804
  <a href="#install" class="big-btn primary">[ ◆ Convene a Room ]</a>
1805
- <a href="https://github.com/kaysaith1900/privateboard/releases/download/v0.1.21/PrivateBoard-0.1.21-arm64.dmg"
1805
+ <a href="https://github.com/kaysaith1900/privateboard/releases/download/v0.1.23/PrivateBoard-0.1.23-arm64.dmg"
1806
1806
  class="big-btn secondary"
1807
1807
  download
1808
1808
  aria-label="Download PrivateBoard for macOS (Apple Silicon)"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M12 3v12"/><polyline points="6 11 12 17 18 11"/><path d="M5 21h14"/></svg> Download for Mac</a>
package/public/i18n.js CHANGED
@@ -531,6 +531,21 @@
531
531
  vonb_theme_nintendo_name: "CRITIQUE",
532
532
  vonb_theme_nintendo_deck: "burgundy carpet · formal",
533
533
 
534
+ upd_classification_left: "app update",
535
+ upd_classification_right: "// privateboard",
536
+ upd_meta_label: "// release",
537
+ upd_meta_live: "available",
538
+ upd_title: "A new version of PrivateBoard is available.",
539
+ upd_available_deck: "Download now and apply on restart. You can keep working while it downloads.",
540
+ upd_progress_preparing: "preparing…",
541
+ upd_ready_deck: "Download complete. Restart PrivateBoard to apply the update.",
542
+ upd_error_deck: "Could not reach the update server. Check your connection and try again later, or download manually from GitHub Releases.",
543
+ upd_btn_update_now: "Update now",
544
+ upd_btn_later: "Later",
545
+ upd_btn_hide: "Hide",
546
+ upd_btn_restart_now: "Restart to update",
547
+ upd_btn_close: "Close",
548
+
534
549
  onb_v2_classification_left: "first run · welcome",
535
550
  onb_v2_classification_right: "// local · ~60 seconds",
536
551
  onb_v2_back: "[ ◂ Back ]",
@@ -1507,6 +1522,21 @@ When the room ___, raise an objection.`,
1507
1522
  vonb_theme_nintendo_name: "批评",
1508
1523
  vonb_theme_nintendo_deck: "酒红地毯 · 正式",
1509
1524
 
1525
+ upd_classification_left: "应用更新",
1526
+ upd_classification_right: "// privateboard",
1527
+ upd_meta_label: "// 新版本",
1528
+ upd_meta_live: "可用",
1529
+ upd_title: "PrivateBoard 有新版本可用",
1530
+ upd_available_deck: "立即下载,重启后生效。下载过程中你可以继续使用。",
1531
+ upd_progress_preparing: "准备中…",
1532
+ upd_ready_deck: "下载完成,重启 PrivateBoard 即可应用新版本。",
1533
+ upd_error_deck: "无法连接到更新服务器。请检查网络后重试,或前往 GitHub Releases 手动下载。",
1534
+ upd_btn_update_now: "现在更新",
1535
+ upd_btn_later: "稍后",
1536
+ upd_btn_hide: "隐藏",
1537
+ upd_btn_restart_now: "重启以更新",
1538
+ upd_btn_close: "关闭",
1539
+
1510
1540
  onb_v2_classification_left: "首次进入 · 欢迎",
1511
1541
  onb_v2_classification_right: "// 本地 · 约 60 秒",
1512
1542
  onb_v2_back: "[ ◂ 上一步 ]",
@@ -2459,6 +2489,21 @@ When the room ___, raise an objection.`,
2459
2489
  vonb_theme_nintendo_name: "クリティーク",
2460
2490
  vonb_theme_nintendo_deck: "バーガンディ絨毯 · フォーマル",
2461
2491
 
2492
+ upd_classification_left: "アプリ更新",
2493
+ upd_classification_right: "// privateboard",
2494
+ upd_meta_label: "// リリース",
2495
+ upd_meta_live: "利用可能",
2496
+ upd_title: "PrivateBoard の新しいバージョンがあります。",
2497
+ upd_available_deck: "今すぐダウンロードし、再起動で適用されます。ダウンロード中も作業を続けられます。",
2498
+ upd_progress_preparing: "準備中…",
2499
+ upd_ready_deck: "ダウンロード完了。PrivateBoard を再起動して更新を適用してください。",
2500
+ upd_error_deck: "更新サーバーに接続できません。接続を確認して再試行するか、GitHub Releases から手動でダウンロードしてください。",
2501
+ upd_btn_update_now: "今すぐ更新",
2502
+ upd_btn_later: "後で",
2503
+ upd_btn_hide: "隠す",
2504
+ upd_btn_restart_now: "再起動して更新",
2505
+ upd_btn_close: "閉じる",
2506
+
2462
2507
  onb_v2_classification_left: "初回 · ようこそ",
2463
2508
  onb_v2_classification_right: "// ローカル · 約 60 秒",
2464
2509
  onb_v2_back: "[ ◂ 戻る ]",
@@ -3231,6 +3276,21 @@ When the room ___, raise an objection.`,
3231
3276
  vonb_theme_nintendo_name: "CRÍTICA",
3232
3277
  vonb_theme_nintendo_deck: "alfombra burdeos · formal",
3233
3278
 
3279
+ upd_classification_left: "actualización de la app",
3280
+ upd_classification_right: "// privateboard",
3281
+ upd_meta_label: "// versión",
3282
+ upd_meta_live: "disponible",
3283
+ upd_title: "Hay una nueva versión de PrivateBoard.",
3284
+ upd_available_deck: "Descárgala ahora y se aplicará al reiniciar. Puedes seguir trabajando mientras tanto.",
3285
+ upd_progress_preparing: "preparando…",
3286
+ upd_ready_deck: "Descarga completa. Reinicia PrivateBoard para aplicar la actualización.",
3287
+ upd_error_deck: "No se pudo conectar al servidor de actualizaciones. Revisa tu conexión o descarga manualmente desde GitHub Releases.",
3288
+ upd_btn_update_now: "Actualizar ahora",
3289
+ upd_btn_later: "Más tarde",
3290
+ upd_btn_hide: "Ocultar",
3291
+ upd_btn_restart_now: "Reiniciar para actualizar",
3292
+ upd_btn_close: "Cerrar",
3293
+
3234
3294
  onb_v2_classification_left: "primera vez · bienvenida",
3235
3295
  onb_v2_classification_right: "// local · ~60 segundos",
3236
3296
  onb_v2_back: "[ ◂ Atrás ]",
package/public/index.html CHANGED
@@ -523,6 +523,14 @@
523
523
  html.is-electron-mac .room-head .head-cast img {
524
524
  -webkit-app-region: no-drag;
525
525
  }
526
+ /* The expand-btn's ::after halo paints OUTSIDE the button rect
527
+ (inset: -16px), so it doesn't inherit the button's no-drag and
528
+ gets eaten by `.room-head { drag }` above. Stamp no-drag on the
529
+ pseudo directly so the 48×48 hit zone keeps registering clicks.
530
+ (Same shape as the sidebar-collapse-btn::after override.) */
531
+ html.is-electron-mac .room-head .room-head-expand::after {
532
+ -webkit-app-region: no-drag;
533
+ }
526
534
  /* ─── macOS Electron · top-strip drag coverage for non-room views.
527
535
  When the room view is hidden (All Reports / All Notes / Search /
528
536
  Agent Profile) or the room view is shown with no current room (the
@@ -817,42 +825,65 @@
817
825
 
818
826
  /* ─── In-header expand button ───
819
827
  Lives at the leading edge of `.room-head` (rendered by
820
- renderHeader() in app.js). Twin of `.sidebar-collapse-btn`
821
- naked CSS-glyph, no border, faint text, lime on hover. The
822
- glyph mirrors the on the collapse counterpart so the
823
- two controls read as a paired set. Hidden by default; shown
824
- only when the sidebar is collapsed AND a room is loaded. */
828
+ renderHeader() in app.js). Visual twin of `.sidebar-collapse-btn`
829
+ so the collapse/expand pair reads as the SAME control across
830
+ the two places it can appear (in-sidebar when open · in-header
831
+ when sidebar collapsed AND a room is loaded). Icon-only · no
832
+ chrome · same Lucide PanelLeft mask · same hover-lime cascade.
833
+ Always rendered in the mirrored variant (panel folded to the
834
+ right edge) since this button only ever appears in the collapsed
835
+ state · clicking it expands the sidebar back. */
825
836
  .room-head-expand {
826
837
  appearance: none;
827
- background: var(--panel-3);
828
- border: 0.5px solid var(--line-strong);
838
+ background: transparent;
839
+ border: 0;
829
840
  color: var(--text-soft);
830
841
  cursor: pointer;
831
- width: 24px;
832
- height: 22px;
842
+ width: 16px;
843
+ height: 16px;
833
844
  padding: 0;
834
- line-height: 1;
835
- font-family: var(--mono);
836
- font-size: 16px;
837
- font-weight: 700;
838
- /* position: relative so the ::after tooltip anchors to the
839
- button rather than escaping into the room-head's layout. */
845
+ flex-shrink: 0;
846
+ transition: color 0.12s;
847
+ /* position: relative anchors the ::after hit-area expander so
848
+ the cursor doesn't have to land precisely on the 16px icon. */
840
849
  position: relative;
841
850
  display: none;
842
- align-items: center;
843
- justify-content: center;
844
- transition: color 0.12s, background 0.12s, border-color 0.12s;
845
- flex-shrink: 0;
846
851
  }
847
- .room-head-expand::before { content: "›"; }
848
- body.sidebar-collapsed .room-head-expand {
849
- display: inline-flex;
852
+ .room-head-expand::before {
853
+ content: "";
854
+ display: block;
855
+ width: 16px;
856
+ height: 16px;
857
+ background-color: currentColor;
858
+ -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>");
859
+ 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>");
860
+ -webkit-mask-repeat: no-repeat;
861
+ mask-repeat: no-repeat;
862
+ -webkit-mask-position: center;
863
+ mask-position: center;
864
+ -webkit-mask-size: 16px 16px;
865
+ mask-size: 16px 16px;
866
+ /* Same scaleX(-1) the sidebar-collapse-btn uses when
867
+ body.sidebar-collapsed · since this button is ONLY visible
868
+ in the collapsed state, the flip is unconditional here. */
869
+ transform: scaleX(-1);
850
870
  }
851
- .room-head-expand:hover {
852
- color: var(--lime);
853
- border-color: var(--lime-dim);
854
- background: var(--panel-2);
871
+ /* Invisible hit-area expander · matches `.sidebar-collapse-btn::after`
872
+ so the click rectangle grows to 48×48 around the 16px icon. */
873
+ .room-head-expand::after {
874
+ content: "";
875
+ position: absolute;
876
+ inset: -16px;
877
+ }
878
+ body.sidebar-collapsed .room-head-expand {
879
+ display: block;
880
+ /* Extra 5px breathing room between the icon and the title cluster
881
+ on top of the room-head's own 12px column gap · the icon sat
882
+ too close to the kicker / subject otherwise. Scoped to the
883
+ collapsed state since the button itself only exists then. */
884
+ margin-right: 5px;
855
885
  }
886
+ .room-head-expand:hover { color: var(--lime); }
856
887
 
857
888
  /* ─── Sidebar tabs (Rooms / Agents) — segmented control with icon ─── */
858
889
  .sidebar-tabs {
@@ -2752,7 +2783,12 @@
2752
2783
  transition: background-color 5000s ease-in-out 0s;
2753
2784
  }
2754
2785
  .search-page.has-results .search-input {
2755
- padding: 9px 6px;
2786
+ /* Horizontal padding matches the new-room composer's `.cmp-input`
2787
+ (14 px each side) so the text-to-input-box gutter reads the same
2788
+ across the two surfaces. The earlier 6 px was visibly tighter
2789
+ than the new-room textarea and made the head-row input feel
2790
+ cramped against its panel-2 pill. */
2791
+ padding: 9px 14px;
2756
2792
  font-size: 13px;
2757
2793
  }
2758
2794
  .search-input::placeholder {
@@ -11773,11 +11809,11 @@
11773
11809
  align-items: center;
11774
11810
  gap: 8px;
11775
11811
  padding: 6px 10px;
11776
- /* Bottombar matches the topbar's brand-tinted treatment: `--strip-bg`
11777
- fill + `--accent-line` hairline on top. The two strips bookend the
11778
- textarea zone so the composer reads as one cohesive brand surface. */
11779
- background: var(--strip-bg);
11780
- border-top: 0.5px solid var(--accent-line);
11812
+ /* Bottombar floats transparently over the input frame no fill,
11813
+ no top hairline. Only the topbar carries the brand-tinted strip
11814
+ treatment; the bottom controls read as buttons-on-the-input,
11815
+ not a second chrome band. */
11816
+ background: transparent;
11781
11817
  min-height: 48px;
11782
11818
  /* Establish a size container · child elements (notably the
11783
11819
  voice-mode toggle's text) can respond to the toolbar's width
@@ -15692,11 +15728,14 @@
15692
15728
  <script src="user-settings.js" defer></script>
15693
15729
  <link rel="stylesheet" href="voice-replay.css">
15694
15730
  <script src="voice-replay.js" defer></script>
15731
+ <script src="mention-picker.js" defer></script>
15695
15732
  <script src="auto-hide-scroll.js" defer></script>
15696
15733
  <link rel="stylesheet" href="onboarding.css">
15697
15734
  <script src="onboarding.js" defer></script>
15698
15735
  <link rel="stylesheet" href="voice-onboarding.css">
15699
15736
  <script src="voice-onboarding.js" defer></script>
15737
+ <link rel="stylesheet" href="app-updater.css">
15738
+ <script src="app-updater.js" defer></script>
15700
15739
  <link rel="stylesheet" href="quote-cta.css">
15701
15740
  <script src="quote-cta.js" defer></script>
15702
15741
  <script src="typing-sfx.js" defer></script>
@@ -16946,6 +16985,16 @@
16946
16985
  else if (which === "agents") applyAgentsSubState(lsGet(AGENTS_KEY) || "new");
16947
16986
 
16948
16987
  if (persist) lsSet(TAB_KEY, which);
16988
+
16989
+ // Voice mini-player visibility depends on the active sidebar
16990
+ // tab (refreshMiniPlayer in app.js gates on it). A bare tab
16991
+ // switch doesn't fire any of the boardroom:replay-* events, so
16992
+ // we trigger the refresh explicitly here — otherwise the player
16993
+ // sticks in whichever state it was last evaluated under (most
16994
+ // visibly: hidden when entering Agents from the playing room).
16995
+ if (window.app && typeof window.app.refreshMiniPlayer === "function") {
16996
+ window.app.refreshMiniPlayer();
16997
+ }
16949
16998
  }
16950
16999
 
16951
17000
  /* ─── Capture-phase tracker for sidebar agent rows ─────────────
@@ -17424,6 +17473,93 @@
17424
17473
  </article>
17425
17474
  </template>
17426
17475
 
17476
+ <div class="upd-overlay" id="upd-overlay" role="dialog" aria-modal="true" aria-hidden="true">
17477
+ <div class="upd-backdrop" data-upd-close></div>
17478
+ <div class="upd-modal" role="document">
17479
+
17480
+ <div class="upd-classification">
17481
+ <span><span class="dot">●</span> <span data-i18n="upd_classification_left">app update</span></span>
17482
+ <span class="right" data-i18n="upd_classification_right">// privateboard</span>
17483
+ </div>
17484
+
17485
+ <header class="upd-head">
17486
+ <div class="upd-head-text">
17487
+ <div class="meta">
17488
+ <span data-i18n="upd_meta_label">// release</span>
17489
+ <span>·</span>
17490
+ <span class="live" data-i18n="upd_meta_live">available</span>
17491
+ </div>
17492
+ <div class="title" data-upd-title data-i18n="upd_title">A new version of PrivateBoard is available.</div>
17493
+ </div>
17494
+ <button type="button" class="close-btn" data-upd-close aria-label="Close">✕</button>
17495
+ </header>
17496
+
17497
+ <div class="upd-body">
17498
+
17499
+ <!-- AVAILABLE · prompt the user to start the download -->
17500
+ <div class="upd-state upd-state-available">
17501
+ <div class="upd-version">
17502
+ <span class="from" data-upd-from-version>v0.0.0</span>
17503
+ <span class="arrow">→</span>
17504
+ <span class="to" data-upd-to-version>v0.0.0</span>
17505
+ </div>
17506
+ <p class="upd-deck" data-i18n="upd_available_deck">Download now and apply on restart. You can keep working while it downloads.</p>
17507
+ </div>
17508
+
17509
+ <!-- DOWNLOADING · live progress -->
17510
+ <div class="upd-state upd-state-downloading">
17511
+ <div class="upd-version">
17512
+ <span class="from" data-upd-from-version-d>v0.0.0</span>
17513
+ <span class="arrow">→</span>
17514
+ <span class="to" data-upd-to-version-d>v0.0.0</span>
17515
+ </div>
17516
+ <div class="upd-progress-card">
17517
+ <div class="upd-progress-head">
17518
+ <span class="upd-progress-pct" data-upd-pct>0%</span>
17519
+ <span class="upd-progress-rate" data-upd-rate>—</span>
17520
+ </div>
17521
+ <div class="upd-progress-bar indeterminate" data-upd-bar><span></span></div>
17522
+ <div class="upd-progress-bytes" data-upd-bytes data-i18n="upd_progress_preparing">preparing…</div>
17523
+ </div>
17524
+ </div>
17525
+
17526
+ <!-- READY · download complete, prompt restart -->
17527
+ <div class="upd-state upd-state-ready">
17528
+ <div class="upd-version">
17529
+ <span class="from" data-upd-from-version-r>v0.0.0</span>
17530
+ <span class="arrow">→</span>
17531
+ <span class="to" data-upd-to-version-r>v0.0.0</span>
17532
+ </div>
17533
+ <p class="upd-deck" data-i18n="upd_ready_deck">Download complete. Restart PrivateBoard to apply the update.</p>
17534
+ </div>
17535
+
17536
+ <!-- ERROR -->
17537
+ <div class="upd-state upd-state-error">
17538
+ <div class="upd-error" data-upd-error-message>—</div>
17539
+ <p class="upd-deck" data-i18n="upd_error_deck">Could not reach the update server. Check your connection and try again later, or download manually from GitHub Releases.</p>
17540
+ </div>
17541
+
17542
+ </div>
17543
+
17544
+ <footer class="upd-foot">
17545
+ <span class="upd-foot-state upd-foot-state-available">
17546
+ <button type="button" class="upd-btn" data-upd-dismiss data-i18n="upd_btn_later">Later</button>
17547
+ <button type="button" class="upd-btn primary" data-upd-download data-i18n="upd_btn_update_now">Update now</button>
17548
+ </span>
17549
+ <span class="upd-foot-state upd-foot-state-downloading">
17550
+ <button type="button" class="upd-btn" data-upd-dismiss data-i18n="upd_btn_hide">Hide</button>
17551
+ </span>
17552
+ <span class="upd-foot-state upd-foot-state-ready">
17553
+ <button type="button" class="upd-btn" data-upd-dismiss data-i18n="upd_btn_later">Later</button>
17554
+ <button type="button" class="upd-btn primary" data-upd-install data-i18n="upd_btn_restart_now">Restart to update</button>
17555
+ </span>
17556
+ <span class="upd-foot-state upd-foot-state-error">
17557
+ <button type="button" class="upd-btn" data-upd-dismiss data-i18n="upd_btn_close">Close</button>
17558
+ </span>
17559
+ </footer>
17560
+ </div>
17561
+ </div>
17562
+
17427
17563
  <div class="vonb-overlay" id="vonb-overlay" role="dialog" aria-modal="true" aria-hidden="true">
17428
17564
  <div class="vonb-backdrop" data-vonb-close></div>
17429
17565
  <div class="vonb-modal" role="document">