privateboard 0.1.13 → 0.1.16

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "privateboard",
3
- "version": "0.1.13",
3
+ "version": "0.1.16",
4
4
  "description": "PrivateBoard · your private board meeting, on call. Local-first, multi-agent thinking amplifier.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -78,7 +78,7 @@
78
78
  }
79
79
  .adjourn-head .meta {
80
80
  font-family: var(--mono);
81
- font-size: 9.5px;
81
+ font-size: 10px;
82
82
  color: var(--text-dim, #5C5A52);
83
83
  text-transform: uppercase;
84
84
  letter-spacing: 0.12em;
@@ -159,7 +159,7 @@
159
159
  }
160
160
  .adjourn-summary-key {
161
161
  font-family: var(--mono);
162
- font-size: 9.5px;
162
+ font-size: 10px;
163
163
  letter-spacing: 0.14em;
164
164
  text-transform: uppercase;
165
165
  color: var(--text-faint);
@@ -199,7 +199,7 @@
199
199
  background: transparent;
200
200
  border: none;
201
201
  font-family: var(--mono);
202
- font-size: 9.5px;
202
+ font-size: 10px;
203
203
  letter-spacing: 0.14em;
204
204
  text-transform: uppercase;
205
205
  color: var(--text-soft);
@@ -231,7 +231,7 @@
231
231
  }
232
232
  .adjourn-mode-label {
233
233
  font-family: var(--mono);
234
- font-size: 9.5px;
234
+ font-size: 10px;
235
235
  letter-spacing: 0.14em;
236
236
  color: var(--text-faint);
237
237
  margin-bottom: 8px;
@@ -395,7 +395,7 @@
395
395
  align-items: center;
396
396
  gap: 8px;
397
397
  font-family: var(--mono);
398
- font-size: 10.5px;
398
+ font-size: 10px;
399
399
  font-weight: 700;
400
400
  letter-spacing: 0.14em;
401
401
  text-transform: uppercase;
@@ -421,7 +421,7 @@
421
421
  .adjourn-cancel,
422
422
  .adjourn-confirm {
423
423
  font-family: var(--mono);
424
- font-size: 10.5px;
424
+ font-size: 10px;
425
425
  font-weight: 700;
426
426
  letter-spacing: 0.14em;
427
427
  text-transform: uppercase;
@@ -0,0 +1,292 @@
1
+ /* ────────────────────────────────────────────────────────────────
2
+ agent-build-bgm.js · Sci-fi SCANNING palette played during
3
+ agent-creation flows (Signal-mode `_runAgentSpecGeneration` AND
4
+ Full-persona `personaJob` builds). Reads as "the system is
5
+ scanning the search space" — a slow radar/sonar sweep with deep
6
+ echoey tail. Earlier meditative drone gave users a headache
7
+ (gain LFO chopping + sustained pad layered too dense); this
8
+ version trades the drone for movement, with quieter overall
9
+ level and no rhythmic chopping.
10
+
11
+ Public surface · window.boardroomAgentBuildBgm:
12
+ · start() · idempotent · build audio graph + fade in
13
+ · stop() · idempotent · fade out + tear down
14
+ · isPlaying()· boolean state for diagnostics
15
+
16
+ Sound design (scanning palette):
17
+ · Sweep ping · sine carrier whose frequency rises from 400 Hz
18
+ to ~1200 Hz over each 2.5 s cycle then snaps
19
+ back · driven by a sawtooth LFO. Classic radar
20
+ sweep. The audible motion IS the audio.
21
+ · Sub presence · sine 80 Hz at low volume, constant · gives
22
+ the scanner a "machine is online" foundation
23
+ without adding a drone the ear fixates on.
24
+ · Counter sweep · a second sine that sweeps DOWN (1100 Hz →
25
+ 350 Hz) in counter-phase with the main · the
26
+ two crossing pitches read as "two scanners
27
+ triangulating". Half the level of the main.
28
+ · Long delay · 0.55 s with 0.22 feedback, low-pass 2 kHz on
29
+ the feedback chain · the ping decays into
30
+ fainter echoes, creating spaciousness without
31
+ adding new tones.
32
+ · Master gain · peak 0.035 (slightly quieter than the previous
33
+ 0.04 drone). The pitch motion is more salient
34
+ than steady drone gain, so the same loudness
35
+ would read as too loud.
36
+ · NO gain-LFO chopping. NO tremolo. NO shimmer layer. Those
37
+ three together produced the "headache" report on v1 ·
38
+ removed entirely here. Movement comes purely from the
39
+ pitch sweep + the delay tail.
40
+
41
+ Gating · the SAME global toggle as `typing-sfx.js`
42
+ (`boardroom.sfx.typing`, surfaced as "Sound effects" in User
43
+ Settings). When that's OFF, this BGM is silent regardless of
44
+ call frequency. The caller (`_syncAgentBuildBgm` in app.js)
45
+ decides WHEN to call start/stop based on composer state +
46
+ build status; this module just plays / doesn't play.
47
+
48
+ Gesture gate · same pattern as typing-sfx.js: AudioContext is
49
+ created lazily on the first `start()` call that follows a user
50
+ gesture. If `start()` is called pre-gesture, the start is
51
+ deferred via the gesture listener.
52
+ ──────────────────────────────────────────────────────────────── */
53
+
54
+ (function () {
55
+ "use strict";
56
+
57
+ /* ─── State ─── */
58
+ let _ctx = null;
59
+ let _ctxFailed = false;
60
+ let _hadGesture = false;
61
+ let _pendingStart = false;
62
+ let _running = false;
63
+ /** Audio graph references kept alive while playing · cleared in
64
+ * stop() so the GC can reclaim the nodes. */
65
+ let _nodes = null;
66
+
67
+ function readGlobalSfxEnabled() {
68
+ try {
69
+ if (typeof window.boardroomTypingSfx?.isEnabled === "function") {
70
+ return window.boardroomTypingSfx.isEnabled();
71
+ }
72
+ } catch { /* fall through */ }
73
+ // If the typing-sfx module hasn't loaded yet, default to ON ·
74
+ // the caller already gated on a build being active, so this
75
+ // is a soft permissive default. Once typing-sfx is around the
76
+ // real toggle wins.
77
+ return true;
78
+ }
79
+
80
+ function markGesture() {
81
+ _hadGesture = true;
82
+ if (_ctx && _ctx.state === "suspended") {
83
+ _ctx.resume().catch(() => { /* swallow */ });
84
+ }
85
+ if (_pendingStart && readGlobalSfxEnabled()) {
86
+ _pendingStart = false;
87
+ start();
88
+ }
89
+ }
90
+ ["pointerdown", "keydown", "touchstart"].forEach((ev) => {
91
+ window.addEventListener(ev, markGesture, { passive: true, capture: true });
92
+ });
93
+
94
+ function ensureContext() {
95
+ if (_ctxFailed) return null;
96
+ if (_ctx) {
97
+ if (_ctx.state === "suspended") {
98
+ _ctx.resume().catch(() => { /* */ });
99
+ }
100
+ return _ctx;
101
+ }
102
+ if (!_hadGesture) return null;
103
+ try {
104
+ const Ctx = window.AudioContext || window.webkitAudioContext;
105
+ if (!Ctx) { _ctxFailed = true; return null; }
106
+ _ctx = new Ctx();
107
+ if (_ctx.state === "suspended") {
108
+ _ctx.resume().catch(() => { /* */ });
109
+ }
110
+ return _ctx;
111
+ } catch {
112
+ _ctxFailed = true;
113
+ return null;
114
+ }
115
+ }
116
+
117
+ /* ─── Build the audio graph ─── */
118
+ function buildGraph(ctx) {
119
+ const t0 = ctx.currentTime;
120
+
121
+ /* Master gain · everything routes through here. Starts silent,
122
+ * ramped up by start(). */
123
+ const master = ctx.createGain();
124
+ master.gain.setValueAtTime(0.0001, t0);
125
+ master.connect(ctx.destination);
126
+
127
+ /* Long delay · feeds back its low-pass-filtered tail. The two
128
+ * sweeps below send into this directly so each cycle leaves a
129
+ * fainter echo decaying for a few seconds — classic radar / sonar
130
+ * "ping returning from far away" feel. */
131
+ const delay = ctx.createDelay(2.0);
132
+ delay.delayTime.value = 0.55;
133
+ const delayFb = ctx.createGain();
134
+ delayFb.gain.value = 0.22;
135
+ const delayFbFilter = ctx.createBiquadFilter();
136
+ delayFbFilter.type = "lowpass";
137
+ delayFbFilter.frequency.value = 2000;
138
+ delayFbFilter.Q.value = 0.5;
139
+ delay.connect(delayFbFilter).connect(delayFb).connect(delay);
140
+ delay.connect(master);
141
+
142
+ /* Sub presence · sine 80 Hz, constant low volume. Gives the
143
+ * scanner an "online" hum without becoming a drone the ear
144
+ * latches onto. Routed direct to master (no delay). */
145
+ const subOsc = ctx.createOscillator();
146
+ subOsc.type = "sine";
147
+ subOsc.frequency.value = 80;
148
+ const subGain = ctx.createGain();
149
+ subGain.gain.value = 0.06;
150
+ subOsc.connect(subGain).connect(master);
151
+
152
+ /* Main sweep · sine carrier swept upward by a sawtooth LFO.
153
+ * Base 400 Hz, peak ~1200 Hz, 2.5-second cycle (LFO at 0.4 Hz).
154
+ * Sawtooth gives a rising ramp that snaps back at the end of
155
+ * each cycle — the canonical radar pattern. */
156
+ const mainOsc = ctx.createOscillator();
157
+ mainOsc.type = "sine";
158
+ mainOsc.frequency.value = 400;
159
+ const mainGain = ctx.createGain();
160
+ mainGain.gain.value = 0.34;
161
+
162
+ const mainLfo = ctx.createOscillator();
163
+ mainLfo.type = "sawtooth";
164
+ mainLfo.frequency.value = 0.4;
165
+ const mainLfoGain = ctx.createGain();
166
+ mainLfoGain.gain.value = 400; // ±400 around 400 base
167
+ mainLfo.connect(mainLfoGain);
168
+ mainLfoGain.connect(mainOsc.frequency);
169
+
170
+ /* Counter sweep · second sine that sweeps DOWN in counter-phase.
171
+ * Base 1100 Hz, troughs around 350 Hz, half the level of the
172
+ * main so the two crossing pitches feel like two scanners
173
+ * triangulating rather than a single noisy ramp. Uses an
174
+ * INVERTED sawtooth LFO (negative gain) so it ramps downward
175
+ * while the main ramps upward. */
176
+ const counterOsc = ctx.createOscillator();
177
+ counterOsc.type = "sine";
178
+ counterOsc.frequency.value = 1100;
179
+ const counterGain = ctx.createGain();
180
+ counterGain.gain.value = 0.17;
181
+
182
+ const counterLfo = ctx.createOscillator();
183
+ counterLfo.type = "sawtooth";
184
+ counterLfo.frequency.value = 0.4;
185
+ const counterLfoGain = ctx.createGain();
186
+ counterLfoGain.gain.value = -375; // negative · sweep DOWN
187
+ counterLfo.connect(counterLfoGain);
188
+ counterLfoGain.connect(counterOsc.frequency);
189
+
190
+ /* Both sweeps go direct to master AND into the delay. The
191
+ * direct path keeps the ping crisp; the delay path adds the
192
+ * spacious decay trail without smearing the present audio. */
193
+ mainOsc.connect(mainGain);
194
+ counterOsc.connect(counterGain);
195
+ mainGain.connect(master);
196
+ counterGain.connect(master);
197
+ mainGain.connect(delay);
198
+ counterGain.connect(delay);
199
+
200
+ /* Start everything · they run forever until stop() takes them
201
+ * down. LFOs start out-of-phase by 1 ms so the two oscillators
202
+ * don't lock to perfectly mirrored peaks. */
203
+ subOsc.start(t0);
204
+ mainOsc.start(t0);
205
+ counterOsc.start(t0);
206
+ mainLfo.start(t0);
207
+ counterLfo.start(t0 + 0.001);
208
+
209
+ return {
210
+ master,
211
+ delay, delayFb, delayFbFilter,
212
+ subOsc, subGain,
213
+ mainOsc, mainGain, mainLfo, mainLfoGain,
214
+ counterOsc, counterGain, counterLfo, counterLfoGain,
215
+ };
216
+ }
217
+
218
+ function teardownGraph(nodes, ctx) {
219
+ if (!nodes) return;
220
+ const oscs = [nodes.subOsc, nodes.mainOsc, nodes.counterOsc, nodes.mainLfo, nodes.counterLfo];
221
+ for (const o of oscs) {
222
+ try { o.stop(); } catch { /* already stopped */ }
223
+ }
224
+ // Disconnect every node we kept a reference to · loose
225
+ // try/catch so a single failure doesn't strand others.
226
+ for (const key of Object.keys(nodes)) {
227
+ try { nodes[key]?.disconnect(); } catch { /* */ }
228
+ }
229
+ }
230
+
231
+ /* ─── Public API ─── */
232
+ function start() {
233
+ if (_running) return;
234
+ if (!readGlobalSfxEnabled()) return;
235
+ const ctx = ensureContext();
236
+ if (!ctx) {
237
+ // No gesture yet · queue a deferred start. The gesture
238
+ // listener (above) will retry when a real interaction lands.
239
+ _pendingStart = true;
240
+ return;
241
+ }
242
+ try {
243
+ _nodes = buildGraph(ctx);
244
+ // Ramp master gain in over 1 s. Exponential ramps need a
245
+ // non-zero starting value; setValueAtTime(0.0001) at t0
246
+ // happened in buildGraph already. 0.035 is slightly quieter
247
+ // than the previous drone's 0.04 · pitch motion is more
248
+ // salient than steady drone, so the same loudness reads as
249
+ // too loud in this palette.
250
+ const t = ctx.currentTime;
251
+ const targetGain = 0.035;
252
+ _nodes.master.gain.exponentialRampToValueAtTime(targetGain, t + 1.0);
253
+ _running = true;
254
+ } catch (e) {
255
+ // Audio graph creation rarely fails, but if it does we tear
256
+ // down anything that partially succeeded and stay silent.
257
+ try { teardownGraph(_nodes, ctx); } catch { /* */ }
258
+ _nodes = null;
259
+ _running = false;
260
+ }
261
+ }
262
+
263
+ function stop() {
264
+ _pendingStart = false;
265
+ if (!_running || !_ctx || !_nodes) {
266
+ // Even if not playing, defensively kill any deferred start so
267
+ // a later gesture doesn't resurrect a build the caller already
268
+ // declared finished.
269
+ _pendingStart = false;
270
+ return;
271
+ }
272
+ const ctx = _ctx;
273
+ const stale = _nodes;
274
+ _nodes = null;
275
+ _running = false;
276
+ try {
277
+ const t = ctx.currentTime;
278
+ stale.master.gain.cancelScheduledValues(t);
279
+ stale.master.gain.setValueAtTime(stale.master.gain.value, t);
280
+ stale.master.gain.exponentialRampToValueAtTime(0.0001, t + 0.8);
281
+ } catch { /* ignore · setTimeout still tears down */ }
282
+ setTimeout(() => {
283
+ teardownGraph(stale, ctx);
284
+ }, 1000);
285
+ }
286
+
287
+ function isPlaying() {
288
+ return _running;
289
+ }
290
+
291
+ window.boardroomAgentBuildBgm = { start, stop, isPlaying };
292
+ })();
@@ -118,14 +118,14 @@ img[data-agent]:hover { filter: brightness(1.15); }
118
118
  letter-spacing: -0.01em;
119
119
  }
120
120
  .agent-card-id .role {
121
- font-size: 9.5px;
121
+ font-size: 10px;
122
122
  color: var(--lime, #6FB572);
123
123
  text-transform: uppercase;
124
124
  letter-spacing: 0.16em;
125
125
  font-weight: 700;
126
126
  }
127
127
  .agent-card-id .handle {
128
- font-size: 10.5px;
128
+ font-size: 10px;
129
129
  color: var(--text-faint, #3A382F);
130
130
  margin-top: 5px;
131
131
  letter-spacing: 0.04em;
@@ -151,7 +151,7 @@ img[data-agent]:hover { filter: brightness(1.15); }
151
151
 
152
152
  .agent-block { margin-bottom: 16px; }
153
153
  .agent-block-label {
154
- font-size: 9.5px;
154
+ font-size: 10px;
155
155
  font-weight: 700;
156
156
  text-transform: uppercase;
157
157
  letter-spacing: 0.16em;
@@ -170,7 +170,7 @@ img[data-agent]:hover { filter: brightness(1.15); }
170
170
  }
171
171
  .agent-block-label .badge {
172
172
  margin-left: auto;
173
- font-size: 8.5px;
173
+ font-size: 8px;
174
174
  letter-spacing: 0.12em;
175
175
  color: var(--text-faint, #3A382F);
176
176
  font-weight: 600;
@@ -194,7 +194,7 @@ img[data-agent]:hover { filter: brightness(1.15); }
194
194
  flex-wrap: wrap;
195
195
  gap: 8px;
196
196
  font-family: var(--mono);
197
- font-size: 11.5px;
197
+ font-size: 12px;
198
198
  color: var(--text, #C8C5BE);
199
199
  letter-spacing: 0.02em;
200
200
  }
@@ -202,7 +202,7 @@ img[data-agent]:hover { filter: brightness(1.15); }
202
202
  font-weight: 700;
203
203
  }
204
204
  .agent-model-provider {
205
- font-size: 9.5px;
205
+ font-size: 10px;
206
206
  letter-spacing: 0.16em;
207
207
  text-transform: uppercase;
208
208
  color: var(--text-soft, #8E8B83);
@@ -219,7 +219,7 @@ img[data-agent]:hover { filter: brightness(1.15); }
219
219
  .agent-trait {
220
220
  border: 0.5px solid var(--line-bright, #2A2A26);
221
221
  padding: 3px 8px;
222
- font-size: 9.5px;
222
+ font-size: 10px;
223
223
  font-family: var(--mono);
224
224
  text-transform: uppercase;
225
225
  letter-spacing: 0.1em;
@@ -266,14 +266,14 @@ img[data-agent]:hover { filter: brightness(1.15); }
266
266
  .agent-note-entry:last-child { border-bottom: none; }
267
267
  .agent-note-time {
268
268
  font-family: var(--mono);
269
- font-size: 9.5px;
269
+ font-size: 10px;
270
270
  color: var(--text-faint, #3A382F);
271
271
  letter-spacing: 0.04em;
272
272
  padding-top: 3px;
273
273
  }
274
274
  .agent-note-body {
275
275
  font-family: var(--sans);
276
- font-size: 12.5px;
276
+ font-size: 12px;
277
277
  line-height: 1.5;
278
278
  color: var(--text-soft, #8E8B83);
279
279
  letter-spacing: -0.003em;
@@ -281,7 +281,7 @@ img[data-agent]:hover { filter: brightness(1.15); }
281
281
  .agent-note-tag {
282
282
  display: inline-block;
283
283
  font-family: var(--mono);
284
- font-size: 8.5px;
284
+ font-size: 8px;
285
285
  font-weight: 700;
286
286
  letter-spacing: 0.1em;
287
287
  text-transform: uppercase;
@@ -321,7 +321,7 @@ img[data-agent]:hover { filter: brightness(1.15); }
321
321
  }
322
322
  .agent-memory-empty .lock-text {
323
323
  font-family: var(--sans);
324
- font-size: 11.5px;
324
+ font-size: 12px;
325
325
  line-height: 1.5;
326
326
  color: var(--text-soft, #8E8B83);
327
327
  letter-spacing: -0.003em;
@@ -349,7 +349,7 @@ img[data-agent]:hover { filter: brightness(1.15); }
349
349
  line-height: 1;
350
350
  }
351
351
  .agent-stat .l {
352
- font-size: 8.5px;
352
+ font-size: 8px;
353
353
  text-transform: uppercase;
354
354
  letter-spacing: 0.14em;
355
355
  color: var(--text-faint, #3A382F);
@@ -366,7 +366,7 @@ img[data-agent]:hover { filter: brightness(1.15); }
366
366
  }
367
367
  .agent-card-foot .meta {
368
368
  font-family: var(--mono);
369
- font-size: 9.5px;
369
+ font-size: 10px;
370
370
  color: var(--text-faint, #3A382F);
371
371
  text-transform: uppercase;
372
372
  letter-spacing: 0.12em;
@@ -425,7 +425,7 @@ img[data-agent]:hover { filter: brightness(1.15); }
425
425
  font-family: var(--mono);
426
426
  text-transform: uppercase;
427
427
  letter-spacing: 0.08em;
428
- font-size: 10.5px;
428
+ font-size: 10px;
429
429
  margin-right: 4px;
430
430
  }
431
431
  .agent-locked .lock-link:hover { text-decoration: underline; }