@khanglvm/relay 0.12.0 → 0.12.1

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": "@khanglvm/relay",
3
- "version": "0.12.0",
3
+ "version": "0.12.1",
4
4
  "description": "Question boards with rich blocks (markdown, charts, mermaid, tables, code, diffs, video, sandboxed HTML), clickable local file-links, and element-level annotations for AI coding agents (Claude Code, Codex, …): ask users structured questions, present interactive visuals, collect inline comments, read answers as JSON — in a local browser board OR rendered INLINE inside the Claude & Codex apps as an MCP App (SEP-1865).",
5
5
  "keywords": [
6
6
  "ai-agents",
@@ -107,6 +107,9 @@
107
107
  let popOpen = false;
108
108
  let popScrollY = 0;
109
109
  let popSave = null;
110
+ // Frozen by the host (e.g. relay's connection-lost block): suppress every way
111
+ // to START a comment, so the user can't type feedback that won't be saved.
112
+ let disabled = false;
110
113
  let badgeTimer = 0;
111
114
  let railOpen = false;
112
115
 
@@ -377,6 +380,7 @@
377
380
 
378
381
  // ---------- hover pin ----------
379
382
  function showPin(entry) {
383
+ if (disabled) return;
380
384
  clearTimeout(pinTimer);
381
385
  pinEntry = entry;
382
386
  const rect = entry.el.getBoundingClientRect();
@@ -483,6 +487,7 @@
483
487
  }
484
488
 
485
489
  function openPopover(info, anchorEl) {
490
+ if (disabled) return;
486
491
  ensureDom();
487
492
  closePopover();
488
493
  hidePin();
@@ -641,6 +646,7 @@
641
646
  }
642
647
 
643
648
  function maybeShowSelBtn(rootEl, baseInfo) {
649
+ if (disabled) return hideSelBtn();
644
650
  const sel = window.getSelection();
645
651
  if (!sel || sel.isCollapsed || !sel.rangeCount) return hideSelBtn();
646
652
  if (!rootEl.contains(sel.anchorNode) || !rootEl.contains(sel.focusNode)) return hideSelBtn();
@@ -851,11 +857,28 @@
851
857
  return popOpen;
852
858
  }
853
859
 
860
+ // Commit a mid-typed comment (the open popover's "Add a comment" box) as a
861
+ // saved annotation, then close it — so freezing the board doesn't throw away
862
+ // text the user was in the middle of writing. ponytail: only the main comment
863
+ // box is flushed, not an in-progress thread reply (the rare case).
864
+ function flushOpen() {
865
+ if (popOpen && popSave) { try { popSave(); } catch { closePopover(); } }
866
+ }
867
+
868
+ // Freeze/unfreeze comment creation. While disabled, no pin, selection button,
869
+ // or popover appears, so the user can't start feedback that won't be saved.
870
+ function setDisabled(v) {
871
+ disabled = Boolean(v);
872
+ // Also close the rail flushOpen() may have just opened — otherwise it covers
873
+ // the host's top-right "connection lost" notice.
874
+ if (disabled) { closePopover(); hideSelBtn(); hidePin(); closeRail(); }
875
+ }
876
+
854
877
  function renderSummary(target) {
855
878
  ensureDom();
856
879
  summaryEls.add(target);
857
880
  renderSummaryInto(target);
858
881
  }
859
882
 
860
- window.RelayAnnotate = { init, register, enableTextSelection, openExternal, list, renderSummary, onBadgeRefresh, teardown, isComposing };
883
+ window.RelayAnnotate = { init, register, enableTextSelection, openExternal, list, renderSummary, onBadgeRefresh, teardown, isComposing, flushOpen, setDisabled };
861
884
  })();
package/src/ui/app.js CHANGED
@@ -271,13 +271,15 @@
271
271
  // board's local HTTP server (server gone, port taken over, socket dropped,
272
272
  // machine slept). Autosaves then fail silently and the user keeps typing
273
273
  // answers/comments that are never persisted, then Submit fails too — all of it
274
- // thrown away. When persistence is CONFIRMED lost we hard-block: disable every
275
- // control and overlay an unmissable scrim with a Retry. Local `state` is never
276
- // touched, so the moment the connection recovers we flush it and unblock —
277
- // nothing the user typed during the outage is lost.
274
+ // thrown away. When persistence is CONFIRMED lost we DISABLE every control (and
275
+ // freeze new comments) and show a small, non-dismissable top-right notice with
276
+ // a Reconnect button without blocking the page, so the user can still read
277
+ // and scroll. Local `state` is never touched, and a mid-typed comment is saved
278
+ // before the freeze, so nothing the user entered during the outage is lost; the
279
+ // moment the connection recovers we flush state and re-enable everything.
278
280
  let persistenceLost = false;
