@khanglvm/relay 0.10.0 → 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/README.md +2 -1
- package/docs/AGENT.md +45 -10
- package/package.json +1 -1
- package/skills/relay/SKILL.md +53 -2
- package/src/cli.js +48 -11
- package/src/server.js +5 -1
- package/src/ui/blocks.css +4 -0
- package/src/ui/blocks.js +16 -3
package/README.md
CHANGED
|
@@ -35,7 +35,8 @@ That's it. Next time your agent needs a decision or wants to show you a plan,
|
|
|
35
35
|
it opens a board like the ones above and waits for your Submit.
|
|
36
36
|
|
|
37
37
|
Keep relay current with **`rly upgrade`** — it installs the latest CLI and
|
|
38
|
-
refreshes the skill
|
|
38
|
+
refreshes the skill (via `npx skills`, falling back to the bundled copy) in one
|
|
39
|
+
step, leaving any boards you have open untouched.
|
|
39
40
|
|
|
40
41
|
## What it improves
|
|
41
42
|
|
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,13 +563,23 @@ 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
|
|
546
574
|
reloads and answers survive via draft autosave. Batch updates; do not spam.
|
|
575
|
+
- When the user asks to *see* code changes — "show me the diff", "show me git
|
|
576
|
+
diff", "review these changes" — capture `git diff` (or `git show <sha>`) and
|
|
577
|
+
render it in a `diff` block instead of printing it to the terminal; for a
|
|
578
|
+
brand-new file use a `code` block. Point them at a file to inspect with a
|
|
579
|
+
clickable local path in markdown.
|
|
547
580
|
- For sensitive PlantUML diagrams, set `"server": "https://your-server"` to avoid
|
|
548
581
|
sending source to the public plantuml.com server.
|
|
549
582
|
- Bundled universal skill (Claude Code, Codex, any SKILL.md-aware agent):
|
|
550
583
|
`rly skill install` — or `npx skills add khanglvm/relay --skill relay --all`.
|
|
584
|
+
`rly upgrade` refreshes the CLI **and** the skill (via npx skills, falling back
|
|
585
|
+
to the bundled copy) in one step.
|
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
|
|
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`)
|
|
@@ -28,6 +28,9 @@ Schema).** The essentials are below.
|
|
|
28
28
|
| Present a prototype / demo an idea | **rly show** — never hand-roll an HTML file + server |
|
|
29
29
|
| Gather requirements / plan approval / feedback round | **rly** |
|
|
30
30
|
| Architecture or flow that benefits from a diagram | **rly** (mermaid block) |
|
|
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
|
+
| A demo, screen recording or walkthrough | **rly** (`video` block) |
|
|
33
|
+
| Point the user at a file to open (log, capture, report) | **rly** (a clickable local file-link in markdown) |
|
|
31
34
|
| Something you can decide yourself from context | neither — just decide |
|
|
32
35
|
|
|
33
36
|
Once the user has answered one board in a session, prefer boards for later
|
|
@@ -119,11 +122,25 @@ single/multi question.
|
|
|
119
122
|
{ "type": "table", "columns": ["A","B"], "rows": [["x","y"]], "sortable": true }
|
|
120
123
|
// ^ use a `table` block for tabular data — sortable + per-cell comments.
|
|
121
124
|
// (markdown blocks render GFM pipe tables too, but those are display-only.)
|
|
122
|
-
{ "type": "code", "lang": "js", "code": "const x = 1;" }
|
|
125
|
+
{ "type": "code", "lang": "js", "code": "const x = 1;", "filename": "demo.js" }
|
|
126
|
+
{ "type": "code", "codeFile": "src/server.js" } // load text from a local file
|
|
127
|
+
{ "type": "diff", "lang": "js", "filename": "src/auth.js", "view": "split",
|
|
128
|
+
"diff": "@@ -1,3 +1,3 @@\n ctx\n-old line\n+new line\n ctx" }
|
|
129
|
+
// ^ a unified / `git diff` text rendered as a colored, line-numbered comparison
|
|
130
|
+
// (no git needed — just paste the diff). "view":"split" = side-by-side; the
|
|
131
|
+
// viewer also has a live Unified⇄Split toggle. "diffFile" loads it from a file.
|
|
132
|
+
{ "type": "video", "src": "https://youtu.be/dQw4w9WgXcQ", "title": "Demo walkthrough" }
|
|
133
|
+
{ "type": "video", "src": "recordings/demo.mp4", "title": "Local capture", "height": 360 }
|
|
134
|
+
// ^ YouTube/Vimeo URL embeds a player; an http(s) media URL or a local video
|
|
135
|
+
// file (mp4/webm/ogv/mov/mkv/m4v) plays inline (local files stream, not embedded).
|
|
123
136
|
{ "type": "html", "html": "<p>hi</p>", "height": 360 }
|
|
124
137
|
{ "type": "html", "htmlFile": "viz.html", "height": 400 }
|
|
125
138
|
{ "type": "image", "src": "screenshot.png" } // local file, URL, or data URI
|
|
126
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).
|
|
127
144
|
```
|
|
128
145
|
|
|
129
146
|
### Visual options — show each choice, don't describe it
|
|
@@ -154,6 +171,15 @@ stays dependency-free. PlantUML uses the public plantuml.com server by default;
|
|
|
154
171
|
pass `"server"` for a self-hosted instance. Legacy `"html"` / `"htmlFile"` /
|
|
155
172
|
`"htmlHeight"` on root or questions are still accepted and normalised automatically.
|
|
156
173
|
|
|
174
|
+
### Local file links — clickable, open in the default app
|
|
175
|
+
|
|
176
|
+
Write a local path in any markdown (the `intro` or a `markdown` block) — `~/clip.mp4`,
|
|
177
|
+
`./src/app.ts`, `/abs/report.pdf`, a `file://` URL, or a backtick-wrapped path — and it
|
|
178
|
+
renders as a click-to-open link that opens the file in the user's OS default app
|
|
179
|
+
(editor, video player, viewer …); `[label](~/path)` works too. Only paths you actually
|
|
180
|
+
wrote on the board can be opened (same-origin + allowlist guarded). Surface a real
|
|
181
|
+
clickable path instead of telling the user to paste it into a terminal.
|
|
182
|
+
|
|
157
183
|
## Annotations
|
|
158
184
|
|
|
159
185
|
Users can hover chart points, diagram nodes (mermaid + graphviz), table cells,
|
|
@@ -181,6 +207,23 @@ mention annotation in the board intro.
|
|
|
181
207
|
Read annotations before generating your next output — a comment on a specific
|
|
182
208
|
data point often carries sharper signal than a checkbox answer.
|
|
183
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
|
+
|
|
184
227
|
### Reply to annotations (agent → user conversation)
|
|
185
228
|
|
|
186
229
|
```sh
|
|
@@ -210,6 +253,14 @@ urgency, `textarea` for constraints.
|
|
|
210
253
|
`html`/`image` block rendering that variant (see Visual options above); `scale`
|
|
211
254
|
for confidence; `textarea` for what's missing from both.
|
|
212
255
|
|
|
256
|
+
**Show a git diff / code changes** — when the user says "show me the diff" /
|
|
257
|
+
"show me git diff" / "review these changes": capture `git diff` (or `git diff
|
|
258
|
+
<ref>`, `git show <sha>`) and present it in a `diff` block — set `"view":
|
|
259
|
+
"split"` for side-by-side — instead of dumping it in the terminal. Pair it with
|
|
260
|
+
a `yesno` "Apply these changes?" and a `textarea` for feedback; users can select
|
|
261
|
+
diff text to comment on a specific line. For a brand-new file prefer a `code` block; for a
|
|
262
|
+
recorded walkthrough of the change add a `video` block.
|
|
263
|
+
|
|
213
264
|
**Metrics review** — board-level `chart` block (bar or line) showing the key
|
|
214
265
|
numbers, followed by a `table` block for the raw data; at least one question
|
|
215
266
|
asking what to act on. In the intro, tell the user they can click chart points
|
package/src/cli.js
CHANGED
|
@@ -773,6 +773,25 @@ function removeLegacySkills() {
|
|
|
773
773
|
return removed;
|
|
774
774
|
}
|
|
775
775
|
|
|
776
|
+
// Stamps the current CLI version into every installed skill dir's .rly-version.
|
|
777
|
+
// `rly skill install` writes this itself, but an `npx skills add` refresh does
|
|
778
|
+
// not — so after any refresh we re-stamp the dirs that now have a SKILL.md,
|
|
779
|
+
// keeping skillFreshnessWarning() from nagging about a just-refreshed skill.
|
|
780
|
+
function stampSkillVersion() {
|
|
781
|
+
const stamped = [];
|
|
782
|
+
for (const dir of Object.values(KNOWN_SKILL_DIRS())) {
|
|
783
|
+
try {
|
|
784
|
+
if (fs.existsSync(path.join(dir, 'SKILL.md'))) {
|
|
785
|
+
fs.writeFileSync(path.join(dir, '.rly-version'), VERSION);
|
|
786
|
+
stamped.push(dir);
|
|
787
|
+
}
|
|
788
|
+
} catch {
|
|
789
|
+
// best effort — a marker we couldn't write just re-triggers the nudge
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
return stamped;
|
|
793
|
+
}
|
|
794
|
+
|
|
776
795
|
// Short instruction block for an agent's main instruction file (CLAUDE.md,
|
|
777
796
|
// AGENTS.md, …). A skill is only a tier-1 hint that agents can ignore; these
|
|
778
797
|
// rules, appended to the instructions the agent always reads, enforce usage.
|
|
@@ -783,8 +802,12 @@ const SKILL_RULES = `## relay — ask & show in the browser, not the terminal
|
|
|
783
802
|
beyond one trivial yes/no)? Do NOT ask in plain terminal text. Build a board
|
|
784
803
|
spec and run: \`rly ask --file spec.json --detach\`, then \`rly wait <boardId>\`.
|
|
785
804
|
- Presenting a plan, structure, architecture, data, or a prototype? Show a
|
|
786
|
-
relay board with mermaid/graphviz/chart/table/image/html
|
|
787
|
-
ASCII diagrams or walls of prose.
|
|
805
|
+
relay board with mermaid/graphviz/chart/table/code/diff/video/image/html
|
|
806
|
+
blocks — never ASCII diagrams or walls of prose.
|
|
807
|
+
- "Show me the diff / git diff / these changes"? Run \`git diff\` (or \`git show\`)
|
|
808
|
+
and render the output in a \`diff\` block — never paste a raw diff in the
|
|
809
|
+
terminal. Point the user at a file with a clickable local path in a markdown
|
|
810
|
+
block, and embed a screen recording with a \`video\` block.
|
|
788
811
|
- When answer choices are visual (designs, layouts, variants), give each
|
|
789
812
|
option its own visual (\`options[].blocks\`) so the user picks by looking.
|
|
790
813
|
- Read the result JSON from stdout; treat \`comment\` and \`annotations\` as
|
|
@@ -1031,8 +1054,9 @@ function cmdAgent() {
|
|
|
1031
1054
|
return 0;
|
|
1032
1055
|
}
|
|
1033
1056
|
|
|
1034
|
-
// `rly upgrade` — install the latest CLI globally AND refresh the
|
|
1035
|
-
//
|
|
1057
|
+
// `rly upgrade` — install the latest CLI globally AND refresh the skill (via the
|
|
1058
|
+
// npx-skills package manager, falling back to the bundled copy) in one shot.
|
|
1059
|
+
// (`update` is taken by the live-mutate command, so this is
|
|
1036
1060
|
// `upgrade` / `self-update`.) Running boards are surfaced and handled: a global
|
|
1037
1061
|
// reinstall overwrites relay's files, but live detached servers snapshot their
|
|
1038
1062
|
// UI at first request and serve from memory, so they keep working on their own
|
|
@@ -1051,7 +1075,7 @@ async function cmdUpgrade(args) {
|
|
|
1051
1075
|
if (args.dryRun === true) {
|
|
1052
1076
|
printJson({
|
|
1053
1077
|
dryRun: true,
|
|
1054
|
-
wouldRun: [wantCli && `npm install -g ${PKG_NAME}@latest`, wantSkill && '
|
|
1078
|
+
wouldRun: [wantCli && `npm install -g ${PKG_NAME}@latest`, wantSkill && 'npx skills add khanglvm/relay --skill relay --all'].filter(Boolean),
|
|
1055
1079
|
runningBoards: running.map((r) => r.id),
|
|
1056
1080
|
runningHandling: running.length
|
|
1057
1081
|
? doStop
|
|
@@ -1108,17 +1132,30 @@ async function cmdUpgrade(args) {
|
|
|
1108
1132
|
}
|
|
1109
1133
|
|
|
1110
1134
|
if (wantSkill) {
|
|
1111
|
-
//
|
|
1112
|
-
//
|
|
1113
|
-
|
|
1114
|
-
|
|
1135
|
+
// Refresh the skill through the npx-skills package manager — the same channel
|
|
1136
|
+
// the skill is normally managed by (skills-lock.json, ~/.agents, ~/.claude, …)
|
|
1137
|
+
// — so the global install stays in sync and pulls the freshest skill for
|
|
1138
|
+
// khanglvm/relay. `npx -y` so the one-off download needs no prompt.
|
|
1139
|
+
process.stderr.write('\nRefreshing the relay skill (npx skills add khanglvm/relay --skill relay --all)\n');
|
|
1140
|
+
let r = spawnSync('npx', ['-y', 'skills', 'add', 'khanglvm/relay', '--skill', 'relay', '--all'], { stdio: 'inherit', shell: true });
|
|
1141
|
+
let how = 'npx-skills';
|
|
1142
|
+
if (r.error || r.status !== 0) {
|
|
1143
|
+
// Fallback: install the skill bundled with the CLI we just upgraded. Works
|
|
1144
|
+
// offline / when the skills CLI is unreachable, and matches this version.
|
|
1145
|
+
process.stderr.write('\nnpx skills unavailable — falling back to the bundled skill (rly skill install)\n');
|
|
1146
|
+
r = spawnSync('rly', ['skill', 'install'], { stdio: 'inherit', shell: true });
|
|
1147
|
+
how = 'bundled';
|
|
1148
|
+
}
|
|
1115
1149
|
if (r.error || r.status !== 0) {
|
|
1116
1150
|
process.stderr.write(
|
|
1117
1151
|
`Skill refresh did not complete${r.error ? ` (${r.error.message})` : ` (exit ${r.status})`} — ` +
|
|
1118
|
-
'run `
|
|
1152
|
+
'run `npx skills add khanglvm/relay --skill relay --all` (or `rly skill install`) yourself.\n'
|
|
1119
1153
|
);
|
|
1120
1154
|
} else {
|
|
1121
|
-
|
|
1155
|
+
// Keep the freshness marker accurate however the skill landed, so the next
|
|
1156
|
+
// `rly` run doesn't nag that the just-refreshed skill is stale.
|
|
1157
|
+
stampSkillVersion();
|
|
1158
|
+
did.skill = how;
|
|
1122
1159
|
}
|
|
1123
1160
|
}
|
|
1124
1161
|
|
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
|
|