@khanglvm/relay 0.10.1 → 0.10.3
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 +46 -10
- package/package.json +1 -1
- package/skills/relay/SKILL.md +29 -1
- package/src/cli.js +6 -2
- 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
|
@@ -5,12 +5,36 @@ rich content blocks (markdown, charts, diagrams, tables, code, custom HTML),
|
|
|
5
5
|
then **wait for them to click Submit** and read the answers as JSON from stdout.
|
|
6
6
|
No "type 'done' in the terminal", no hand-rolled HTML+server.
|
|
7
7
|
|
|
8
|
+
**Use relay even in plan mode** — for any clarifying or decision question (Claude
|
|
9
|
+
Code / Codex), route it through a relay board, **not** the native
|
|
10
|
+
`AskUserQuestion` / ask-user tool: that tool is **not** a substitute, even though
|
|
11
|
+
plan-mode guidance suggests it. Do NOT ask in plain terminal text and do NOT
|
|
12
|
+
reach for the native ask tool for anything beyond one trivial yes/no — open a
|
|
13
|
+
board. Reserve the harness's `ExitPlanMode` strictly for the final plan-approval
|
|
14
|
+
gate (it's a mode transition, not a question).
|
|
15
|
+
|
|
8
16
|
**Tell the user** at the start of your intro text that they can hover chart
|
|
9
17
|
points, diagram nodes, table cells, and any element of a custom-HTML block to
|
|
10
18
|
leave comments, and select text in markdown blocks to annotate — their comments
|
|
11
19
|
come back in `result.annotations` alongside their answers. Treat annotations as
|
|
12
20
|
first-class feedback.
|
|
13
21
|
|
|
22
|
+
**Read ALL four feedback channels, not just `answers`.** A result carries the
|
|
23
|
+
user's input across **four** places, each first-class — never act on `answers`
|
|
24
|
+
alone:
|
|
25
|
+
|
|
26
|
+
- `answers` — the per-question values (what they picked/typed).
|
|
27
|
+
- `notes` — per-question free-text notes (`result.notes[questionId]`): the small
|
|
28
|
+
note box under a question, where users qualify or override a pick. **Single
|
|
29
|
+
(radio) questions show this box by default**, so it is a very common place for
|
|
30
|
+
the real reasoning to land. Easy to miss because empty notes are omitted —
|
|
31
|
+
**always inspect `result.notes`.**
|
|
32
|
+
- `comment` — one board-level free-text note ("Anything else?").
|
|
33
|
+
- `annotations` — element-level inline comments on specific blocks/data points.
|
|
34
|
+
|
|
35
|
+
If any of `notes`, `comment`, or `annotations` is non-empty, it can override or
|
|
36
|
+
contradict an `answers` value — reconcile them before generating output.
|
|
37
|
+
|
|
14
38
|
Everything machine-relevant is on **stdout as JSON**; human-facing logs go to
|
|
15
39
|
stderr. Exit codes: `0` submitted/acknowledged · `2` timeout · `3` cancelled ·
|
|
16
40
|
`4` usage error · `5` not found.
|
|
@@ -137,12 +161,18 @@ boolean/bool/yn→yesno, input→text, longtext→textarea, rating/likert→scal
|
|
|
137
161
|
```
|
|
138
162
|
|
|
139
163
|
Unanswered questions are absent from `answers` and listed in `skipped`.
|
|
140
|
-
|
|
141
|
-
notes
|
|
142
|
-
|
|
143
|
-
`"note":
|
|
144
|
-
|
|
145
|
-
so
|
|
164
|
+
|
|
165
|
+
`notes` (a `{ questionId: "text" }` map) is **always present** in a result —
|
|
166
|
+
`{}` when empty — so you can never miss that the channel exists; iterate it
|
|
167
|
+
even when you only expected `answers`. Questions with `"note": true` show a
|
|
168
|
+
small optional free-text field, and only **non-empty** notes appear as keys.
|
|
169
|
+
**`single` (radio) questions show this note by default** so the user can
|
|
170
|
+
qualify or override their pick — set `"note": false` to hide it. A note like
|
|
171
|
+
`notes.approach = "actually B, not A"` is the user's real intent and outranks
|
|
172
|
+
the `answers.approach` radio value — always check `notes` before acting.
|
|
173
|
+
|
|
174
|
+
On `timeout`/`cancelled`, a `draft` field carries the autosaved partial
|
|
175
|
+
answers, notes, and any annotations written so far.
|
|
146
176
|
|
|
147
177
|
## Blocks
|
|
148
178
|
|
|
@@ -277,7 +307,10 @@ Rules of thumb:
|
|
|
277
307
|
{ "type": "html", "htmlFile": "viz.html", "height": 400 }
|
|
278
308
|
|
|
279
309
|
// Image — local file path (embedded at spec time, works offline), http(s) URL,
|
|
280
|
-
// or data URI. "height"
|
|
310
|
+
// or data URI. "height" sets only the COMPACT inline preview — every image keeps
|
|
311
|
+
// a full-screen + zoom (⌘/Ctrl+wheel or −/+, up to 8× native) + drag-to-pan
|
|
312
|
+
// viewer, so a small height never hides detail. Local images embed up to 8 MB;
|
|
313
|
+
// for a huge / high-resolution image pass an http(s) URL (streamed, no size cap).
|
|
281
314
|
{ "type": "image", "src": "screenshots/variant-a.png", "alt": "Variant A", "height": 220 }
|
|
282
315
|
{ "type": "image", "src": "https://example.com/mock.png" }
|
|
283
316
|
```
|
|
@@ -314,7 +347,7 @@ real path over telling the user to paste it into a terminal.
|
|
|
314
347
|
- `mermaid`: natural flow, max-height 1200 px with internal scroll. Override with `"height"`.
|
|
315
348
|
- `chart`: default 320 px. Override with `"height"`.
|
|
316
349
|
- `html`: default 360 px. Override with `"height"`.
|
|
317
|
-
- `image`: natural size (never upscaled), max-height 1200 px with scroll. `"height"` caps
|
|
350
|
+
- `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
351
|
- `table`: natural flow.
|
|
319
352
|
- All heights clamp to 100–2400 px.
|
|
320
353
|
- Inside OPTION cards, always set a compact `"height"` (~140–260) on
|
|
@@ -538,8 +571,11 @@ call rather than calling it repeatedly in a loop.
|
|
|
538
571
|
check whether the user has started answering.
|
|
539
572
|
- In the board `intro`, tell users they can hover chart points / select text to
|
|
540
573
|
leave inline comments — they won't discover it otherwise.
|
|
541
|
-
- Check `result.annotations` before generating your next
|
|
542
|
-
|
|
574
|
+
- Check `result.notes` AND `result.annotations` before generating your next
|
|
575
|
+
output — not just `answers`. A per-question note (e.g. `notes.scope = "docs can
|
|
576
|
+
wait"`) or a comment on a specific data point / quoted sentence often qualifies
|
|
577
|
+
or overrides the checkbox answer. `notes` is always present (`{}` when empty),
|
|
578
|
+
so iterate it every time.
|
|
543
579
|
- Use `rly reopen <id> --replies replies.json` to answer the user's element
|
|
544
580
|
comments and reopen the board as a conversation thread.
|
|
545
581
|
- 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.3",
|
|
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`)
|
|
@@ -31,8 +31,15 @@ Schema).** The essentials are below.
|
|
|
31
31
|
| "Show me the diff" / git diff / code changes / before-after | **rly** (`diff` block — run `git diff`, render it; never dump it in the terminal) |
|
|
32
32
|
| A demo, screen recording or walkthrough | **rly** (`video` block) |
|
|
33
33
|
| Point the user at a file to open (log, capture, report) | **rly** (a clickable local file-link in markdown) |
|
|
34
|
+
| Plan-mode clarifying question (Claude Code / Codex) | **rly** (not AskUserQuestion / the native ask tool) |
|
|
34
35
|
| Something you can decide yourself from context | neither — just decide |
|
|
35
36
|
|
|
37
|
+
This holds **in plan mode** too: route every clarifying or decision question
|
|
38
|
+
through relay — the native `AskUserQuestion` / ask-user tool is **not** a
|
|
39
|
+
substitute, even though plan-mode guidance suggests it. Reserve the harness's
|
|
40
|
+
`ExitPlanMode` strictly for the final plan-approval gate (it's a mode
|
|
41
|
+
transition, not a question).
|
|
42
|
+
|
|
36
43
|
Once the user has answered one board in a session, prefer boards for later
|
|
37
44
|
question rounds too — they've shown they engage with them. Batch related
|
|
38
45
|
questions into ONE board rather than opening several in a row.
|
|
@@ -137,6 +144,10 @@ single/multi question.
|
|
|
137
144
|
{ "type": "html", "htmlFile": "viz.html", "height": 400 }
|
|
138
145
|
{ "type": "image", "src": "screenshot.png" } // local file, URL, or data URI
|
|
139
146
|
{ "type": "image", "src": "https://…/mock.png", "alt": "Mockup B", "height": 220 }
|
|
147
|
+
// ^ `height` only sets the COMPACT inline preview — every image keeps a full-
|
|
148
|
+
// screen + zoom (⌘/Ctrl+wheel or −/+, up to 8× native) + drag-to-pan viewer,
|
|
149
|
+
// so detail is always reachable regardless of height. Local images embed up
|
|
150
|
+
// to 8 MB; for a huge/high-detail image pass an http(s) URL (streamed, no cap).
|
|
140
151
|
```
|
|
141
152
|
|
|
142
153
|
### Visual options — show each choice, don't describe it
|
|
@@ -203,6 +214,23 @@ mention annotation in the board intro.
|
|
|
203
214
|
Read annotations before generating your next output — a comment on a specific
|
|
204
215
|
data point often carries sharper signal than a checkbox answer.
|
|
205
216
|
|
|
217
|
+
## Reading the result — four feedback channels
|
|
218
|
+
|
|
219
|
+
A result is more than `answers`. **Always read all four** — never act on
|
|
220
|
+
`answers` alone; the user's real intent often lives in the others:
|
|
221
|
+
|
|
222
|
+
| field | what it is |
|
|
223
|
+
|---|---|
|
|
224
|
+
| `answers` | per-question values `{ questionId: value }` (skipped ones absent, listed in `skipped`) |
|
|
225
|
+
| `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. |
|
|
226
|
+
| `comment` | one board-level free-text note ("Anything else?") |
|
|
227
|
+
| `annotations` | element-level inline comments (array; see below) |
|
|
228
|
+
|
|
229
|
+
A `notes[questionId]`, a `comment`, or an `annotation` can qualify or override
|
|
230
|
+
the matching `answers` value (e.g. `answers.approach = "a"` but
|
|
231
|
+
`notes.approach = "actually B"` → the user means B). Reconcile them before
|
|
232
|
+
generating output. The same channels appear under `draft` on timeout/cancel.
|
|
233
|
+
|
|
206
234
|
### Reply to annotations (agent → user conversation)
|
|
207
235
|
|
|
208
236
|
```sh
|
package/src/cli.js
CHANGED
|
@@ -799,8 +799,12 @@ function stampSkillVersion() {
|
|
|
799
799
|
const SKILL_RULES = `## relay — ask & show in the browser, not the terminal
|
|
800
800
|
|
|
801
801
|
- Need the user's decisions, feedback, requirements, or approval (anything
|
|
802
|
-
beyond one trivial yes/no)? Do NOT ask in plain terminal text
|
|
803
|
-
|
|
802
|
+
beyond one trivial yes/no)? Do NOT ask in plain terminal text **and do NOT use
|
|
803
|
+
the native \`AskUserQuestion\` / ask-user tool** — for anything beyond one
|
|
804
|
+
trivial yes/no, open a relay board. Build a board spec and run:
|
|
805
|
+
\`rly ask --file spec.json --detach\`, then \`rly wait <boardId>\`. **This holds
|
|
806
|
+
in plan mode too** — route clarifying/decision questions through relay there as
|
|
807
|
+
well; reserve \`ExitPlanMode\` for the final plan-approval gate only.
|
|
804
808
|
- Presenting a plan, structure, architecture, data, or a prototype? Show a
|
|
805
809
|
relay board with mermaid/graphviz/chart/table/code/diff/video/image/html
|
|
806
810
|
blocks — never ASCII diagrams or walls of prose.
|
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
|
|