privateboard 0.1.8 → 0.1.9

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.
@@ -1198,6 +1198,68 @@
1198
1198
  }
1199
1199
  return r.json();
1200
1200
  }
1201
+ /** Spawn the dream-cycle modal overlay · returns the overlay
1202
+ * element so the caller can mount its body content. Idempotent
1203
+ * on re-entry (the user double-clicked) — closes any prior
1204
+ * overlay first so we never stack them. ESC + backdrop click
1205
+ * + ✕ button all funnel into closeDreamOverlay. */
1206
+ function openDreamOverlay() {
1207
+ closeDreamOverlay();
1208
+ const overlay = document.createElement("div");
1209
+ overlay.className = "dream-overlay";
1210
+ overlay.setAttribute("role", "dialog");
1211
+ overlay.setAttribute("aria-modal", "true");
1212
+ overlay.dataset.dreamOverlay = "1";
1213
+ overlay.innerHTML = `
1214
+ <div class="dream-backdrop" data-dream-close></div>
1215
+ <div class="dream-modal" role="document">
1216
+ <div class="dream-modal-head">
1217
+ <span class="dream-modal-kicker"><span class="dream-modal-glyph">☾</span> memory · sleep mode</span>
1218
+ <button type="button" class="dream-modal-close" data-dream-close aria-label="Close">✕</button>
1219
+ </div>
1220
+ <div class="dream-modal-body" data-dream-body></div>
1221
+ </div>
1222
+ `;
1223
+ document.body.appendChild(overlay);
1224
+ document.body.style.overflow = "hidden";
1225
+ // Backdrop / ✕ click → close. Listener bound on the overlay
1226
+ // root, delegated to anything carrying [data-dream-close].
1227
+ overlay.addEventListener("click", (ev) => {
1228
+ if (ev.target.closest && ev.target.closest("[data-dream-close]")) {
1229
+ ev.preventDefault();
1230
+ closeDreamOverlay();
1231
+ }
1232
+ });
1233
+ // ESC closes too. Detached on close so we don't accumulate
1234
+ // listeners across opens.
1235
+ overlay.__dreamEsc = (ev) => {
1236
+ if (ev.key === "Escape") {
1237
+ ev.stopImmediatePropagation();
1238
+ closeDreamOverlay();
1239
+ }
1240
+ };
1241
+ document.addEventListener("keydown", overlay.__dreamEsc, true);
1242
+ return overlay;
1243
+ }
1244
+ function closeDreamOverlay() {
1245
+ const overlay = document.querySelector('[data-dream-overlay="1"]');
1246
+ if (!overlay) return;
1247
+ if (overlay.__dreamPhaseTimer) {
1248
+ clearInterval(overlay.__dreamPhaseTimer);
1249
+ overlay.__dreamPhaseTimer = null;
1250
+ }
1251
+ if (overlay.__dreamAutoClose) {
1252
+ clearTimeout(overlay.__dreamAutoClose);
1253
+ overlay.__dreamAutoClose = null;
1254
+ }
1255
+ if (overlay.__dreamEsc) {
1256
+ document.removeEventListener("keydown", overlay.__dreamEsc, true);
1257
+ overlay.__dreamEsc = null;
1258
+ }
1259
+ overlay.remove();
1260
+ document.body.style.overflow = "";
1261
+ }
1262
+
1201
1263
  /** DELETE /api/agents/:slug/memories/:id */