279
281
  let probing = false;
280
- let lostOverlay = null;
282
+ let lostNote = null;
281
283
  let lostRetryBtn = null;
282
284
 
283
285
  // A direct, side-effect-free reachability probe. Resolves true when the local
@@ -293,46 +295,55 @@
293
295
  }
294
296
  }
295
297
 
296
- function buildLostOverlay() {
297
- if (lostOverlay) return lostOverlay;
298
- lostRetryBtn = el('button', { class: 'lost-retry', type: 'button' }, 'Retry connection');
298
+ function buildLostNote() {
299
+ if (lostNote) return lostNote;
300
+ lostRetryBtn = el('button', { class: 'lost-retry', type: 'button' }, 'Reconnect');
299
301
  lostRetryBtn.addEventListener('click', retryConnection);
300
- lostOverlay = el('div', { class: 'lost-overlay', role: 'alertdialog', 'aria-modal': 'true', 'aria-label': 'Connection lost' },
301
- el('div', { class: 'lost-card' },
302
- el('div', { class: 'lost-mark' }, '⚠'),
303
- el('h2', {}, 'Connection lost — input isn’t being saved'),
304
- el('p', {}, 'This board can no longer reach your agent’s session, so anything you type now won’t be saved. Editing is paused to keep you from losing work.'),
305
- el('p', { class: 'lost-sub' }, 'Your input up to this point is kept in this tab. Click Retry once the agent’s session is back, or prompt the agent to reopen this board — your draft and unsaved edits will be restored.'),
306
- lostRetryBtn
307
- )
302
+ lostNote = el('div', { class: 'lost-note', role: 'status', 'aria-live': 'polite' },
303
+ el('span', { class: 'lost-note-dot', 'aria-hidden': 'true' }),
304
+ el('div', { class: 'lost-note-body' },
305
+ el('div', { class: 'lost-note-title' }, 'Connection lost'),
306
+ el('div', { class: 'lost-note-sub' }, 'Editing paused your input is kept.')
307
+ ),
308
+ lostRetryBtn
308
309
  );
309
- return lostOverlay;
310
+ return lostNote;
310
311
  }
311
312
 
312
- // Enter the blocked state: disable controls, mount the overlay. Idempotent.
313
+ // Enter the blocked state: save a mid-typed comment, freeze comments, disable
314
+ // controls, and pin the small notice. Idempotent. Does NOT block the page —
315
+ // the user can still read and scroll; only editing is paused.
313
316
  function blockForLostPersistence() {
314
317
  if (persistenceLost || submitted) return;
315
318
  persistenceLost = true;
316
319
  document.documentElement.classList.add('relay-blocked');
317
- // Disable every interactive control inside the form (inputs the user could
318
- // otherwise keep typing into) plus Submit.
320
+ // Save whatever the user was typing into the comment popover BEFORE freezing
321
+ // it, so the in-progress comment is kept (in `state` + localStorage) instead
322
+ // of discarded; then stop any new comment from being started.
323
+ if (Annotate) {
324
+ if (typeof Annotate.flushOpen === 'function') { try { Annotate.flushOpen(); } catch { /* non-fatal */ } }
325
+ if (typeof Annotate.setDisabled === 'function') { try { Annotate.setDisabled(true); } catch { /* non-fatal */ } }
326
+ }
327
+ // Disable every interactive control inside the form. Values already entered
328
+ // stay in the DOM (and in `state`), so nothing typed so far is lost.
319
329
  for (const node of app.querySelectorAll('input, textarea, button, select')) {
320
330
  node.disabled = true;
321
331
  }
322
- document.body.append(buildLostOverlay());
332
+ document.body.append(buildLostNote());
323
333
  if (saveEl) saveEl.textContent = 'connection lost — not saving';
324
334
  }
325
335
 
326
- // Leave the blocked state: re-enable controls, remove the overlay, and flush
327
- // whatever the user typed during the outage so it's persisted right away.
336
+ // Leave the blocked state: re-enable controls + comments, drop the notice, and
337
+ // flush whatever the user typed during the outage so it's persisted right away.
328
338
  function unblockAfterRecovery() {
329
339
  if (!persistenceLost) return;
330
340
  persistenceLost = false;
331
341
  document.documentElement.classList.remove('relay-blocked');
342
+ if (Annotate && typeof Annotate.setDisabled === 'function') { try { Annotate.setDisabled(false); } catch { /* non-fatal */ } }
332
343
  for (const node of app.querySelectorAll('input, textarea, button, select')) {
333
344
  node.disabled = false;
334
345
  }
335
- if (lostOverlay) lostOverlay.remove();
346
+ if (lostNote) lostNote.remove();
336
347
  // Re-arm the heartbeat (it stops itself when it confirms loss) and persist
337
348
  // everything typed during the outage. saveDraft() updates the save label.
338
349
  startHeartbeat();
package/src/ui/style.css CHANGED
@@ -282,46 +282,35 @@ textarea { min-height: 90px; resize: vertical; }
282
282
  .scale button { width: 40px; height: 40px; }
283
283
  }
284
284
 
285
- /* Persistence-lost block: when the board can no longer save the user's input
285
+ /* Persistence-lost notice: when the board can no longer save the user's input
286
286
  (server gone / port taken / machine slept) AND recovery probes have failed,
287
- we disable every control and overlay this unmissable scrim so the user is
288
- physically prevented from typing feedback that would be silently discarded.
289
- Sits above everything (full-screen overlay z 49 / popover / rail / toast). */
290
- .relay-blocked #app { filter: grayscale(0.4) blur(1px); pointer-events: none; user-select: none; }
291
- .lost-overlay {
292
- position: fixed; inset: 0; z-index: 60;
293
- display: flex; align-items: center; justify-content: center;
294
- padding: 24px;
295
- background: color-mix(in srgb, var(--bg) 78%, transparent);
296
- backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px);
297
- }
298
- .lost-card {
299
- max-width: 460px; width: 100%;
287
+ we disable every control (and freeze new comments) and pin this SMALL,
288
+ non-dismissable note top-right with a Reconnect button. It does NOT block the
289
+ page the user can still read and scroll; only editing is paused. Recovery
290
+ re-enables everything and removes the note. */
291
+ .lost-note {
292
+ position: fixed; top: 12px; right: 12px; z-index: 60;
293
+ display: flex; align-items: center; gap: 10px;
294
+ max-width: min(320px, calc(100vw - 24px));
300
295
  background: var(--card); color: var(--fg);
301
- border: 1px solid var(--border-strong); border-radius: 14px;
296
+ border: 1px solid var(--danger); border-radius: 11px;
302
297
  box-shadow: var(--shadow-lift);
303
- padding: 28px 26px; text-align: center;
304
- }
305
- .lost-card .lost-mark {
306
- width: 52px; height: 52px; border-radius: 50%;
307
- background: var(--danger); color: var(--danger-fg);
308
- font-size: 1.6rem; line-height: 52px; margin: 0 auto 16px;
298
+ padding: 9px 11px;
309
299
  }
310
- .lost-card h2 {
311
- font-family: var(--serif); font-weight: 500; letter-spacing: -0.015em;
312
- font-size: 1.3rem; margin: 0 0 10px;
313
- }
314
- .lost-card p { color: var(--fg-2); font-size: 0.92rem; line-height: 1.55; margin: 0 0 8px; }
315
- .lost-card .lost-sub { color: var(--muted); font-size: 0.82rem; margin: 12px 0 18px; }
316
- .lost-card .lost-retry {
300
+ .lost-note-dot { flex: none; width: 9px; height: 9px; border-radius: 50%; background: var(--danger); }
301
+ .lost-note-body { flex: 1; min-width: 0; line-height: 1.3; }
302
+ .lost-note-title { font-size: 0.82rem; font-weight: 600; }
303
+ .lost-note-sub { font-size: 0.72rem; color: var(--muted); margin-top: 1px; }
304
+ .lost-note .lost-retry {
305
+ flex: none;
317
306
  background: var(--accent); color: var(--accent-fg);
318
- border: none; border-radius: 10px;
319
- padding: 11px 26px; font: inherit; font-size: 0.95rem; font-weight: 600; cursor: pointer;
307
+ border: none; border-radius: 8px;
308
+ padding: 6px 12px; font: inherit; font-size: 0.78rem; font-weight: 600; cursor: pointer;
320
309
  transition: background 150ms var(--ease);
321
310
  }
322
- .lost-card .lost-retry:hover { background: var(--accent-hover); }
323
- .lost-card .lost-retry:disabled { opacity: 0.6; cursor: default; }
324
- .lost-card .lost-retry:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
311
+ .lost-note .lost-retry:hover { background: var(--accent-hover); }
312
+ .lost-note .lost-retry:disabled { opacity: 0.6; cursor: default; }
313
+ .lost-note .lost-retry:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
325
314
 
326
315
  @media (prefers-reduced-motion: reduce) {
327
316
  * { transition: none !important; animation: none !important; }