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.
@@ -1173,6 +1173,44 @@
1173
1173
  </button>
1174
1174
  </section>`;
1175
1175
  }
1176
+
1177
+ /** Build log card · sibling to the persona dossier. Surfaces a 1-line
1178
+ * teaser drawn from the narrator's pitch summary plus a CTA that
1179
+ * opens the build-log modal. Hidden when the agent has no
1180
+ * `personaSpec.buildLog` (older Full-mode builds without the
1181
+ * buildLog field; all Signal-mode agents; all seed directors). */
1182
+ function renderBuildLogSection(slug, p) {
1183
+ const live = window.app && window.app.agentsById ? window.app.agentsById[slug] : null;
1184
+ const spec = live && live.personaSpec ? live.personaSpec : null;
1185
+ const buildLog = spec && spec.buildLog ? spec.buildLog : null;
1186
+ if (!buildLog) return "";
1187
+ const narrative = typeof buildLog.narrative === "string" ? buildLog.narrative : "";
1188
+ // Teaser · first ~160 chars of the narrative or a localised
1189
+ // fallback if the narrator pass came back empty. The narrative is
1190
+ // plain prose so we just trim on the nearest whitespace.
1191
+ let teaser = narrative.trim();
1192
+ if (teaser.length === 0) {
1193
+ teaser = uiT("ap_build_log_teaser_fallback");
1194
+ } else if (teaser.length > 160) {
1195
+ const cut = teaser.slice(0, 160);
1196
+ const lastSpace = cut.lastIndexOf(" ");
1197
+ teaser = (lastSpace > 80 ? cut.slice(0, lastSpace) : cut).trim() + "…";
1198
+ }
1199
+ return `
1200
+ <section class="ap-block ap-buildlog-block">
1201
+ <header class="ap-block-h">
1202
+ <span class="ap-block-h-title">${escape(uiT("ap_build_log"))}</span>
1203
+ <span class="ap-block-h-tag">${escape(uiT("ap_build_log_kicker"))}</span>
1204
+ </header>
1205
+ <button type="button" class="ap-buildlog-card" data-ap-buildlog-open data-slug="${escape(slug)}" aria-label="${escape(uiT("ap_build_log_open"))}">
1206
+ <p class="ap-buildlog-teaser">${escape(teaser)}</p>
1207
+ <div class="ap-buildlog-card-cta">
1208
+ <span class="ap-buildlog-card-cta-label">${escape(uiT("ap_build_log_open_cta"))}</span>
1209
+ </div>
1210
+ </button>
1211
+ </section>`;
1212
+ }
1213
+
1176
1214
  function renderRulesInner(slug) {
1177
1215
  const rules = rulesForAgent(slug);
1178
1216
  const list = rules.length === 0
@@ -1939,6 +1977,199 @@
1939
1977
  }
1940
1978
  }
1941
1979
 
1980
+ /* ─── Build-log overlay ────────────────────────────────
1981
+ Sibling to the persona dossier overlay. Reads the buildLog from
1982
+ window.app.agentsById[slug].personaSpec.buildLog (already on the
1983
+ client — the spec rides the agent payload). Renders:
1984
+ · the narrator's pitch summary (hero block)
1985
+ · a 7-phase timeline rail with per-phase blurbs
1986
+ · dimension-card grid stitched under phase 2 from the
1987
+ `dimension-plan` event + matching `search` events
1988
+ · footer stats: voice-uniqueness · tokens · duration
1989
+ Closed on backdrop click or Escape. */
1990
+ let _buildLogOverlayEsc = null;
1991
+ function openBuildLogOverlay(slug, agentName) {
1992
+ closeBuildLogOverlay();
1993
+ const live = window.app && window.app.agentsById ? window.app.agentsById[slug] : null;
1994
+ const spec = live && live.personaSpec ? live.personaSpec : null;
1995
+ const buildLog = spec && spec.buildLog ? spec.buildLog : null;
1996
+ if (!buildLog) return; // safety · the entry point is hidden in this case anyway
1997
+
1998
+ const overlay = document.createElement("div");
1999
+ overlay.id = "ap-buildlog-overlay";
2000
+ overlay.className = "ap-buildlog-overlay";
2001
+ overlay.innerHTML = `
2002
+ <div class="ap-buildlog-overlay-backdrop" data-ap-buildlog-close></div>
2003
+ <div class="ap-buildlog-overlay-modal" role="dialog" aria-modal="true" aria-label="${escape(uiT("ap_build_log"))}">
2004
+ <div class="ap-buildlog-overlay-classification">
2005
+ <span><span class="dot">●</span> ${escape(uiT("ap_build_log_kicker"))}</span>
2006
+ <span class="right">${escape(agentName || "")}</span>
2007
+ </div>
2008
+ <div class="ap-buildlog-overlay-head">
2009
+ <div class="ap-buildlog-overlay-title">${escape(uiT("ap_build_log"))}</div>
2010
+ <div class="ap-buildlog-overlay-actions">
2011
+ <button type="button" class="ap-buildlog-overlay-close" data-ap-buildlog-close aria-label="${escape(uiT("ap_build_close"))}">✕</button>
2012
+ </div>
2013
+ </div>
2014
+ <div class="ap-buildlog-overlay-body">
2015
+ ${renderBuildLogBody(buildLog)}
2016
+ </div>
2017
+ </div>
2018
+ `;
2019
+ document.body.appendChild(overlay);
2020
+ document.body.classList.add("ap-buildlog-overlay-open");
2021
+ _buildLogOverlayEsc = (ev) => {
2022
+ if (ev.key === "Escape") {
2023
+ ev.stopImmediatePropagation();
2024
+ closeBuildLogOverlay();
2025
+ }
2026
+ };
2027
+ document.addEventListener("keydown", _buildLogOverlayEsc, true);
2028
+ }
2029
+
2030
+ function closeBuildLogOverlay() {
2031
+ const el = document.getElementById("ap-buildlog-overlay");
2032
+ if (el) el.remove();
2033
+ document.body.classList.remove("ap-buildlog-overlay-open");
2034
+ if (_buildLogOverlayEsc) {
2035
+ document.removeEventListener("keydown", _buildLogOverlayEsc, true);
2036
+ _buildLogOverlayEsc = null;
2037
+ }
2038
+ }
2039
+
2040
+ /** Render the modal body · narrative hero + 7-phase timeline +
2041
+ * dimension cards under phase 2 + footer stats. Pure HTML string. */
2042
+ function renderBuildLogBody(buildLog) {
2043
+ const events = Array.isArray(buildLog.events) ? buildLog.events : [];
2044
+ const narrative = typeof buildLog.narrative === "string" ? buildLog.narrative.trim() : "";
2045
+
2046
+ // Collect dimensions + searches by walking the event log once.
2047
+ let dimensionPlan = [];
2048
+ const searchesByDim = new Map();
2049
+ const topupSearches = [];
2050
+ const phaseEnd = new Map(); // phase → durationMs
2051
+ let divergenceScore = null;
2052
+ for (const e of events) {
2053
+ if (e.kind === "dimension-plan" && Array.isArray(e.dimensions)) {
2054
+ dimensionPlan = e.dimensions;
2055
+ } else if (e.kind === "search") {
2056
+ if (e.topup) {
2057
+ topupSearches.push(e);
2058
+ } else if (e.dimension) {
2059
+ const cur = searchesByDim.get(e.dimension) || { count: 0, sources: 0, queries: [] };
2060
+ cur.count += 1;
2061
+ cur.sources += (typeof e.pagesRead === "number" ? e.pagesRead : 0);
2062
+ cur.queries.push(e.query);
2063
+ searchesByDim.set(e.dimension, cur);
2064
+ }
2065
+ } else if (e.kind === "phase-end" && typeof e.phase === "number") {
2066
+ phaseEnd.set(e.phase, typeof e.durationMs === "number" ? e.durationMs : 0);
2067
+ } else if (e.kind === "divergence") {
2068
+ divergenceScore = (typeof e.score === "number") ? e.score : null;
2069
+ }
2070
+ }
2071
+
2072
+ // Narrative hero. Empty narrative → show a localised fallback line
2073
+ // so the modal doesn't open with an empty top half.
2074
+ const narrativeHTML = narrative.length > 0
2075
+ ? `<div class="ap-buildlog-narrative">${narrative.split(/\n\n+/).map((p) => `<p>${escape(p.trim())}</p>`).join("")}</div>`
2076
+ : `<div class="ap-buildlog-narrative ap-buildlog-narrative-empty"><p>${escape(uiT("ap_build_log_no_narrative"))}</p></div>`;
2077
+
2078
+ // Timeline · 7 cards. Phase 2 expands to a dimension grid
2079
+ // beneath the card. We render all 7 even if some events are
2080
+ // missing (e.g. aborted-then-resumed builds) — missing phases
2081
+ // just don't show a duration.
2082
+ const phaseCards = [1, 2, 3, 4, 5, 6, 7].map((n) => {
2083
+ const num = String(n).padStart(2, "0");
2084
+ const label = escape(uiT("ap_build_phase_" + n));
2085
+ const blurb = escape(uiT("ap_build_phase_" + n + "_blurb"));
2086
+ const dur = phaseEnd.get(n);
2087
+ const durText = (typeof dur === "number" && dur > 0)
2088
+ ? `<span class="ap-buildlog-phase-dur">${Math.max(1, Math.round(dur / 1000))}s</span>`
2089
+ : "";
2090
+ let extras = "";
2091
+ if (n === 2) {
2092
+ // Dimension grid under the research-phase card.
2093
+ const dimCards = dimensionPlan.map((d) => {
2094
+ const stats = searchesByDim.get(d.dimension) || { sources: 0, count: 0 };
2095
+ const why = d.why ? escape(d.why) : escape(d.query || "");
2096
+ const sources = uiT("ap_build_sources_short", { n: stats.sources });
2097
+ return `
2098
+ <div class="ap-buildlog-dim">
2099
+ <div class="ap-buildlog-dim-name">${escape(d.dimension)}</div>
2100
+ <div class="ap-buildlog-dim-why">${why}</div>
2101
+ <div class="ap-buildlog-dim-stat">${escape(sources)}</div>
2102
+ </div>
2103
+ `;
2104
+ }).join("");
2105
+ const topupBlock = topupSearches.length > 0
2106
+ ? `
2107
+ <div class="ap-buildlog-topup">
2108
+ <div class="ap-buildlog-topup-label">${escape(uiT("ap_build_topup_label"))}</div>
2109
+ <ul class="ap-buildlog-topup-list">
2110
+ ${topupSearches.map((t) => `<li>“${escape(t.query)}” · ${escape(uiT("ap_build_sources_short", { n: typeof t.pagesRead === "number" ? t.pagesRead : 0 }))}</li>`).join("")}
2111
+ </ul>
2112
+ </div>`
2113
+ : "";
2114
+ if (dimensionPlan.length > 0 || topupSearches.length > 0) {
2115
+ extras = `
2116
+ <div class="ap-buildlog-phase-extras">
2117
+ ${dimensionPlan.length > 0 ? `
2118
+ <div class="ap-buildlog-dims-label">${escape(uiT("ap_build_dimensions_label"))}</div>
2119
+ <div class="ap-buildlog-dims-grid">${dimCards}</div>
2120
+ ` : ""}
2121
+ ${topupBlock}
2122
+ </div>
2123
+ `;
2124
+ }
2125
+ }
2126
+ return `
2127
+ <li class="ap-buildlog-phase">
2128
+ <div class="ap-buildlog-phase-head">
2129
+ <span class="ap-buildlog-phase-num">${num}</span>
2130
+ <span class="ap-buildlog-phase-label">${label}</span>
2131
+ ${durText}
2132
+ </div>
2133
+ <p class="ap-buildlog-phase-blurb">${blurb}</p>
2134
+ ${extras}
2135
+ </li>
2136
+ `;
2137
+ }).join("");
2138
+
2139
+ // Footer stats.
2140
+ const totalTokens = typeof buildLog.totalTokens === "number" ? buildLog.totalTokens : 0;
2141
+ const totalDurationMs = Array.from(phaseEnd.values()).reduce((a, b) => a + (typeof b === "number" ? b : 0), 0);
2142
+ const totalDurationSec = Math.round(totalDurationMs / 1000);
2143
+ const divergencePct = (divergenceScore === null || typeof divergenceScore !== "number")
2144
+ ? "—"
2145
+ : (Math.round(divergenceScore * 100) + "%");
2146
+ const tokensFmt = totalTokens > 0 ? totalTokens.toLocaleString() : "—";
2147
+ const durFmt = totalDurationSec > 0
2148
+ ? (totalDurationSec >= 60
2149
+ ? `${Math.floor(totalDurationSec / 60)}m ${totalDurationSec % 60}s`
2150
+ : `${totalDurationSec}s`)
2151
+ : "—";
2152
+
2153
+ return `
2154
+ ${narrativeHTML}
2155
+ <ol class="ap-buildlog-timeline">${phaseCards}</ol>
2156
+ <footer class="ap-buildlog-footer">
2157
+ <div class="ap-buildlog-stat">
2158
+ <div class="ap-buildlog-stat-l">${escape(uiT("ap_build_divergence_label"))}</div>
2159
+ <div class="ap-buildlog-stat-v">${escape(divergencePct)}</div>
2160
+ </div>
2161
+ <div class="ap-buildlog-stat">
2162
+ <div class="ap-buildlog-stat-l">${escape(uiT("ap_build_tokens_label"))}</div>
2163
+ <div class="ap-buildlog-stat-v">${escape(tokensFmt)}</div>
2164
+ </div>
2165
+ <div class="ap-buildlog-stat">
2166
+ <div class="ap-buildlog-stat-l">${escape(uiT("ap_build_duration_label"))}</div>
2167
+ <div class="ap-buildlog-stat-v">${escape(durFmt)}</div>
2168
+ </div>
2169
+ </footer>
2170
+ `;
2171
+ }
2172
+
1942
2173
  /* ─── Profile · ⋯ menu (top-right of the cover) ─────
1943
2174
  Small popover anchored to the menu button with one or more
1944
2175
  actions. v1 ships a single "regenerate 8-bit avatar" item. */
@@ -2889,6 +3120,7 @@
2889
3120
  </section>
2890
3121
 
2891
3122
  ${renderPersonaDossierSection(slug, p)}
3123
+ ${renderBuildLogSection(slug, p)}
2892
3124
 
2893
3125
  <section class="ap-block">
2894
3126
  <header class="ap-block-h">
@@ -3367,6 +3599,28 @@
3367
3599
  return;
3368
3600
  }
3369
3601
 
3602
+ // Build-log card · open the build-log modal. Mirrors the
3603
+ // persona-dossier open/close pattern. The teaser card is a
3604
+ // <button> so the click can land on any child element.
3605
+ const buildLogOpen = e.target.closest("[data-ap-buildlog-open]");
3606
+ if (buildLogOpen) {
3607
+ e.preventDefault();
3608
+ e.stopPropagation();
3609
+ const slug = buildLogOpen.getAttribute("data-slug");
3610
+ if (!slug) return;
3611
+ const live = window.app && window.app.agentsById ? window.app.agentsById[slug] : null;
3612
+ const agentName = live && live.name ? live.name : "";
3613
+ openBuildLogOverlay(slug, agentName);
3614
+ return;
3615
+ }
3616
+ const buildLogClose = e.target.closest("[data-ap-buildlog-close]");
3617
+ if (buildLogClose) {
3618
+ e.preventDefault();
3619
+ e.stopPropagation();
3620
+ closeBuildLogOverlay();
3621
+ return;
3622
+ }
3623
+
3370
3624
  // ⋯ menu · open the popover (anchored to the button).
3371
3625
  const idMenuBtn = e.target.closest("[data-ap-id-menu]");
3372
3626
  if (idMenuBtn) {