1202
1264
  async function deleteMemoryFor(slug, id) {
1203
1265
  const r = await fetch(
@@ -2300,7 +2362,10 @@
2300
2362
  <section class="ap-block">
2301
2363
  <header class="ap-block-h">
2302
2364
  <span class="ap-block-h-title">Memory</span>
2303
- <button type="button" class="ap-block-h-action" data-ap-memory-add-toggle data-slug="${escape(slug)}">+ add note</button>
2365
+ <div class="ap-block-h-actions">
2366
+ <button type="button" class="ap-block-h-action ap-dream-trigger" data-ap-dream-trigger data-slug="${escape(slug)}" title="Consolidate · drop stale notes, keep stable patterns">☾ run consolidation</button>
2367
+ <button type="button" class="ap-block-h-action" data-ap-memory-add-toggle data-slug="${escape(slug)}">+ add note</button>
2368
+ </div>
2304
2369
  </header>
2305
2370
  ${renderMemoryBlock(slug)}
2306
2371
  </section>
@@ -2626,6 +2691,14 @@
2626
2691
  document.addEventListener("click", (e) => {
2627
2692
  const trigger = e.target.closest("[data-agent-profile]");
2628
2693
  if (!trigger) return;
2694
+ // Per-row action buttons (pin toggle, future delete / overflow
2695
+ // glyphs that live INSIDE the agent-row anchor) need their own
2696
+ // click to land. Without this guard, the capture-phase
2697
+ // stopPropagation below swallows the action's bubble path and
2698
+ // opens the profile instead — making "click pin → nothing
2699
+ // happens" the visible bug. Bail out here so the action's own
2700
+ // delegated handler runs as intended.
2701
+ if (e.target.closest("[data-pin-toggle], [data-row-action]")) return;
2629
2702
  const slug = trigger.dataset.agentProfile;
2630
2703
  if (!slug) return;
2631
2704
  // Accept either a hardcoded profile or any live agent the app
@@ -2854,6 +2927,210 @@
2854
2927
  .catch((err) => alert("Couldn't delete: " + (err && err.message ? err.message : err)));
2855
2928
  return;
2856
2929
  }
2930
+ // Memory · manual consolidation trigger (chair "⊘ run consolidation"
2931
+ // button). Hits POST /api/agents/:id/dream, shows the before/after
2932
+ // counts inline for a few seconds, refreshes the memory list. The
2933
+ // button is disabled while the cycle runs to prevent double-click.
2934
+ const dreamTrigger = e.target.closest("[data-ap-dream-trigger]");
2935
+ if (dreamTrigger) {
2936
+ e.preventDefault();
2937
+ const slug = dreamTrigger.getAttribute("data-slug");
2938
+ if (!slug || dreamTrigger.disabled) return;
2939
+ const origLabel = dreamTrigger.textContent;
2940
+ dreamTrigger.disabled = true;
2941
+ dreamTrigger.textContent = "consolidating…";
2942
+ // Spawn the overlay · running widget mounted inside.
2943
+ // Earlier rev rendered the widget inline above the memory
2944
+ // list which felt cramped and shifted the page layout.
2945
+ // The overlay treatment matches the adjourn / supplement
2946
+ // modals' chrome so the dream reads as a proper "agent is
2947
+ // sleeping now" event with focused attention.
2948
+ const overlay = openDreamOverlay();
2949
+ const body = overlay.querySelector("[data-dream-body]");
2950
+ body.innerHTML = `
2951
+ <div class="dream-stage-pad">
2952
+ <div class="dream-section-kicker">
2953
+ <span class="dream-kicker-text">// running</span>
2954
+ <span class="dream-step" data-dream-step>1 / 6</span>
2955
+ </div>
2956
+ <div class="dream-frame" data-dream-state="running">
2957
+ <div class="dream-sky" aria-hidden="true">
2958
+ <span class="dream-z z1">Z</span>
2959
+ <span class="dream-z z2">z</span>
2960
+ <span class="dream-z z3">z</span>
2961
+ <span class="dream-z z4">Z</span>
2962
+ <span class="dream-z z5">z</span>
2963
+ </div>
2964
+ <div class="dream-lanes" aria-hidden="true">
2965
+ <div class="dream-lane lane-decay">
2966
+ <span class="dream-lane-label">decay</span>
2967
+ <span class="dream-lane-bar"><i></i></span>
2968
+ </div>
2969
+ <div class="dream-lane lane-merge">
2970
+ <span class="dream-lane-label">merge</span>
2971
+ <span class="dream-lane-bar"><i></i></span>
2972
+ </div>
2973
+ <div class="dream-lane lane-promote">
2974
+ <span class="dream-lane-label">promote</span>
2975
+ <span class="dream-lane-bar"><i></i></span>
2976
+ </div>
2977
+ </div>
2978
+ </div>
2979
+ <div class="dream-divider" aria-hidden="true"></div>
2980
+ <div class="dream-caption">
2981
+ <span class="dream-phase" data-dream-phase>scanning recent extractions…</span>
2982
+ </div>
2983
+ </div>
2984
+ `;
2985
+ // Rotate phase text every 1.6s · matches the actual
2986
+ // pipeline stages so the user's mental model of "what's
2987
+ // happening" tracks the backend.
2988
+ const phases = [
2989
+ "scanning recent extractions…",
2990
+ "clustering near-duplicates…",
2991
+ "merging clusters into canonical notes…",
2992
+ "resolving contradictions…",
2993
+ "promoting stable cross-room patterns…",
2994
+ "settling memory…",
2995
+ ];
2996
+ let phaseIdx = 0;
2997
+ const phaseTimer = setInterval(() => {
2998
+ phaseIdx = (phaseIdx + 1) % phases.length;
2999
+ const phaseEl = overlay.querySelector("[data-dream-phase]");
3000
+ const stepEl = overlay.querySelector("[data-dream-step]");
3001
+ if (phaseEl) phaseEl.textContent = phases[phaseIdx];
3002
+ if (stepEl) stepEl.textContent = `${phaseIdx + 1} / ${phases.length}`;
3003
+ }, 1600);
3004
+ // Pin the timer on the overlay so closeDreamOverlay can
3005
+ // also clear it (user can dismiss mid-run via ✕ / ESC /
3006
+ // backdrop and we shouldn't keep the interval ticking).
3007
+ overlay.__dreamPhaseTimer = phaseTimer;
3008
+ fetch("/api/agents/" + encodeURIComponent(slug) + "/dream", {
3009
+ method: "POST",
3010
+ headers: { "content-type": "application/json" },
3011
+ body: JSON.stringify({}),
3012
+ })
3013
+ .then(async (r) => {
3014
+ const j = await r.json().catch(() => ({}));
3015
+ if (!r.ok) throw new Error(j.error || ("HTTP " + r.status));
3016
+ return j;
3017
+ })
3018
+ .then((j) => {
3019
+ const s = j.summary || {};
3020
+ const before = s.beforeCount ?? 0;
3021
+ const after = s.afterCount ?? 0;
3022
+ const decayed = s.decayed ?? 0;
3023
+ const merged = s.merged ?? 0;
3024
+ const promoted = s.promoted ?? 0;
3025
+ const superseded = s.superseded ?? 0;
3026
+ // Stop phase rotation now that we're swapping to result.
3027
+ if (overlay.__dreamPhaseTimer) {
3028
+ clearInterval(overlay.__dreamPhaseTimer);
3029
+ overlay.__dreamPhaseTimer = null;
3030
+ }
3031
+ // Flip the modal class so the chrome (border / kicker
3032
+ // tint) follows the resolved state, then swap body.
3033
+ overlay.classList.add("is-done");
3034
+ const allZero = decayed === 0 && merged === 0 && promoted === 0 && superseded === 0;
3035
+ const delta = before - after;
3036
+ if (allZero) {
3037
+ // No-op result · still uses the full stage layout so
3038
+ // the modal doesn't visually shrink between phases.
3039
+ // Hero is the moon glyph + "settled" headline; the
3040
+ // bottom half is a quiet caption acknowledging the
3041
+ // already-tidy state.
3042
+ body.innerHTML = `
3043
+ <div class="dream-stage-pad is-quiet">
3044
+ <div class="dream-section-kicker">
3045
+ <span class="dream-kicker-text">// settled</span>
3046
+ <span class="dream-step is-quiet">no change</span>
3047
+ </div>
3048
+ <div class="dream-hero is-quiet">
3049
+ <span class="dream-hero-glyph">☾</span>
3050
+ <div class="dream-hero-text">
3051
+ <div class="dream-hero-title">Memory is already settled</div>
3052
+ <div class="dream-hero-sub">${after} note${after === 1 ? "" : "s"} · nothing to consolidate</div>
3053
+ </div>
3054
+ </div>
3055
+ <div class="dream-caption">
3056
+ <span class="dream-caption-foot">no duplicates · no contradictions · pile is clean.</span>
3057
+ </div>
3058
+ </div>
3059
+ `;
3060
+ } else {
3061
+ // Non-trivial result · hero shows the count delta
3062
+ // (was → now framed) with a magnitude chip; tiles
3063
+ // below break the change down per operation.
3064
+ const tile = (n, label, cls) =>
3065
+ `<div class="dream-tile ${cls}${n === 0 ? " is-empty" : ""}">
3066
+ <span class="dream-tile-n">${n}</span>
3067
+ <span class="dream-tile-l">${label}</span>
3068
+ </div>`;
3069
+ const deltaLabel = delta > 0 ? `−${delta}` : delta < 0 ? `+${Math.abs(delta)}` : "±0";
3070
+ const deltaTone = delta > 0 ? "is-shrink" : delta < 0 ? "is-grow" : "is-flat";
3071
+ body.innerHTML = `
3072
+ <div class="dream-stage-pad">
3073
+ <div class="dream-section-kicker">
3074
+ <span class="dream-kicker-text">// consolidated</span>
3075
+ <span class="dream-step ${deltaTone}">${deltaLabel}</span>
3076
+ </div>
3077
+ <div class="dream-hero">
3078
+ <div class="dream-hero-numerals">
3079
+ <span class="dream-num was">${before}</span>
3080
+ <span class="dream-num-arrow" aria-hidden="true">→</span>
3081
+ <span class="dream-num-frame">
3082
+ <span class="dream-num now">${after}</span>
3083
+ </span>
3084
+ </div>
3085
+ <span class="dream-hero-unit">note${after === 1 ? "" : "s"}</span>
3086
+ </div>
3087
+ <div class="dream-divider" aria-hidden="true"></div>
3088
+ <div class="dream-tiles">
3089
+ ${tile(decayed, "decayed", "is-decay")}
3090
+ ${tile(merged, "merged", "is-merge")}
3091
+ ${tile(superseded, "superseded", "is-supersede")}
3092
+ ${tile(promoted, "promoted", "is-promote")}
3093
+ </div>
3094
+ </div>
3095
+ `;
3096
+ }
3097
+ // Auto-dismiss after a longer beat so the user has
3098
+ // time to read the tiles. Manual dismissal still
3099
+ // works via ✕ / ESC / backdrop.
3100
+ overlay.__dreamAutoClose = setTimeout(() => closeDreamOverlay(), 9000);
3101
+ return loadMemoriesFor(slug);
3102
+ })
3103
+ .catch((err) => {
3104
+ if (overlay.__dreamPhaseTimer) {
3105
+ clearInterval(overlay.__dreamPhaseTimer);
3106
+ overlay.__dreamPhaseTimer = null;
3107
+ }
3108
+ overlay.classList.add("is-error");
3109
+ body.innerHTML = `
3110
+ <div class="dream-stage-pad is-quiet">
3111
+ <div class="dream-section-kicker">
3112
+ <span class="dream-kicker-text">// failed</span>
3113
+ <span class="dream-step is-error">error</span>
3114
+ </div>
3115
+ <div class="dream-hero is-quiet">
3116
+ <span class="dream-hero-glyph">✕</span>
3117
+ <div class="dream-hero-text">
3118
+ <div class="dream-hero-title">Consolidation failed</div>
3119
+ <div class="dream-hero-sub">${escape(err && err.message ? err.message : String(err))}</div>
3120
+ </div>
3121
+ </div>
3122
+ <div class="dream-caption">
3123
+ <span class="dream-caption-foot">memory pile unchanged · safe to retry.</span>
3124
+ </div>
3125
+ </div>
3126
+ `;
3127
+ })
3128
+ .finally(() => {
3129
+ dreamTrigger.disabled = false;
3130
+ dreamTrigger.textContent = origLabel;
3131
+ });
3132
+ return;
3133
+ }
2857
3134
  // Memory · toggle the add form (mirrors the Rules add pattern · the
2858
3135
  // input area is hidden by default and only revealed when the user
2859
3136
  // clicks the [+ add note] section action).