@khanglvm/relay 0.4.0 → 0.4.2
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/skills/relay/SKILL.md +1 -1
- package/src/cli.js +6 -7
- package/src/ui/annotate.js +7 -0
- package/src/ui/blocks.css +34 -0
- package/src/ui/blocks.js +120 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@khanglvm/relay",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "Browser-based question boards with rich blocks (markdown, charts, mermaid, tables, code, sandboxed HTML) and element-level annotations for AI coding agents (Claude Code, Codex, …): ask users structured questions, present interactive visuals, collect inline comments, wait for submit, read answers as JSON.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-agents",
|
package/skills/relay/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: relay
|
|
3
|
-
description: Show the user anything visual in an interactive browser board - repo/file structures, architecture diagrams (mermaid, graphviz, plantuml, uml), charts, sortable
|
|
3
|
+
description: "Show the user anything visual in an interactive browser board - repo/file structures, architecture diagrams (mermaid, graphviz, plantuml, uml), charts, sortable tables, code, prototypes - and/or ask structured questions (choice, yes-no, text, scale), then wait for Submit and read JSON answers plus element comments and user-edited diagrams. PROACTIVELY use instead of (a) drawing an ASCII tree/table/diagram in the terminal or describing a structure/design/plan in prose, (b) a native ask-user tool for 2+ questions or options needing explanation, (c) hand-rolling an HTML demo + server. Triggers: show me the structure, repo/folder structure, file tree, directory layout, codebase map, architecture overview, dependency graph, sequence/class diagram, uml, visualize, diagram, chart, data table, metrics review, prototype review, plan approval, design feedback, compare alternatives, edit the diagram, clarify requirements, survey, ask the user, show the user, get feedback. Skip for a single trivial yes/no confirmation."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# relay (`rly`)
|
package/src/cli.js
CHANGED
|
@@ -652,6 +652,9 @@ const SKILL_SRC = path.join(PKG_ROOT, 'skills', 'relay');
|
|
|
652
652
|
const KNOWN_SKILL_DIRS = () => ({
|
|
653
653
|
claude: path.join(os.homedir(), '.claude', 'skills', 'relay'),
|
|
654
654
|
codex: path.join(os.homedir(), '.codex', 'skills', 'relay'),
|
|
655
|
+
// The cross-agent skills dir (npx-skills ecosystem). Some Codex/agent
|
|
656
|
+
// setups load skills from here INSTEAD of ~/.codex/skills.
|
|
657
|
+
agents: path.join(os.homedir(), '.agents', 'skills', 'relay'),
|
|
655
658
|
});
|
|
656
659
|
|
|
657
660
|
// Pre-rename skill dirs (quest-board). `skill install` removes these so a
|
|
@@ -702,19 +705,15 @@ function skillFreshnessWarning() {
|
|
|
702
705
|
}
|
|
703
706
|
|
|
704
707
|
function skillTargets(target) {
|
|
705
|
-
const
|
|
706
|
-
const known = {
|
|
707
|
-
claude: path.join(home, '.claude', 'skills', 'relay'),
|
|
708
|
-
codex: path.join(home, '.codex', 'skills', 'relay'),
|
|
709
|
-
};
|
|
708
|
+
const known = KNOWN_SKILL_DIRS();
|
|
710
709
|
if (!target || target === true || target === 'auto') {
|
|
711
710
|
const found = Object.values(known).filter((p) => fs.existsSync(path.dirname(path.dirname(p))));
|
|
712
711
|
if (!found.length) {
|
|
713
|
-
throw new CliError('no agent dirs found (~/.claude or ~/.
|
|
712
|
+
throw new CliError('no agent dirs found (~/.claude, ~/.codex, or ~/.agents). Use --target claude|codex|agents|all|<dir>.');
|
|
714
713
|
}
|
|
715
714
|
return found;
|
|
716
715
|
}
|
|
717
|
-
if (target === 'both') return Object.values(known);
|
|
716
|
+
if (target === 'both' || target === 'all') return Object.values(known);
|
|
718
717
|
if (known[target]) return [known[target]];
|
|
719
718
|
return [path.join(path.resolve(target), 'relay')];
|
|
720
719
|
}
|
package/src/ui/annotate.js
CHANGED
|
@@ -202,7 +202,14 @@
|
|
|
202
202
|
badges = [];
|
|
203
203
|
if (torndown) return;
|
|
204
204
|
registered = registered.filter((r) => r.el.isConnected);
|
|
205
|
+
// While a block is expanded full-screen, body-level overlay badges for
|
|
206
|
+
// OTHER blocks would float above the overlay at stale positions — only
|
|
207
|
+
// badge elements inside the expanded block.
|
|
208
|
+
const fullEl = document.body.classList.contains('blk-full-open')
|
|
209
|
+
? document.querySelector('.blk-full')
|
|
210
|
+
: null;
|
|
205
211
|
for (const entry of registered) {
|
|
212
|
+
if (fullEl && !fullEl.contains(entry.el)) continue;
|
|
206
213
|
const count = matching(entry.info).length;
|
|
207
214
|
if (!count) continue;
|
|
208
215
|
const badge = el('span', { class: 'ann-badge', title: count + (count === 1 ? ' comment' : ' comments') }, String(count));
|
package/src/ui/blocks.css
CHANGED
|
@@ -265,3 +265,37 @@
|
|
|
265
265
|
max-width: 100%;
|
|
266
266
|
height: auto;
|
|
267
267
|
}
|
|
268
|
+
|
|
269
|
+
/* ---------- viewer controls (zoom / fit / full-screen) ---------- */
|
|
270
|
+
.blk-viewer { position: relative; }
|
|
271
|
+
.blk-tools {
|
|
272
|
+
position: absolute; top: 6px; right: 6px;
|
|
273
|
+
display: flex; gap: 2px; align-items: center;
|
|
274
|
+
background: var(--card); border: 1px solid var(--border);
|
|
275
|
+
border-radius: 8px; padding: 2px 4px;
|
|
276
|
+
opacity: 0.35; transition: opacity 150ms var(--ease);
|
|
277
|
+
z-index: 5;
|
|
278
|
+
}
|
|
279
|
+
.blk-viewer:hover .blk-tools, .blk-full .blk-tools { opacity: 1; }
|
|
280
|
+
.blk-tools button {
|
|
281
|
+
background: none; border: none; color: var(--fg-2);
|
|
282
|
+
font: inherit; font-size: 0.8rem; line-height: 1;
|
|
283
|
+
padding: 4px 6px; border-radius: 6px; cursor: pointer;
|
|
284
|
+
}
|
|
285
|
+
.blk-tools button:hover { background: var(--bg-sunken); color: var(--accent); }
|
|
286
|
+
.blk-tools .tool-pct { color: var(--muted); font-size: 0.72rem; min-width: 30px; text-align: center; }
|
|
287
|
+
/* charts keep their annotation-count badge top-right; tools go top-left there */
|
|
288
|
+
.blk-chart .blk-tools { right: auto; left: 6px; }
|
|
289
|
+
|
|
290
|
+
/* full-screen = fixed overlay (NOT native fullscreen: annotation pins/badges/
|
|
291
|
+
popover live on <body> and must stay visible above the expanded block) */
|
|
292
|
+
.blk-full {
|
|
293
|
+
position: fixed !important; inset: 12px; z-index: 49;
|
|
294
|
+
background: var(--card); border: 1px solid var(--border);
|
|
295
|
+
border-radius: 12px; box-shadow: var(--shadow-lift);
|
|
296
|
+
overflow: auto; max-height: none !important; margin: 0 !important;
|
|
297
|
+
padding: 48px 16px 16px;
|
|
298
|
+
}
|
|
299
|
+
body.blk-full-open { overflow: hidden; }
|
|
300
|
+
.blk-chart.blk-full { height: calc(100vh - 24px) !important; width: auto !important; }
|
|
301
|
+
.blk-htmlwrap.blk-full iframe.viz { height: calc(100vh - 90px) !important; width: 100%; }
|
package/src/ui/blocks.js
CHANGED
|
@@ -502,6 +502,7 @@
|
|
|
502
502
|
}
|
|
503
503
|
syncBadge();
|
|
504
504
|
wrap.append(badge);
|
|
505
|
+
attachViewer(wrap, { zoomEl: null }); // full-screen only; charts redraw responsively
|
|
505
506
|
|
|
506
507
|
loadChart().then((Chart) => {
|
|
507
508
|
let config = block.config
|
|
@@ -724,6 +725,8 @@
|
|
|
724
725
|
});
|
|
725
726
|
});
|
|
726
727
|
}
|
|
728
|
+
// re-attach per render: innerHTML replacement above wiped the old bar
|
|
729
|
+
if (svgEl) attachViewer(container, { zoomEl: svgEl, natural: svgNatural(svgEl) });
|
|
727
730
|
if (onDone) onDone();
|
|
728
731
|
};
|
|
729
732
|
try {
|
|
@@ -809,6 +812,7 @@
|
|
|
809
812
|
const svgEl = viz.renderSVGElement(block.dot || '');
|
|
810
813
|
sizeDiagramSvg(svgEl);
|
|
811
814
|
container.replaceChildren(svgEl);
|
|
815
|
+
attachViewer(container, { zoomEl: svgEl, natural: svgNatural(svgEl) });
|
|
812
816
|
if (!ctx.annotate) return;
|
|
813
817
|
const parts = svgEl.querySelectorAll('g.node, g.edge');
|
|
814
818
|
parts.forEach((g) => {
|
|
@@ -885,6 +889,13 @@
|
|
|
885
889
|
if (block.height) img.style.height = clampHeight(block.height, 360) + 'px';
|
|
886
890
|
img.addEventListener('error', fail);
|
|
887
891
|
container.replaceChildren(img);
|
|
892
|
+
const attachImgViewer = () =>
|
|
893
|
+
attachViewer(container, {
|
|
894
|
+
zoomEl: img,
|
|
895
|
+
natural: () => (img.naturalWidth > 0 ? { w: img.naturalWidth, h: img.naturalHeight } : null),
|
|
896
|
+
});
|
|
897
|
+
if (img.complete && img.naturalWidth > 0) attachImgViewer();
|
|
898
|
+
else img.addEventListener('load', attachImgViewer, { once: true });
|
|
888
899
|
if (ctx.annotate) {
|
|
889
900
|
ctx.annotate.register(img, {
|
|
890
901
|
blockId,
|
|
@@ -897,10 +908,115 @@
|
|
|
897
908
|
return container;
|
|
898
909
|
}
|
|
899
910
|
|
|
911
|
+
// ---------- viewer controls (zoom / fit / full-screen) ----------
|
|
912
|
+
// Large diagrams get squeezed to the column width; these controls let the
|
|
913
|
+
// user zoom (buttons or cmd/ctrl+wheel) and expand any visual block into a
|
|
914
|
+
// full-screen overlay. An overlay — NOT the native Fullscreen API — keeps
|
|
915
|
+
// the annotation pins/badges/popover usable while expanded (they live on
|
|
916
|
+
// <body>, which native fullscreen would hide).
|
|
917
|
+
let fullOpen = null; // container currently expanded
|
|
918
|
+
|
|
919
|
+
function exitFull() {
|
|
920
|
+
if (!fullOpen) return;
|
|
921
|
+
fullOpen.classList.remove('blk-full');
|
|
922
|
+
document.body.classList.remove('blk-full-open');
|
|
923
|
+
const btn = fullOpen.querySelector('.blk-tools .tool-full');
|
|
924
|
+
if (btn) btn.textContent = '⛶';
|
|
925
|
+
fullOpen = null;
|
|
926
|
+
window.dispatchEvent(new Event('resize'));
|
|
927
|
+
}
|
|
928
|
+
document.addEventListener('keydown', (e) => {
|
|
929
|
+
if (e.key === 'Escape' && fullOpen) exitFull();
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
// opts: { zoomEl: svg|img|null, natural: () => ({w,h}|null) } — zoomEl null
|
|
933
|
+
// means full-screen only (charts, html iframes).
|
|
934
|
+
function attachViewer(container, opts) {
|
|
935
|
+
if (container._rlyTools) container._rlyTools.remove();
|
|
936
|
+
container.classList.add('blk-viewer');
|
|
937
|
+
const zoomable = Boolean(opts && opts.zoomEl);
|
|
938
|
+
let z = null; // null = fit-to-width
|
|
939
|
+
|
|
940
|
+
const pct = el('span', { class: 'tool-pct' }, 'fit');
|
|
941
|
+
function apply() {
|
|
942
|
+
if (!zoomable) return;
|
|
943
|
+
const target = opts.zoomEl;
|
|
944
|
+
const nat = opts.natural();
|
|
945
|
+
if (z === null || !nat || !nat.w) {
|
|
946
|
+
target.style.width = '100%';
|
|
947
|
+
target.style.maxWidth = nat && nat.w ? Math.ceil(nat.w) + 'px' : '100%';
|
|
948
|
+
target.style.height = 'auto';
|
|
949
|
+
pct.textContent = 'fit';
|
|
950
|
+
} else {
|
|
951
|
+
target.style.maxWidth = 'none';
|
|
952
|
+
target.style.width = Math.round(nat.w * z) + 'px';
|
|
953
|
+
target.style.height = 'auto';
|
|
954
|
+
pct.textContent = Math.round(z * 100) + '%';
|
|
955
|
+
}
|
|
956
|
+
// annotation overlay badges reposition on resize
|
|
957
|
+
window.dispatchEvent(new Event('resize'));
|
|
958
|
+
}
|
|
959
|
+
function currentZ() {
|
|
960
|
+
if (z !== null) return z;
|
|
961
|
+
const nat = opts.natural();
|
|
962
|
+
if (!nat || !nat.w) return 1;
|
|
963
|
+
const shown = opts.zoomEl.getBoundingClientRect().width;
|
|
964
|
+
return shown > 0 ? shown / nat.w : 1;
|
|
965
|
+
}
|
|
966
|
+
function setZoom(next) {
|
|
967
|
+
z = next === null ? null : Math.min(5, Math.max(0.2, next));
|
|
968
|
+
apply();
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
const tools = el('div', { class: 'blk-tools' });
|
|
972
|
+
if (zoomable) {
|
|
973
|
+
tools.append(
|
|
974
|
+
el('button', { type: 'button', title: 'Zoom out', onclick: () => setZoom(currentZ() / 1.25) }, '−'),
|
|
975
|
+
pct,
|
|
976
|
+
el('button', { type: 'button', title: 'Zoom in', onclick: () => setZoom(currentZ() * 1.25) }, '+'),
|
|
977
|
+
el('button', { type: 'button', title: 'Actual size', onclick: () => setZoom(1) }, '1:1'),
|
|
978
|
+
el('button', { type: 'button', title: 'Fit to width', onclick: () => setZoom(null) }, 'fit')
|
|
979
|
+
);
|
|
980
|
+
container.addEventListener(
|
|
981
|
+
'wheel',
|
|
982
|
+
(e) => {
|
|
983
|
+
if (!(e.ctrlKey || e.metaKey)) return;
|
|
984
|
+
e.preventDefault();
|
|
985
|
+
setZoom(currentZ() * (e.deltaY < 0 ? 1.15 : 1 / 1.15));
|
|
986
|
+
},
|
|
987
|
+
{ passive: false }
|
|
988
|
+
);
|
|
989
|
+
}
|
|
990
|
+
const fullBtn = el('button', { class: 'tool-full', type: 'button', title: 'Full screen (Esc closes)' }, '⛶');
|
|
991
|
+
fullBtn.addEventListener('click', () => {
|
|
992
|
+
if (fullOpen === container) {
|
|
993
|
+
exitFull();
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
exitFull();
|
|
997
|
+
container.classList.add('blk-full');
|
|
998
|
+
document.body.classList.add('blk-full-open');
|
|
999
|
+
fullOpen = container;
|
|
1000
|
+
fullBtn.textContent = '✕';
|
|
1001
|
+
window.dispatchEvent(new Event('resize'));
|
|
1002
|
+
});
|
|
1003
|
+
tools.append(fullBtn);
|
|
1004
|
+
container.append(tools);
|
|
1005
|
+
container._rlyTools = tools;
|
|
1006
|
+
if (zoomable) apply();
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
function svgNatural(svgEl) {
|
|
1010
|
+
return () => {
|
|
1011
|
+
const vb = svgEl.viewBox && svgEl.viewBox.baseVal;
|
|
1012
|
+
return vb && vb.width > 0 ? { w: vb.width, h: vb.height } : null;
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
|
|
900
1016
|
// ---------- html (sandboxed iframe) ----------
|
|
901
1017
|
function renderHtml(block, ctx, blockId) {
|
|
902
1018
|
const height = clampHeight(block.height, 360);
|
|
903
|
-
|
|
1019
|
+
const iframe = el('iframe', {
|
|
904
1020
|
class: 'viz',
|
|
905
1021
|
'data-block-id': blockId,
|
|
906
1022
|
'data-question-id': ctx.questionId || '',
|
|
@@ -909,6 +1025,9 @@
|
|
|
909
1025
|
sandbox: 'allow-scripts allow-forms allow-popups allow-modals',
|
|
910
1026
|
loading: 'lazy',
|
|
911
1027
|
});
|
|
1028
|
+
const wrap = el('div', { class: 'blk-htmlwrap' }, iframe);
|
|
1029
|
+
attachViewer(wrap, { zoomEl: null });
|
|
1030
|
+
return wrap;
|
|
912
1031
|
}
|
|
913
1032
|
|
|
914
1033
|
// ---------- dispatch ----------
|