bopbop 0.0.0-stage → 0.1.0

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.
@@ -0,0 +1,18 @@
1
+ // Presets for the emulate tool: devices (CSS px, like DevTools device mode) and network throttling.
2
+ const IOS_UA = 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1';
3
+ const ANDROID_UA = 'Mozilla/5.0 (Linux; Android 15; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Mobile Safari/537.36';
4
+ export const DEVICES = {
5
+ 'iphone-15': { width: 393, height: 852, deviceScaleFactor: 3, mobile: true, ua: IOS_UA },
6
+ 'iphone-se': { width: 375, height: 667, deviceScaleFactor: 2, mobile: true, ua: IOS_UA },
7
+ 'pixel-8': { width: 412, height: 915, deviceScaleFactor: 2.625, mobile: true, ua: ANDROID_UA },
8
+ ipad: { width: 820, height: 1180, deviceScaleFactor: 2, mobile: true, ua: IOS_UA.replace('iPhone; CPU iPhone OS', 'iPad; CPU OS') },
9
+ laptop: { width: 1280, height: 800, deviceScaleFactor: 1, mobile: false },
10
+ desktop: { width: 1440, height: 900, deviceScaleFactor: 1, mobile: false },
11
+ };
12
+ export const NETWORKS = {
13
+ offline: { offline: true, latency: 0, downloadThroughput: 0, uploadThroughput: 0 },
14
+ 'slow-3g': { offline: false, latency: 2000, downloadThroughput: ((500 * 1024) / 8) * 0.8, uploadThroughput: ((500 * 1024) / 8) * 0.8 },
15
+ 'fast-3g': { offline: false, latency: 563, downloadThroughput: ((1.6 * 1024 * 1024) / 8) * 0.9, uploadThroughput: ((750 * 1024) / 8) * 0.9 },
16
+ '4g': { offline: false, latency: 150, downloadThroughput: (9 * 1024 * 1024) / 8, uploadThroughput: (1.5 * 1024 * 1024) / 8 },
17
+ none: { offline: false, latency: 0, downloadThroughput: -1, uploadThroughput: -1 },
18
+ };
@@ -0,0 +1,103 @@
1
+ // Line diff for snapshot deltas (Myers O(ND)), rendered as compact hunks.
2
+ const CONTEXT = 1;
3
+ const MAX_D = 3000;
4
+ /** Returns ops: array of [' '|'-'|'+', line]. Falls back to a multiset diff for huge edit distances. */
5
+ function myers(a, b) {
6
+ const n = a.length, m = b.length, max = Math.min(n + m, MAX_D);
7
+ const v = new Int32Array(2 * max + 2);
8
+ const trace = [];
9
+ const off = max + 1;
10
+ for (let d = 0; d <= max; d++) {
11
+ trace.push(v.slice());
12
+ for (let k = -d; k <= d; k += 2) {
13
+ let x = k === -d || (k !== d && v[off + k - 1] < v[off + k + 1]) ? v[off + k + 1] : v[off + k - 1] + 1;
14
+ let y = x - k;
15
+ while (x < n && y < m && a[x] === b[y]) {
16
+ x++;
17
+ y++;
18
+ }
19
+ v[off + k] = x;
20
+ if (x >= n && y >= m)
21
+ return backtrack(trace, a, b, off);
22
+ }
23
+ }
24
+ return null;
25
+ }
26
+ function backtrack(trace, a, b, off) {
27
+ const ops = [];
28
+ let x = a.length, y = b.length;
29
+ for (let d = trace.length - 1; d >= 0; d--) {
30
+ const v = trace[d];
31
+ const k = x - y;
32
+ const prevK = k === -d || (k !== d && v[off + k - 1] < v[off + k + 1]) ? k + 1 : k - 1;
33
+ const px = v[off + prevK], py = px - prevK;
34
+ while (x > px && y > py) {
35
+ ops.push([' ', a[--x]]);
36
+ y--;
37
+ }
38
+ if (d > 0) {
39
+ if (x === px)
40
+ ops.push(['+', b[--y]]);
41
+ else
42
+ ops.push(['-', a[--x]]);
43
+ }
44
+ }
45
+ return ops.reverse();
46
+ }
47
+ function multiset(a, b) {
48
+ const count = new Map();
49
+ for (const l of b)
50
+ count.set(l, (count.get(l) || 0) + 1);
51
+ const ops = [];
52
+ for (const l of a) {
53
+ const c = count.get(l);
54
+ if (c)
55
+ count.set(l, c - 1);
56
+ else
57
+ ops.push(['-', l]);
58
+ }
59
+ const ca = new Map();
60
+ for (const l of a)
61
+ ca.set(l, (ca.get(l) || 0) + 1);
62
+ for (const l of b) {
63
+ const c = ca.get(l);
64
+ if (c)
65
+ ca.set(l, c - 1);
66
+ else
67
+ ops.push(['+', l]);
68
+ }
69
+ return ops;
70
+ }
71
+ export function diffLines(prev, next) {
72
+ const ops = myers(prev, next) || multiset(prev, next);
73
+ let added = 0, removed = 0;
74
+ for (const [t] of ops) {
75
+ if (t === '+')
76
+ added++;
77
+ else if (t === '-')
78
+ removed++;
79
+ }
80
+ if (!added && !removed)
81
+ return { changed: false, added, removed, text: '' };
82
+ // keep changed lines + CONTEXT lines around them
83
+ const keep = new Uint8Array(ops.length);
84
+ ops.forEach(([t], i) => {
85
+ if (t === ' ')
86
+ return;
87
+ for (let j = Math.max(0, i - CONTEXT); j <= Math.min(ops.length - 1, i + CONTEXT); j++)
88
+ keep[j] = 1;
89
+ });
90
+ const out = [];
91
+ let gap = false;
92
+ ops.forEach(([t, l], i) => {
93
+ if (!keep[i]) {
94
+ gap = true;
95
+ return;
96
+ }
97
+ if (gap && out.length)
98
+ out.push('…');
99
+ gap = false;
100
+ out.push(`${t} ${l}`);
101
+ });
102
+ return { changed: true, added, removed, text: out.join('\n') };
103
+ }
@@ -0,0 +1,35 @@
1
+ // Tab group names: "Agent:<code>". The code is 1-5 letters, digits or emoji (an emoji counts as one), short enough
2
+ // for a tab group chip and different per session. Default: the project's initials + the session's slot number.
3
+ import { basename } from 'node:path';
4
+ const graphemes = (s) => [...new Intl.Segmenter(undefined, { granularity: 'grapheme' }).segment(s)].map((g) => g.segment);
5
+ const isLetterOrDigit = (g) => /^[\p{L}\p{N}]\p{M}*$/u.test(g);
6
+ const isEmoji = (g) => /\p{Extended_Pictographic}|\p{Regional_Indicator}/u.test(g) && !/[\p{L}\p{N}]/u.test(g.replace(/\p{Emoji_Component}/gu, '').normalize());
7
+ /** The label, trimmed; throws unless it is 1-5 letters, digits or emoji. */
8
+ export function checkLabel(label) {
9
+ const s = label.trim();
10
+ const gs = graphemes(s);
11
+ if (gs.length < 1 || gs.length > 5 || !gs.every((g) => isLetterOrDigit(g) || isEmoji(g))) {
12
+ throw new Error('label must be 1-5 letters, digits or emoji, e.g. CT2, ALD, 🧪 or 🦊🔥 (an emoji counts as one)');
13
+ }
14
+ return s;
15
+ }
16
+ /** Project initials (1-3 letters, or the first 2-3 letters of a one-word name) + slot number (1-based). */
17
+ export function defaultLabel(projectDir, slot) {
18
+ const words = basename(projectDir)
19
+ .replace(/([a-z])([A-Z])/g, '$1 $2')
20
+ .split(/[^\p{L}\p{N}]+/u)
21
+ .filter((w) => /^\p{L}/u.test(w));
22
+ return abbreviate(words).toUpperCase() + String(slot + 1);
23
+ }
24
+ function abbreviate(words) {
25
+ if (words.length > 1)
26
+ return words
27
+ .slice(0, 3)
28
+ .map((w) => graphemes(w)[0])
29
+ .join('');
30
+ if (words.length === 1)
31
+ return graphemes(words[0])
32
+ .slice(0, words[0].length > 3 ? 3 : 2)
33
+ .join('');
34
+ return 'BB'; // no usable name
35
+ }
@@ -0,0 +1,44 @@
1
+ // Network formatting: cookie redaction, sizes, durations and DevTools-style timing phases.
2
+ // Cookie and credential header values are often login sessions or keys: shown as a prefix + length unless BBM_SHOW_COOKIES=1 (a server
3
+ // setting on purpose, so a page cannot talk the model into revealing them). Applies to the details view and HAR files.
4
+ export const showCookies = () => process.env.BBM_SHOW_COOKIES === '1';
5
+ export const redact = (v) => (showCookies() || v.length <= 6 ? v : `${v.slice(0, 4)}…(${v.length} chars)`);
6
+ // Headers that carry credentials (tokens, keys) are shortened like cookie values.
7
+ const CREDENTIAL_HEADER = /^(authorization|proxy-authorization|x-api-key|api-key|x-auth-token|x-access-token|x-csrf-token|x-xsrf-token|x-amz-security-token)$|token|secret|api[-_]?key|password/i;
8
+ export const redactHeader = (name, value) => {
9
+ const n = name.toLowerCase();
10
+ if (CREDENTIAL_HEADER.test(n))
11
+ return String(value)
12
+ .split('\n')
13
+ .map((v) => redact(v))
14
+ .join('\n');
15
+ if (n === 'cookie')
16
+ return String(value)
17
+ .split(/;\s*/)
18
+ .map((kv) => {
19
+ const i = kv.indexOf('=');
20
+ return i < 0 ? kv : `${kv.slice(0, i)}=${redact(kv.slice(i + 1))}`;
21
+ })
22
+ .join('; ');
23
+ if (n === 'set-cookie')
24
+ return String(value)
25
+ .split('\n')
26
+ .map((line) => line.replace(/^([^=;]+)=([^;]*)/, (_, k, v) => `${k}=${redact(v)}`))
27
+ .join('\n');
28
+ return value;
29
+ };
30
+ export const kb = (n) => (n == null ? '' : n < 1024 ? `${n}B` : `${(n / 1024).toFixed(1)}kB`);
31
+ export const durMs = (e) => (e.end != null ? Math.round((e.end - e.ts) * 1000) : null);
32
+ /** DevTools-style phases from response.timing (ms). */
33
+ export function phases(t) {
34
+ if (!t)
35
+ return null;
36
+ const d = (a, b) => (t[a] >= 0 && t[b] >= 0 ? Math.round(t[b] - t[a]) : 0);
37
+ return {
38
+ dns: d('dnsStart', 'dnsEnd'),
39
+ connect: d('connectStart', 'connectEnd'),
40
+ tls: d('sslStart', 'sslEnd'),
41
+ send: d('sendStart', 'sendEnd'),
42
+ wait: d('sendEnd', 'receiveHeadersEnd'),
43
+ };
44
+ }