@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 +1 -1
- package/src/ui/annotate.js +24 -1
- package/src/ui/app.js +35 -24
- package/src/ui/style.css +22 -33
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@khanglvm/relay",
|
|
3
|
-
"version": "0.12.
|
|
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",
|
package/src/ui/annotate.js
CHANGED
|
@@ -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
|
|
275
|
-
//
|
|
276
|
-
//
|
|
277
|
-
//
|
|
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
|
|
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
|
|
297
|
-
if (
|
|
298
|
-
lostRetryBtn = el('button', { class: 'lost-retry', type: 'button' }, '
|
|
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
|
-
|
|
301
|
-
el('
|
|
302
|
-
|
|
303
|
-
el('
|
|
304
|
-
el('
|
|
305
|
-
|
|
306
|
-
|
|
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
|
|
310
|
+
return lostNote;
|
|
310
311
|
}
|
|
311
312
|
|
|
312
|
-
// Enter the blocked state:
|
|
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
|
-
//
|
|
318
|
-
//
|
|
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(
|
|
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,
|
|
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 (
|
|
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
|
|
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
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
.lost-
|
|
292
|
-
position: fixed;
|
|
293
|
-
display: flex; align-items: center;
|
|
294
|
-
|
|
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(--
|
|
296
|
+
border: 1px solid var(--danger); border-radius: 11px;
|
|
302
297
|
box-shadow: var(--shadow-lift);
|
|
303
|
-
padding:
|
|
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-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
}
|
|
314
|
-
.lost-
|
|
315
|
-
|
|
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:
|
|
319
|
-
padding:
|
|
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-
|
|
323
|
-
.lost-
|
|
324
|
-
.lost-
|
|
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; }
|