@khanglvm/relay 0.10.1 → 0.10.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/docs/AGENT.md +38 -10
- package/package.json +1 -1
- package/skills/relay/SKILL.md +22 -1
- package/src/server.js +5 -1
- package/src/ui/blocks.css +4 -0
- package/src/ui/blocks.js +16 -3
package/docs/AGENT.md
CHANGED
|
@@ -11,6 +11,22 @@ leave comments, and select text in markdown blocks to annotate — their comment
|
|
|
11
11
|
come back in `result.annotations` alongside their answers. Treat annotations as
|
|
12
12
|
first-class feedback.
|
|
13
13
|
|
|
14
|
+
**Read ALL four feedback channels, not just `answers`.** A result carries the
|
|
15
|
+
user's input across **four** places, each first-class — never act on `answers`
|
|
16
|
+
alone:
|
|
17
|
+
|
|
18
|
+
- `answers` — the per-question values (what they picked/typed).
|
|
19
|
+
- `notes` — per-question free-text notes (`result.notes[questionId]`): the small
|
|
20
|
+
note box under a question, where users qualify or override a pick. **Single
|
|
21
|
+
(radio) questions show this box by default**, so it is a very common place for
|
|
22
|
+
the real reasoning to land. Easy to miss because empty notes are omitted —
|
|
23
|
+
**always inspect `result.notes`.**
|
|
24
|
+
- `comment` — one board-level free-text note ("Anything else?").
|
|
25
|
+
- `annotations` — element-level inline comments on specific blocks/data points.
|
|
26
|
+
|
|
27
|
+
If any of `notes`, `comment`, or `annotations` is non-empty, it can override or
|
|
28
|
+
contradict an `answers` value — reconcile them before generating output.
|
|
29
|
+
|
|
14
30
|
Everything machine-relevant is on **stdout as JSON**; human-facing logs go to
|
|
15
31
|
stderr. Exit codes: `0` submitted/acknowledged · `2` timeout · `3` cancelled ·
|
|
16
32
|
`4` usage error · `5` not found.
|
|
@@ -137,12 +153,18 @@ boolean/bool/yn→yesno, input→text, longtext→textarea, rating/likert→scal
|
|
|
137
153
|
```
|
|
138
154
|
|
|
139
155
|
Unanswered questions are absent from `answers` and listed in `skipped`.
|
|
140
|
-
|
|
141
|
-
notes
|
|
142
|
-
|
|
143
|
-
`"note":
|
|
144
|
-
|
|
145
|
-
so
|
|
156
|
+
|
|
157
|
+
`notes` (a `{ questionId: "text" }` map) is **always present** in a result —
|
|
158
|
+
`{}` when empty — so you can never miss that the channel exists; iterate it
|
|
159
|
+
even when you only expected `answers`. Questions with `"note": true` show a
|
|
160
|
+
small optional free-text field, and only **non-empty** notes appear as keys.
|
|
161
|
+
**`single` (radio) questions show this note by default** so the user can
|
|
162
|
+
qualify or override their pick — set `"note": false` to hide it. A note like
|
|
163
|
+
`notes.approach = "actually B, not A"` is the user's real intent and outranks
|
|
164
|
+
the `answers.approach` radio value — always check `notes` before acting.
|
|
165
|
+
|
|
166
|
+
On `timeout`/`cancelled`, a `draft` field carries the autosaved partial
|
|
167
|
+
answers, notes, and any annotations written so far.
|
|
146
168
|
|
|
147
169
|
## Blocks
|
|
148
170
|
|
|
@@ -277,7 +299,10 @@ Rules of thumb:
|
|
|
277
299
|
{ "type": "html", "htmlFile": "viz.html", "height": 400 }
|
|
278
300
|
|
|
279
301
|
// Image — local file path (embedded at spec time, works offline), http(s) URL,
|
|
280
|
-
// or data URI. "height"
|
|
302
|
+
// or data URI. "height" sets only the COMPACT inline preview — every image keeps
|
|
303
|
+
// a full-screen + zoom (⌘/Ctrl+wheel or −/+, up to 8× native) + drag-to-pan
|
|
304
|
+
// viewer, so a small height never hides detail. Local images embed up to 8 MB;
|
|
305
|
+
// for a huge / high-resolution image pass an http(s) URL (streamed, no size cap).
|
|
281
306
|
{ "type": "image", "src": "screenshots/variant-a.png", "alt": "Variant A", "height": 220 }
|
|
282
307
|
{ "type": "image", "src": "https://example.com/mock.png" }
|
|
283
308
|
```
|
|
@@ -314,7 +339,7 @@ real path over telling the user to paste it into a terminal.
|
|
|
314
339
|
- `mermaid`: natural flow, max-height 1200 px with internal scroll. Override with `"height"`.
|
|
315
340
|
- `chart`: default 320 px. Override with `"height"`.
|
|
316
341
|
- `html`: default 360 px. Override with `"height"`.
|
|
317
|
-
- `image`: natural size (never upscaled), max-height 1200 px with scroll. `"height"` caps
|
|
342
|
+
- `image`: natural size (never upscaled), max-height 1200 px with scroll. `"height"` caps the **inline preview only** — full-screen + zoom (up to 8× native) always reach full detail. Local images embed up to 8 MB; use an http(s) URL for larger.
|
|
318
343
|
- `table`: natural flow.
|
|
319
344
|
- All heights clamp to 100–2400 px.
|
|
320
345
|
- Inside OPTION cards, always set a compact `"height"` (~140–260) on
|
|
@@ -538,8 +563,11 @@ call rather than calling it repeatedly in a loop.
|
|
|
538
563
|
check whether the user has started answering.
|
|
539
564
|
- In the board `intro`, tell users they can hover chart points / select text to
|
|
540
565
|
leave inline comments — they won't discover it otherwise.
|
|
541
|
-
- Check `result.annotations` before generating your next
|
|
542
|
-
|
|
566
|
+
- Check `result.notes` AND `result.annotations` before generating your next
|
|
567
|
+
output — not just `answers`. A per-question note (e.g. `notes.scope = "docs can
|
|
568
|
+
wait"`) or a comment on a specific data point / quoted sentence often qualifies
|
|
569
|
+
or overrides the checkbox answer. `notes` is always present (`{}` when empty),
|
|
570
|
+
so iterate it every time.
|
|
543
571
|
- Use `rly reopen <id> --replies replies.json` to answer the user's element
|
|
544
572
|
comments and reopen the board as a conversation thread.
|
|
545
573
|
- Use `rly update <id>` to push spec changes to a running board — the page
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@khanglvm/relay",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.2",
|
|
4
4
|
"description": "Browser-based 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, 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: "The tool for collecting user requirements, decisions, and answers (choice, yes-no, text, scale) and for presenting prototypes, plans, structures, code changes, or reports with rich visuals - mermaid/graphviz/plantuml diagrams, charts, tables, code, diffs, video, custom HTML, clickable file-links - plus inline comments on any element. Opens a browser board, waits for Submit, returns JSON answers, comments, and
|
|
3
|
+
description: "The tool for collecting user requirements, decisions, and answers (choice, yes-no, text, scale) and for presenting prototypes, plans, structures, code changes, or reports with rich visuals - mermaid/graphviz/plantuml diagrams, charts, tables, code, diffs, video, custom HTML, clickable file-links - plus inline comments on any element. Opens a browser board, waits for Submit, returns JSON answers, per-question notes, comments, and annotations. Use PROACTIVELY instead of (a) native ask-user tools for 2+ answers or options needing explanation, (b) ASCII trees/tables/diagrams in the terminal or prose for structures/designs/plans, (c) hand-rolled HTML demos. Triggers: collect requirements, ask the user, get decisions/feedback, present a prototype, plan/design review, show me the structure/file tree, architecture or dependency graph, visualize, diagram, chart, table, compare alternatives, survey, edit the diagram, show me the diff / git diff, video walkthrough, open a file. Skip for a single yes/no confirmation."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# relay (`rly`)
|
|
@@ -137,6 +137,10 @@ single/multi question.
|
|
|
137
137
|
{ "type": "html", "htmlFile": "viz.html", "height": 400 }
|
|
138
138
|
{ "type": "image", "src": "screenshot.png" } // local file, URL, or data URI
|
|
139
139
|
{ "type": "image", "src": "https://…/mock.png", "alt": "Mockup B", "height": 220 }
|
|
140
|
+
// ^ `height` only sets the COMPACT inline preview — every image keeps a full-
|
|
141
|
+
// screen + zoom (⌘/Ctrl+wheel or −/+, up to 8× native) + drag-to-pan viewer,
|
|
142
|
+
// so detail is always reachable regardless of height. Local images embed up
|
|
143
|
+
// to 8 MB; for a huge/high-detail image pass an http(s) URL (streamed, no cap).
|
|
140
144
|
```
|
|
141
145
|
|
|
142
146
|
### Visual options — show each choice, don't describe it
|
|
@@ -203,6 +207,23 @@ mention annotation in the board intro.
|
|
|
203
207
|
Read annotations before generating your next output — a comment on a specific
|
|
204
208
|
data point often carries sharper signal than a checkbox answer.
|
|
205
209
|
|
|
210
|
+
## Reading the result — four feedback channels
|
|
211
|
+
|
|
212
|
+
A result is more than `answers`. **Always read all four** — never act on
|
|
213
|
+
`answers` alone; the user's real intent often lives in the others:
|
|
214
|
+
|
|
215
|
+
| field | what it is |
|
|
216
|
+
|---|---|
|
|
217
|
+
| `answers` | per-question values `{ questionId: value }` (skipped ones absent, listed in `skipped`) |
|
|
218
|
+
| `notes` | **per-question free-text notes** `{ questionId: "text" }` — the note box under a question. **Always present (`{}` when empty)**; iterate it every time. `single` (radio) questions show this box by default, so it's a very common place for the user's reasoning — and easy to miss. |
|
|
219
|
+
| `comment` | one board-level free-text note ("Anything else?") |
|
|
220
|
+
| `annotations` | element-level inline comments (array; see below) |
|
|
221
|
+
|
|
222
|
+
A `notes[questionId]`, a `comment`, or an `annotation` can qualify or override
|
|
223
|
+
the matching `answers` value (e.g. `answers.approach = "a"` but
|
|
224
|
+
`notes.approach = "actually B"` → the user means B). Reconcile them before
|
|
225
|
+
generating output. The same channels appear under `draft` on timeout/cancel.
|
|
226
|
+
|
|
206
227
|
### Reply to annotations (agent → user conversation)
|
|
207
228
|
|
|
208
229
|
```sh
|
package/src/server.js
CHANGED
|
@@ -740,7 +740,11 @@ export async function runBoard({ id, port = 0, open = true, timeoutSec = 1800, q
|
|
|
740
740
|
answers: partial.answers ?? null,
|
|
741
741
|
skipped: partial.skipped ?? null,
|
|
742
742
|
comment: partial.comment ?? '',
|
|
743
|
-
notes
|
|
743
|
+
// Per-question notes are first-class feedback. Always emit the key (even
|
|
744
|
+
// empty {}) so a consumer can SEE it exists and never silently misses a
|
|
745
|
+
// note the user left under a question. Falls back to the autosaved draft
|
|
746
|
+
// on timeout/cancel.
|
|
747
|
+
notes: partial.notes ?? (record.draft?.notes || {}),
|
|
744
748
|
annotations: partial.annotations ?? (record.draft?.annotations || []),
|
|
745
749
|
blockEdits,
|
|
746
750
|
createdAt: record.createdAt,
|
package/src/ui/blocks.css
CHANGED
|
@@ -440,6 +440,10 @@
|
|
|
440
440
|
object-fit: contain;
|
|
441
441
|
object-position: left top;
|
|
442
442
|
}
|
|
443
|
+
/* In full-screen a per-block height must never keep the image small — that
|
|
444
|
+
height only shapes the inline preview; the detail view needs every pixel.
|
|
445
|
+
(Belt-and-suspenders alongside the viewer's reapply() on full-screen toggle.) */
|
|
446
|
+
.blk-imagewrap.blk-full .blk-img { max-height: none !important; }
|
|
443
447
|
|
|
444
448
|
/* ---------- viewer controls (full-screen) ---------- */
|
|
445
449
|
/* The full-screen button lives in a reserved strip ABOVE the visual (user
|
package/src/ui/blocks.js
CHANGED
|
@@ -1398,7 +1398,10 @@
|
|
|
1398
1398
|
alt: block.alt || 'image',
|
|
1399
1399
|
loading: 'lazy',
|
|
1400
1400
|
});
|
|
1401
|
-
|
|
1401
|
+
// The per-block height only caps the COMPACT inline preview — the viewer
|
|
1402
|
+
// lifts it on zoom / full-screen so the user can always reach full detail.
|
|
1403
|
+
const fitMaxHeight = block.height ? clampHeight(block.height, 360) : null;
|
|
1404
|
+
if (fitMaxHeight) img.style.maxHeight = fitMaxHeight + 'px';
|
|
1402
1405
|
img.addEventListener('error', () => {
|
|
1403
1406
|
container.replaceChildren(el('div', { class: 'blk-error' }, 'Image failed to load'));
|
|
1404
1407
|
});
|
|
@@ -1409,6 +1412,7 @@
|
|
|
1409
1412
|
natural: () => (img.naturalWidth > 0 ? { w: img.naturalWidth, h: img.naturalHeight } : null),
|
|
1410
1413
|
label: 'image',
|
|
1411
1414
|
comment: wholeBlockComment(ctx, blockId, 'image'),
|
|
1415
|
+
fitMaxHeight,
|
|
1412
1416
|
});
|
|
1413
1417
|
if (img.complete && img.naturalWidth > 0) attachImgViewer();
|
|
1414
1418
|
else img.addEventListener('load', attachImgViewer, { once: true });
|
|
@@ -1450,6 +1454,7 @@
|
|
|
1450
1454
|
fullOpen = null;
|
|
1451
1455
|
window.dispatchEvent(new Event('resize'));
|
|
1452
1456
|
if (c._rlyToolsSync) c._rlyToolsSync(); // re-pin toolbar to its scrolled corner
|
|
1457
|
+
if (c._rlyZoom) c._rlyZoom.reapply(); // restore the compact inline height cap
|
|
1453
1458
|
}
|
|
1454
1459
|
document.addEventListener('keydown', (e) => {
|
|
1455
1460
|
if (e.key === 'Escape' && fullOpen) exitFull();
|
|
@@ -1569,6 +1574,13 @@
|
|
|
1569
1574
|
const target = opts.zoomEl;
|
|
1570
1575
|
const nat = opts.natural();
|
|
1571
1576
|
const z = container._rlyZ;
|
|
1577
|
+
// A per-block height caps only the compact inline preview. The moment the
|
|
1578
|
+
// user zooms in (z !== null) or goes full-screen they want pixel detail, so
|
|
1579
|
+
// lift the cap there; restore it when back to the inline fit view.
|
|
1580
|
+
if (opts.fitMaxHeight != null) {
|
|
1581
|
+
const fit = z === null && !container.classList.contains('blk-full');
|
|
1582
|
+
target.style.maxHeight = fit ? opts.fitMaxHeight + 'px' : 'none';
|
|
1583
|
+
}
|
|
1572
1584
|
if (z === null || !nat || !nat.w) {
|
|
1573
1585
|
target.style.width = '100%';
|
|
1574
1586
|
target.style.maxWidth = nat && nat.w ? Math.ceil(nat.w) + 'px' : '100%';
|
|
@@ -1592,10 +1604,10 @@
|
|
|
1592
1604
|
return shown > 0 ? shown / nat.w : 1;
|
|
1593
1605
|
}
|
|
1594
1606
|
function setZoom(next) {
|
|
1595
|
-
container._rlyZ = next === null ? null : Math.min(
|
|
1607
|
+
container._rlyZ = next === null ? null : Math.min(8, Math.max(0.2, next));
|
|
1596
1608
|
apply();
|
|
1597
1609
|
}
|
|
1598
|
-
container._rlyZoom = { setZoom, currentZ };
|
|
1610
|
+
container._rlyZoom = { setZoom, currentZ, reapply: apply };
|
|
1599
1611
|
|
|
1600
1612
|
const tools = el('div', { class: 'blk-tools' });
|
|
1601
1613
|
|
|
@@ -1652,6 +1664,7 @@
|
|
|
1652
1664
|
fullBtn.textContent = '✕';
|
|
1653
1665
|
window.dispatchEvent(new Event('resize'));
|
|
1654
1666
|
container._rlyToolsSync();
|
|
1667
|
+
if (container._rlyZoom) container._rlyZoom.reapply(); // lift any inline height cap
|
|
1655
1668
|
});
|
|
1656
1669
|
tools.append(fullBtn);
|
|
1657
1670
|
|