@khanglvm/relay 0.15.0 → 0.16.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/README.md +24 -1
- package/docs/AGENT.md +58 -14
- package/package.json +1 -1
- package/skills/relay/SKILL.md +41 -7
- package/src/cli.js +22 -11
- package/src/mcp-ui/board.js +46 -8
- package/src/mcp.js +9 -3
- package/src/server.js +110 -11
- package/src/spec.js +5 -0
- package/src/store.js +15 -0
- package/src/ui/annotate.js +68 -22
- package/src/ui/app.js +25 -6
- package/src/ui/blocks.css +90 -9
- package/src/ui/blocks.js +359 -30
package/README.md
CHANGED
|
@@ -78,7 +78,7 @@ unchanged; pick whichever surface fits.
|
|
|
78
78
|
| "Here's the diff — paste it in your editor" | Side-by-side **diff** blocks, syntax-highlighted **code**, **video** walkthroughs, and **file paths you click to open** in the default app |
|
|
79
79
|
| "Which commits should I pick? Resolve this conflict manually." | `rly git pick` / `rly git cherry-pick` boards and color-coded `git-conflict` resolvers with ours/theirs/custom hunk choices |
|
|
80
80
|
| "Type *done* when finished reviewing" | A Submit button; answers, notes, and inline comments returned as JSON |
|
|
81
|
-
| Feedback = another wall of text | Click any chart point, diagram node, table cell, or sentence to
|
|
81
|
+
| Feedback = another wall of text | Click any chart point, diagram node, table cell, or sentence; drag an image region to attach a local crop — the agent replies and the thread grows on the board |
|
|
82
82
|
|
|
83
83
|
Everything autosaves in real time, detached board links keep serving after
|
|
84
84
|
agent timeouts, multiple boards run at once, and the package has **zero npm
|
|
@@ -95,6 +95,7 @@ actually needs them.
|
|
|
95
95
|
| `rly git conflict [files…]` | auto-detect unmerged conflict files, or open specific local paths, and return resolved content in `result.blockEdits` |
|
|
96
96
|
| `rly share <board-id>` | activate/list/revoke same-Wi-Fi collaborator, reference-only reviewer, or read-only links for a running browser board |
|
|
97
97
|
| `rly view <file.md> …` | open a quick read-only board that renders local markdown file(s), data files, or PDFs — library-free; great for plans, READMEs, reports, quotes |
|
|
98
|
+
| `rly show --file spec.json --display-only` | present a browser board and continue immediately — no comment/note or acknowledgement required; inline `relay_show` uses this mode by default |
|
|
98
99
|
| `rly install --target <agent>` | write relay's rules into an agent's instruction file — `claude` `codex` `cursor` `copilot` `kiro` `windsurf` `cline` `gemini` `opencode` `droid` `agents`; `--all`, `--scope`, `--print`, `--list` |
|
|
99
100
|
| `rly upgrade` | update the CLI **and** refresh the skill in one step (safe around open boards; `--dry-run`, `--cli-only`, `--skill-only`) |
|
|
100
101
|
| `rly mcp` | run relay as an MCP App server so boards render **inline** in the chat — **stdio** for local desktop hosts (Claude Desktop, Codex), or `rly mcp --http` (Streamable HTTP) for web/mobile/remote; `rly mcp config` / `rly mcp install --target claude\|codex` to register it |
|
|
@@ -110,6 +111,28 @@ npm test # smoke tests with no external services (spawns real servers, fake-
|
|
|
110
111
|
|
|
111
112
|
## Changelog
|
|
112
113
|
|
|
114
|
+
### 0.16.1 — reliable area-comment zones
|
|
115
|
+
|
|
116
|
+
- Primary drag now draws a comment area directly on image and comparison views;
|
|
117
|
+
**Area** remains as a visible one-shot lock. Image pan uses Space-drag or the
|
|
118
|
+
middle button, while the comparison divider moves only from its handle.
|
|
119
|
+
- The selected rectangle remains visible while its comment is composed. Saved
|
|
120
|
+
zones carry a comment icon plus count, reopen for add/edit/delete, and disappear
|
|
121
|
+
automatically when their final comment is removed.
|
|
122
|
+
|
|
123
|
+
### 0.16.0 — native viewer chrome, area comments, and durable agent waits
|
|
124
|
+
|
|
125
|
+
- Viewer toolbars now use browser-native sticky positioning instead of a
|
|
126
|
+
scroll-event counter-translation; open comment composers re-anchor on scroll
|
|
127
|
+
and preserve unsaved per-target drafts.
|
|
128
|
+
- Hold then drag on an image to comment on an exact rectangular area. Relay
|
|
129
|
+
saves the crop beside the browser-board record and returns its path; comparison
|
|
130
|
+
crops also identify and capture the selected Before/After source.
|
|
131
|
+
- Browser boards support `responseRequired:false` / `--display-only`; inline
|
|
132
|
+
`relay_show` is display-only by default. Board and waiter deadlines now default
|
|
133
|
+
to one day (`--timeout 0` removes Relay's deadline), with client-specific
|
|
134
|
+
background/polling guidance in the agent docs.
|
|
135
|
+
|
|
113
136
|
### 0.11.0 — render inline inside the Claude & Codex apps (MCP App)
|
|
114
137
|
- **`rly mcp` — relay as an MCP App** ([SEP-1865](https://modelcontextprotocol.io/seps/1865-mcp-apps-interactive-user-interfaces-for-mcp),
|
|
115
138
|
extension `io.modelcontextprotocol/ui`). A stdio MCP server with no npm runtime dependencies
|
package/docs/AGENT.md
CHANGED
|
@@ -54,7 +54,7 @@ stderr. Exit codes: `0` submitted/acknowledged · `2` timeout · `3` cancelled
|
|
|
54
54
|
prints the result JSON:
|
|
55
55
|
|
|
56
56
|
```sh
|
|
57
|
-
rly ask --file spec.json --timeout
|
|
57
|
+
rly ask --file spec.json --timeout 86400 # one day; 0 = no Relay deadline
|
|
58
58
|
```
|
|
59
59
|
|
|
60
60
|
**2. Detached (recommended when your shell tool has an execution time limit).**
|
|
@@ -62,7 +62,7 @@ Returns immediately with the board URL; collect later:
|
|
|
62
62
|
|
|
63
63
|
```sh
|
|
64
64
|
rly ask --file spec.json --detach # → {"status":"open","boardId":"b-…","url":"…"}
|
|
65
|
-
rly wait b-xxxxx --timeout
|
|
65
|
+
rly wait b-xxxxx --timeout 86400 # default: one day; pass 0 for no deadline
|
|
66
66
|
rly result b-xxxxx # non-blocking peek; while open it includes
|
|
67
67
|
# the live autosaved draft of the user's answers
|
|
68
68
|
```
|
|
@@ -140,8 +140,9 @@ Both take **the exact same board spec** documented below (the tool `inputSchema`
|
|
|
140
140
|
fills it in, and their answers come back to you (answers, per-question notes,
|
|
141
141
|
comment) as a user-message turn, with structured context synced when the host
|
|
142
142
|
supports it — read them just as you would the CLI's result JSON. There is **no
|
|
143
|
-
`--detach`/`rly wait` dance, no stdout parsing, and no timeout** in this mode
|
|
144
|
-
|
|
143
|
+
`--detach`/`rly wait` dance, no stdout parsing, and no timeout** in this mode.
|
|
144
|
+
`relay_ask` stays live until the user submits and the host delivers the result;
|
|
145
|
+
`relay_show` is display-only by default and the agent continues immediately.
|
|
145
146
|
|
|
146
147
|
**Near-full parity with the browser board.** Local `codeFile` / `htmlFile` /
|
|
147
148
|
`diffFile` and local **image** files work inline too — the server inlines them
|
|
@@ -166,12 +167,25 @@ rly ask -q "Deploy to prod now?::yesno" -q "!Environment::single::dev,staging,pr
|
|
|
166
167
|
# label::type::comma,separated,options leading "!" = required
|
|
167
168
|
```
|
|
168
169
|
|
|
169
|
-
Visualization-only
|
|
170
|
+
Visualization-only, with acknowledgement/feedback:
|
|
170
171
|
|
|
171
172
|
```sh
|
|
172
173
|
rly show --html-file prototype.html --title "Dashboard concept" --height 600
|
|
173
174
|
```
|
|
174
175
|
|
|
176
|
+
Display-only (the agent continues immediately; no note, comments, or
|
|
177
|
+
Submit/Acknowledge action):
|
|
178
|
+
|
|
179
|
+
```sh
|
|
180
|
+
rly show --file spec.json --display-only
|
|
181
|
+
# equivalent spec field: {"responseRequired": false, "blocks": [...]}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
`--display-only` automatically detaches the browser board. In MCP App mode,
|
|
185
|
+
`relay_show` is display-only by default; set `responseRequired:true` only when
|
|
186
|
+
the user really needs to acknowledge or leave feedback. `relay_ask` remains the
|
|
187
|
+
response-bearing tool.
|
|
188
|
+
|
|
175
189
|
Read a markdown file (no questions; library-free renderer; submit reads "Done"):
|
|
176
190
|
|
|
177
191
|
```sh
|
|
@@ -219,6 +233,7 @@ choices plus the full resolved file in `result.blockEdits[blockId]`:
|
|
|
219
233
|
"allowPartial": true, // default true: user may submit with gaps -> "skipped"
|
|
220
234
|
"note": true, // default true: optional free-text box -> result "comment"
|
|
221
235
|
"autoClose": true, // default true: tab tries to close itself after submit
|
|
236
|
+
"responseRequired": true, // false = display-only; no feedback/action, agent continues
|
|
222
237
|
"submitLabel": "Submit",
|
|
223
238
|
"questions": [
|
|
224
239
|
{ "id": "approach", "type": "single", "label": "Which approach?", "required": true,
|
|
@@ -521,11 +536,11 @@ the question-types section above.
|
|
|
521
536
|
| `diff` | proposed code changes / before-after — a unified diff rendered as a colored git-style comparison (no git needed) |
|
|
522
537
|
| `video` | demos, screen recordings, walkthroughs — YouTube/Vimeo embeds, a media URL, or a local video file (streamed) |
|
|
523
538
|
| `pdf` | quotes, reports, exports, forms — local `.pdf` files or PDF URLs rendered inline; local files stream from the board server |
|
|
524
|
-
| `image` | screenshots, mockup exports, photos — local files embed and work offline |
|
|
539
|
+
| `image` | screenshots, mockup exports, photos — local files embed and work offline; drag to comment on exact pixels |
|
|
525
540
|
| `palette` | color palettes / themes — swatch cards with hover-hex + click-to-copy; pair with a `color` question to let the user pick |
|
|
526
541
|
| `kpi` | big-number metric cards (`items:[{label,value,delta?,dir?,sub?}]`) with up/down/flat-tinted deltas — at-a-glance numbers without a chart |
|
|
527
542
|
| `typography` | type specimens (`specimens:[{label?,size?,weight?,font?,text?}]`) — react to type choices like a palette |
|
|
528
|
-
| `compare` | before/after images with a draggable divider (`before`/`after` = url/path/`{src,label}`)
|
|
543
|
+
| `compare` | before/after images with a draggable divider (`before`/`after` = url/path/`{src,label}`); area comments preserve which side was selected |
|
|
529
544
|
| `html` | anything else — pixel-perfect mockups, custom widgets, embeds |
|
|
530
545
|
|
|
531
546
|
Two cross-block fields work on **any** block: `"ref":"name"` makes it openable in
|
|
@@ -534,6 +549,20 @@ shown earlier — no scrolling); `image` blocks also take `"pins":true` for
|
|
|
534
549
|
click-to-drop coordinate pin-comments. `table` blocks take `"rowsFile"` (load
|
|
535
550
|
rows from .csv/.tsv/.json), `"filterable"`, and `"exportable"` (CSV download).
|
|
536
551
|
|
|
552
|
+
Every interactive `image` and `compare` block also supports area feedback:
|
|
553
|
+
primary drag draws a rectangle directly; the viewer's **Area** control remains a
|
|
554
|
+
discoverable one-shot lock for the same gesture. On images, Space-drag or the
|
|
555
|
+
middle mouse button pans. In comparisons, only dragging the divider handle moves
|
|
556
|
+
the split. The provisional rectangle remains visible while the comment is
|
|
557
|
+
composed; a saved zone shows a comment icon plus count and reopens for
|
|
558
|
+
add/edit/delete. Removing its final comment removes the zone. Relay saves a pixel
|
|
559
|
+
crop beside the board record and returns its local path with the annotation, so
|
|
560
|
+
the agent can open the exact pixels instead of inferring from a whole-image
|
|
561
|
+
comment. In a comparison, the visible side where drawing began is recorded as
|
|
562
|
+
`before` or `after` and the crop comes from that source image. Remote images that
|
|
563
|
+
prevent browser canvas access still return coordinates and a `cropUnavailable`
|
|
564
|
+
marker.
|
|
565
|
+
|
|
537
566
|
### Height rules
|
|
538
567
|
|
|
539
568
|
- `markdown`, `code`: natural flow (no fixed height).
|
|
@@ -633,6 +662,8 @@ intro. Annotations are autosaved with the draft and returned in the final result
|
|
|
633
662
|
| `text` | `quote`, `prefix` (≤30 chars before), `suffix` (≤30 after) | selecting text in a markdown block |
|
|
634
663
|
| `html-element` | `ref`, `label`, `detail?` | hovering any element in a custom-HTML block (auto), or a `data-relay-annotate` / `relayKit.commentable()` element |
|
|
635
664
|
| `image` | `label` | clicking a PlantUML diagram or an image block |
|
|
665
|
+
| `image-point` | `x`, `y`, `label` | clicking an image authored with `pins:true` |
|
|
666
|
+
| `image-region` | `x`, `y`, `w`, `h`, `label`, optional `side`, `crop` | holding then dragging on an image or comparison; `crop.path` is the local image artifact in browser mode |
|
|
636
667
|
|
|
637
668
|
Read annotations as first-class feedback — they often carry the sharpest insight
|
|
638
669
|
(e.g. a user circling the one data point that concerns them, or quoting the exact
|
|
@@ -663,14 +694,15 @@ time). Use it instead of guessing timeouts:
|
|
|
663
694
|
```sh
|
|
664
695
|
rly result b-xxxxx # open board → includes "presence":
|
|
665
696
|
# {open, seen, visible, focused, secondsSinceActivity, secondsSincePing}
|
|
666
|
-
rly wait b-xxxxx --timeout
|
|
697
|
+
rly wait b-xxxxx --timeout 86400 --while-active --idle-grace 3600
|
|
667
698
|
```
|
|
668
699
|
|
|
669
700
|
`--while-active` keeps extending the wait as long as the user is demonstrably
|
|
670
701
|
active (page visible/focused and interaction within `--idle-grace` seconds,
|
|
671
|
-
default 180); once they
|
|
672
|
-
with `presence` attached
|
|
673
|
-
|
|
702
|
+
default 180); once the deadline is reached and they are idle it returns the
|
|
703
|
+
normal `wait-timeout` JSON, with `presence` attached. Relay now defaults both a
|
|
704
|
+
board and `rly wait` to 86,400 seconds (one day); pass `--timeout 0` for no Relay
|
|
705
|
+
deadline when the host can safely own a long-running waiter.
|
|
674
706
|
|
|
675
707
|
### A `timeout` on a detached board is NOT the end
|
|
676
708
|
|
|
@@ -685,7 +717,7 @@ tells them you stopped waiting and to prompt you afterward. So if you got a
|
|
|
685
717
|
so a late submit push-wakes you. A blocking `rly ask` (no `--detach`) still ends
|
|
686
718
|
hard on timeout, since there's no separate waiter to hand back to.
|
|
687
719
|
|
|
688
|
-
##
|
|
720
|
+
## Waiting and push-wake across agent clients
|
|
689
721
|
|
|
690
722
|
```sh
|
|
691
723
|
rly ask --file spec.json --detach --on-result 'curl -s -X POST localhost:9999/wake -d @-'
|
|
@@ -697,7 +729,19 @@ the board reaches a terminal status — submitted, acknowledged, timeout, or
|
|
|
697
729
|
cancelled — with the full result JSON piped to stdin and `RLY_BOARD_ID`,
|
|
698
730
|
`RLY_STATUS`, `RLY_URL` in the environment. `--notify-cmd` does the same from
|
|
699
731
|
a `wait` that obtains a terminal result. Write a file your harness watches,
|
|
700
|
-
hit a webhook — whatever wakes you.
|
|
732
|
+
hit a webhook — whatever wakes you. This is passive only when that client or
|
|
733
|
+
harness has an actual inbound wake mechanism; Relay does not pretend that a
|
|
734
|
+
file write alone starts a new model turn.
|
|
735
|
+
|
|
736
|
+
| Client/surface | Recommended Relay return path |
|
|
737
|
+
|---|---|
|
|
738
|
+
| MCP App (`relay_ask`) | No CLI wait. The inline board stays live and sends `ui/message` on submit, which starts the user-return turn. |
|
|
739
|
+
| MCP App (`relay_show`) | Display-only by default. Present it and continue immediately; no acknowledgement is requested. |
|
|
740
|
+
| Claude Code | Run `rly wait <id> --timeout 0` as a [background Bash task](https://docs.anthropic.com/en/docs/claude-code/interactive-mode#background-bash-commands); retain its task id/output rather than opening a second board. |
|
|
741
|
+
| Gemini CLI | Run the waiter in the background and set [`tools.shell.backgroundCompletionBehavior` to `"inject"`](https://geminicli.com/docs/reference/configuration/) so completion is returned to the agent; `"notify"` only informs the chat UI. |
|
|
742
|
+
| Codex | Keep one foreground waiter. Codex background terminals use explicit `write_stdin` polling (the [poll window defaults to five minutes](https://developers.openai.com/codex/config-reference)); the process can outlive each poll, but no portable browser-submit API wakes a stopped turn. |
|
|
743
|
+
| OpenCode | Core Bash is foreground. Its [ecosystem](https://opencode.ai/docs/ecosystem/) lists background-PTY extensions; use one only when installed, otherwise keep the waiter foreground. |
|
|
744
|
+
| Pi | Core intentionally has [no background Bash](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/docs/usage.md); keep the waiter foreground or use an explicitly installed background-task extension. |
|
|
701
745
|
|
|
702
746
|
### Codex browser-board pattern
|
|
703
747
|
|
|
@@ -709,7 +753,7 @@ submits:
|
|
|
709
753
|
|
|
710
754
|
```sh
|
|
711
755
|
rly ask --file spec.json --detach
|
|
712
|
-
rly wait b-xxxxx --timeout
|
|
756
|
+
rly wait b-xxxxx --timeout 86400 --while-active --idle-grace 3600
|
|
713
757
|
```
|
|
714
758
|
|
|
715
759
|
If `rly wait` exits with `wait-timeout`, immediately run `rly result <boardId>`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@khanglvm/relay",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.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/skills/relay/SKILL.md
CHANGED
|
@@ -85,8 +85,8 @@ priority call in a `rank` question — not in paragraphs. Unsure which exists? R
|
|
|
85
85
|
| `code` | source / config / command output — highlighted, line numbers, hover-a-line to comment |
|
|
86
86
|
| `diff` | code changes — colored unified/split, multi-file (`rly diff` builds the whole board) |
|
|
87
87
|
| `git-conflict` | conflict-marker files — side-by-side ours/theirs/base with hunk choices; returns resolved content in `result.blockEdits[blockId]` |
|
|
88
|
-
| `image` | screenshots / mockups / renders —
|
|
89
|
-
| `compare` | a before/after pair — draggable divider |
|
|
88
|
+
| `image` | screenshots / mockups / renders — drag → area crop; Space-drag/middle-button → pan; `pins:true` → point comments |
|
|
89
|
+
| `compare` | a before/after pair — draggable divider; area comments retain the selected side |
|
|
90
90
|
| `video` | a demo / screen recording / walkthrough |
|
|
91
91
|
| `pdf` | a quote / report / exported document that should render inline |
|
|
92
92
|
| `palette` | color schemes — swatch cards, hover-hex, click-to-copy |
|
|
@@ -130,7 +130,7 @@ blocking `rly ask` that gets killed cancels the board. Always prefer:
|
|
|
130
130
|
|
|
131
131
|
```sh
|
|
132
132
|
rly ask --file spec.json --detach # returns {"boardId":"b-…","url":…} immediately
|
|
133
|
-
rly wait b-xxxxx --timeout
|
|
133
|
+
rly wait b-xxxxx --timeout 86400 # default: one day; 0 = no Relay deadline
|
|
134
134
|
# (on exit 2 "wait-timeout" just run wait again)
|
|
135
135
|
rly result b-xxxxx # non-blocking peek (includes live draft)
|
|
136
136
|
```
|
|
@@ -142,7 +142,7 @@ create a new board just because a wait timed out.
|
|
|
142
142
|
For long waits prefer presence-aware waiting over a huge --timeout:
|
|
143
143
|
|
|
144
144
|
```sh
|
|
145
|
-
rly wait b-xxxxx --timeout
|
|
145
|
+
rly wait b-xxxxx --timeout 86400 --while-active --idle-grace 3600
|
|
146
146
|
# keeps extending while the user is demonstrably viewing/typing on the board;
|
|
147
147
|
# returns wait-timeout promptly once they are idle/gone (presence included)
|
|
148
148
|
rly result b-xxxxx # while open also shows presence {visible, focused, secondsSinceActivity}
|
|
@@ -159,7 +159,7 @@ So in Codex, keep the waiter in the foreground until the user submits:
|
|
|
159
159
|
|
|
160
160
|
```sh
|
|
161
161
|
rly ask --file spec.json --detach
|
|
162
|
-
rly wait b-xxxxx --timeout
|
|
162
|
+
rly wait b-xxxxx --timeout 86400 --while-active --idle-grace 3600
|
|
163
163
|
```
|
|
164
164
|
|
|
165
165
|
If `rly wait` exits with `wait-timeout`, immediately run `rly result <boardId>`.
|
|
@@ -168,9 +168,30 @@ If it is still open and the user may continue, run `rly wait` again. Do not use
|
|
|
168
168
|
webhooks, but normal Codex CLI sessions do not expose a portable inbound API
|
|
169
169
|
that wakes the current agent turn.
|
|
170
170
|
|
|
171
|
-
Blocking mode (`rly ask --file spec.json --timeout
|
|
171
|
+
Blocking mode (`rly ask --file spec.json --timeout 86400`, no `--detach`) is fine
|
|
172
172
|
ONLY when your shell tool has no execution time limit.
|
|
173
173
|
|
|
174
|
+
Relay defaults board and waiter deadlines to one day; use `--timeout 0` when the
|
|
175
|
+
client can safely own an indefinite process. Choose the return path by host:
|
|
176
|
+
|
|
177
|
+
- Inline MCP `relay_ask`: no CLI wait; its `ui/message` submission wakes the
|
|
178
|
+
return turn. Inline `relay_show` is display-only by default and does not wait.
|
|
179
|
+
- Claude Code: background `rly wait <id> --timeout 0` as a background Bash task.
|
|
180
|
+
- Gemini CLI: background the waiter and use
|
|
181
|
+
`tools.shell.backgroundCompletionBehavior: "inject"`.
|
|
182
|
+
- Codex: keep one foreground waiter; background terminals require explicit
|
|
183
|
+
polling and do not expose a portable browser-submit wake API.
|
|
184
|
+
- OpenCode/Pi: keep it foreground unless a background-task extension is
|
|
185
|
+
explicitly installed (Pi core has no background Bash).
|
|
186
|
+
|
|
187
|
+
Use `--on-result`/`--notify-cmd` only when the surrounding harness exposes a real
|
|
188
|
+
webhook, file watcher, or inbound wake path.
|
|
189
|
+
|
|
190
|
+
To present without asking for any response, use
|
|
191
|
+
`rly show --file spec.json --display-only` (equivalent to
|
|
192
|
+
`"responseRequired":false`). It returns immediately and shows no note,
|
|
193
|
+
comments, or Submit/Acknowledge action.
|
|
194
|
+
|
|
174
195
|
Useful flags: `--no-open` (don't auto-open the browser — for tests/CI; real
|
|
175
196
|
users need the tab, so omit it normally) · `--title` · `--timeout <sec>`.
|
|
176
197
|
|
|
@@ -418,6 +439,19 @@ Add `"pins": true` to an `image` block: the user clicks any point on the image t
|
|
|
418
439
|
drop a comment anchored to that exact spot (Figma-style), returned as an
|
|
419
440
|
`{kind:"image-point", x, y}` annotation. Ideal for design/mockup review.
|
|
420
441
|
|
|
442
|
+
### Comment on an exact image area (local crop)
|
|
443
|
+
|
|
444
|
+
Every interactive `image` and `compare` block accepts direct primary drag for the
|
|
445
|
+
desired rectangle; **Area** remains as a discoverable one-shot lock. Image pan is
|
|
446
|
+
Space-drag or middle-button drag, and a comparison divider moves only from its
|
|
447
|
+
handle. The provisional zone remains while the user writes, and a saved zone
|
|
448
|
+
exposes a comment icon/count plus add/edit/delete threads; deleting its last
|
|
449
|
+
comment removes the zone. Relay returns an `image-region` target with normalized
|
|
450
|
+
`x`, `y`, `w`, and `h`; browser boards also save the selected pixels beside the
|
|
451
|
+
board and return `target.crop.path`, which the agent should open with its image
|
|
452
|
+
viewer. Comparison targets include `side:"before"|"after"`, and the crop comes
|
|
453
|
+
from that source image rather than the composited slider view.
|
|
454
|
+
|
|
421
455
|
### Show a git diff in one step — `rly diff`
|
|
422
456
|
|
|
423
457
|
`rly diff [git args…]` runs `git diff` and opens the result as a diff board —
|
|
@@ -449,7 +483,7 @@ mention annotation in the board intro.
|
|
|
449
483
|
"id": "a1",
|
|
450
484
|
"questionId": "q-id or null",
|
|
451
485
|
"blockId": "b2",
|
|
452
|
-
"target": { "kind": "chart-element | mermaid-node | graphviz-node | table-cell | text | html-element | image | image-point | code-line", "..." },
|
|
486
|
+
"target": { "kind": "chart-element | mermaid-node | graphviz-node | table-cell | text | html-element | image | image-point | image-region | code-line", "..." },
|
|
453
487
|
"text": "user comment",
|
|
454
488
|
"author": "user",
|
|
455
489
|
"createdAt": "ISO",
|
package/src/cli.js
CHANGED
|
@@ -28,6 +28,8 @@ const BIN = path.join(PKG_ROOT, 'bin', 'rly.js');
|
|
|
28
28
|
const PKG_JSON = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, 'package.json'), 'utf8'));
|
|
29
29
|
const VERSION = PKG_JSON.version;
|
|
30
30
|
const PKG_NAME = PKG_JSON.name; // e.g. "@khanglvm/relay" — the global package to upgrade
|
|
31
|
+
const DEFAULT_BOARD_TIMEOUT_SEC = 86400;
|
|
32
|
+
const DEFAULT_WAIT_TIMEOUT_SEC = 86400;
|
|
31
33
|
|
|
32
34
|
const VALUED_FLAGS = new Set([
|
|
33
35
|
'file', 'html', 'html-file', 'title', 'intro', 'timeout', 'port',
|
|
@@ -171,6 +173,7 @@ async function resolveSpecInput(args, mode) {
|
|
|
171
173
|
if (args.htmlFile) raw.htmlFile = args.htmlFile;
|
|
172
174
|
if (args.height) raw.htmlHeight = args.height;
|
|
173
175
|
if (args.submitLabel) raw.submitLabel = args.submitLabel;
|
|
176
|
+
if (args.displayOnly === true) raw.responseRequired = false;
|
|
174
177
|
if (args.q.length) {
|
|
175
178
|
raw.questions = [...(raw.questions || []), ...args.q.map((s, i) => questionFromInline(s, i))];
|
|
176
179
|
}
|
|
@@ -198,7 +201,7 @@ function readFileOrThrow(p) {
|
|
|
198
201
|
|
|
199
202
|
async function runOrDetach(record, args) {
|
|
200
203
|
await assertSpecReady(record.spec);
|
|
201
|
-
const timeoutSec = args.timeout !== undefined ? Math.max(0, Number.parseInt(args.timeout, 10) || 0) :
|
|
204
|
+
const timeoutSec = args.timeout !== undefined ? Math.max(0, Number.parseInt(args.timeout, 10) || 0) : DEFAULT_BOARD_TIMEOUT_SEC;
|
|
202
205
|
const port = args.port !== undefined ? Number.parseInt(args.port, 10) || 0 : 0;
|
|
203
206
|
const open = args.open !== false;
|
|
204
207
|
|
|
@@ -231,7 +234,9 @@ async function runOrDetach(record, args) {
|
|
|
231
234
|
boardId: record.id,
|
|
232
235
|
url: info.url,
|
|
233
236
|
port: info.port,
|
|
234
|
-
hint:
|
|
237
|
+
hint: record.spec.responseRequired === false
|
|
238
|
+
? 'display-only board opened; no response will be collected'
|
|
239
|
+
: `block for the answers with: rly wait ${record.id}`,
|
|
235
240
|
});
|
|
236
241
|
return 0;
|
|
237
242
|
}
|
|
@@ -247,7 +252,7 @@ async function cmdAsk(args, mode) {
|
|
|
247
252
|
const spec = normalizeSpec(raw);
|
|
248
253
|
await assertSpecReady(spec);
|
|
249
254
|
const record = createBoard(spec);
|
|
250
|
-
return runOrDetach(record, args);
|
|
255
|
+
return runOrDetach(record, spec.responseRequired === false ? { ...args, detach: true } : args);
|
|
251
256
|
}
|
|
252
257
|
|
|
253
258
|
// `rly diff [git args…]` — run `git diff` for the user and open the result as a
|
|
@@ -639,11 +644,11 @@ async function fetchPresence(url) {
|
|
|
639
644
|
async function cmdWait(args) {
|
|
640
645
|
const id = args._[0];
|
|
641
646
|
if (!id) throw new CliError('usage: rly wait <board-id> [--timeout <sec>] [--while-active] [--idle-grace <sec>] [--notify-cmd <cmd>]');
|
|
642
|
-
const timeoutSec = args.timeout !== undefined ? Math.max(
|
|
647
|
+
const timeoutSec = args.timeout !== undefined ? Math.max(0, Number.parseInt(args.timeout, 10) || 0) : DEFAULT_WAIT_TIMEOUT_SEC;
|
|
643
648
|
const whileActive = args.whileActive === true;
|
|
644
649
|
const idleGrace = args.idleGrace !== undefined ? Math.max(0, Number.parseInt(args.idleGrace, 10) || 0) : 180;
|
|
645
650
|
const notifyCmd = typeof args.notifyCmd === 'string' && args.notifyCmd.trim() ? args.notifyCmd : null;
|
|
646
|
-
let deadline = Date.now() + timeoutSec * 1000;
|
|
651
|
+
let deadline = timeoutSec === 0 ? Number.POSITIVE_INFINITY : Date.now() + timeoutSec * 1000;
|
|
647
652
|
mustLoad(id);
|
|
648
653
|
|
|
649
654
|
// Push-wake: run the agent's --notify-cmd after a TERMINAL result, then print.
|
|
@@ -1086,7 +1091,7 @@ the user should view — put it in relay instead of printing it.**
|
|
|
1086
1091
|
user-facing "wake this agent turn from a browser submit" command. In Codex,
|
|
1087
1092
|
keep the waiter in the foreground until the user submits:
|
|
1088
1093
|
\`rly ask --file spec.json --detach\` then
|
|
1089
|
-
\`rly wait <boardId> --timeout
|
|
1094
|
+
\`rly wait <boardId> --timeout 86400 --while-active --idle-grace 3600\`.
|
|
1090
1095
|
If wait exits with \`wait-timeout\`, immediately run \`rly result <boardId>\`;
|
|
1091
1096
|
if it is still open and the user may continue, run \`rly wait\` again. Do not
|
|
1092
1097
|
use \`--on-result\` as the primary Codex return path; normal Codex CLI sessions
|
|
@@ -1094,6 +1099,10 @@ the user should view — put it in relay instead of printing it.**
|
|
|
1094
1099
|
Detached boards are durable: a board timeout hands the agent a \`timeout\`
|
|
1095
1100
|
result but keeps the same URL/port serving until Submit or \`rly stop\`. Do
|
|
1096
1101
|
not create a new board just because a wait timed out.
|
|
1102
|
+
- **No response needed** → \`rly show --file spec.json --display-only\`. It
|
|
1103
|
+
returns immediately and shows no note, comments, or Submit/Acknowledge action.
|
|
1104
|
+
In MCP App mode, \`relay_show\` is display-only by default; \`relay_ask\` is the
|
|
1105
|
+
response-bearing tool.
|
|
1097
1106
|
- **A plan, structure, architecture, data, or prototype** → a relay board with
|
|
1098
1107
|
diagram/chart/table/code/image/html blocks — never ASCII diagrams or walls of prose.
|
|
1099
1108
|
- **"Show me the diff / git diff / these changes"** → \`rly diff\` (runs git diff →
|
|
@@ -1116,7 +1125,7 @@ the user should view — put it in relay instead of printing it.**
|
|
|
1116
1125
|
- **There's a purpose-built component for most content — use the MOST SPECIFIC one,
|
|
1117
1126
|
never plain prose when a block fits.** Blocks: \`table\` (sortable/filterable/CSV,
|
|
1118
1127
|
load from .csv/.json), \`chart\`, \`kpi\` (stat cards), \`mermaid\`/\`graphviz\`/\`plantuml\`,
|
|
1119
|
-
\`code\`, \`diff\`, \`git-conflict\`, \`image\` (+\`pins\`), \`compare\` (before/after), \`video\`, \`pdf\`, \`palette\`,
|
|
1128
|
+
\`code\`, \`diff\`, \`git-conflict\`, \`image\` (drag for crop comments; Space-drag/middle-button to pan; +\`pins\` for points), \`compare\` (before/after with side-aware area crops; divider-handle drag), \`video\`, \`pdf\`, \`palette\`,
|
|
1120
1129
|
\`typography\`, \`html\`. Question types: \`single\`/\`multi\`/\`yesno\`/\`scale\`/\`color\`/
|
|
1121
1130
|
\`text\`/\`textarea\` plus \`rank\` (prioritize), \`allocate\` (split a budget), \`checklist\`
|
|
1122
1131
|
(per-item sign-off). For a business user, reach for \`kpi\`+\`chart\`+\`table\` and
|
|
@@ -1580,7 +1589,7 @@ async function cmdServeInternal(args) {
|
|
|
1580
1589
|
id,
|
|
1581
1590
|
port: args.port !== undefined ? Number.parseInt(args.port, 10) || 0 : 0,
|
|
1582
1591
|
open: args.open !== false,
|
|
1583
|
-
timeoutSec: args.timeout !== undefined ? Math.max(0, Number.parseInt(args.timeout, 10) || 0) :
|
|
1592
|
+
timeoutSec: args.timeout !== undefined ? Math.max(0, Number.parseInt(args.timeout, 10) || 0) : DEFAULT_BOARD_TIMEOUT_SEC,
|
|
1584
1593
|
quiet: true,
|
|
1585
1594
|
// Detached board: timeout hands back to the agent but keeps the same
|
|
1586
1595
|
// URL/port serving until Submit or explicit stop.
|
|
@@ -1600,7 +1609,9 @@ USAGE
|
|
|
1600
1609
|
quick inline questions ("!" = required, label::type::options)
|
|
1601
1610
|
rly ask ... --detach no blocking: prints {boardId,url} now; collect via \`rly wait <id>\`
|
|
1602
1611
|
rly ask ... --on-result "<cmd>" push-wake: run <cmd> when the board finishes (result JSON on stdin)
|
|
1603
|
-
rly show --html-file viz.html visualization
|
|
1612
|
+
rly show --html-file viz.html visualization board (submit button = acknowledge)
|
|
1613
|
+
rly show --file spec.json --display-only
|
|
1614
|
+
present without comments/acknowledgement; returns immediately
|
|
1604
1615
|
rly view <file.md> [more.md …] quick read-only board rendering markdown file(s) (no lib)
|
|
1605
1616
|
(.csv/.tsv/.json render as a filterable, sortable table;
|
|
1606
1617
|
.pdf streams in an inline PDF viewer)
|
|
@@ -1611,7 +1622,7 @@ USAGE
|
|
|
1611
1622
|
add --code for split code view with per-hunk Apply/Skip/Hold
|
|
1612
1623
|
rly git conflict [files…] board for resolving conflict-marker files; with no files, auto-detects
|
|
1613
1624
|
unmerged git files and returns resolved content in result.blockEdits
|
|
1614
|
-
rly wait <id> [--timeout
|
|
1625
|
+
rly wait <id> [--timeout 86400] block up to one day until the board finishes (0 = no deadline)
|
|
1615
1626
|
--while-active [--idle-grace 180]: keep waiting past the deadline
|
|
1616
1627
|
while the user is still viewing/focused & recently active
|
|
1617
1628
|
--notify-cmd "<cmd>": run <cmd> on a terminal result (JSON on stdin)
|
|
@@ -1655,7 +1666,7 @@ USAGE
|
|
|
1655
1666
|
|
|
1656
1667
|
COMMON FLAGS
|
|
1657
1668
|
--title <s> --intro <s> --html-file <f> --height <px> --submit-label <s>
|
|
1658
|
-
--timeout <sec> (default
|
|
1669
|
+
--timeout <sec> (default 86400; 0 = none) --port <n> --no-open --detach --display-only
|
|
1659
1670
|
|
|
1660
1671
|
EXIT CODES 0 submitted/acknowledged · 2 timeout · 3 cancelled · 4 usage · 5 not found
|
|
1661
1672
|
|
package/src/mcp-ui/board.js
CHANGED
|
@@ -219,6 +219,7 @@
|
|
|
219
219
|
allowPartial: raw.allowPartial !== false,
|
|
220
220
|
note: raw.note !== false,
|
|
221
221
|
autoClose: raw.autoClose !== false,
|
|
222
|
+
responseRequired: raw.responseRequired !== false,
|
|
222
223
|
questions: [],
|
|
223
224
|
submitLabel: typeof raw.submitLabel === 'string' ? raw.submitLabel : '',
|
|
224
225
|
};
|
|
@@ -388,7 +389,13 @@
|
|
|
388
389
|
questionId: questionId == null ? null : questionId,
|
|
389
390
|
// No annotation during the streaming preview (it re-renders); on the final
|
|
390
391
|
// interactive render, wire the engine so blocks register their targets.
|
|
391
|
-
annotate: composing ? null : Annotate,
|
|
392
|
+
annotate: composing || (spec && spec.responseRequired === false) ? null : Annotate,
|
|
393
|
+
canComment: Boolean(spec && spec.responseRequired !== false),
|
|
394
|
+
saveArtifact: async ({ dataUrl, mime, width, height }) => {
|
|
395
|
+
const comma = typeof dataUrl === 'string' ? dataUrl.indexOf(',') : -1;
|
|
396
|
+
if (comma < 0) throw new Error('invalid image crop');
|
|
397
|
+
return { data: dataUrl.slice(comma + 1), mime: mime || 'image/png', width, height };
|
|
398
|
+
},
|
|
392
399
|
edits: state.blockEdits,
|
|
393
400
|
onBlockEdit: (blockId, codeOrNull) => {
|
|
394
401
|
if (codeOrNull === null || codeOrNull === undefined) delete state.blockEdits[blockId];
|
|
@@ -882,7 +889,7 @@
|
|
|
882
889
|
app.append(card);
|
|
883
890
|
});
|
|
884
891
|
|
|
885
|
-
if (spec.note) {
|
|
892
|
+
if (spec.note && spec.responseRequired !== false) {
|
|
886
893
|
const note = el('textarea', { placeholder: 'optional note back to the agent…' });
|
|
887
894
|
note.value = state.comment || '';
|
|
888
895
|
note.addEventListener('input', () => { state.comment = note.value; });
|
|
@@ -898,6 +905,12 @@
|
|
|
898
905
|
reportSize();
|
|
899
906
|
return;
|
|
900
907
|
}
|
|
908
|
+
if (spec.responseRequired === false) {
|
|
909
|
+
app.append(el('div', { class: 'submitbar display-only' },
|
|
910
|
+
el('span', { class: 'hint' }, 'Display only · no response requested')));
|
|
911
|
+
reportSize();
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
901
914
|
const submitBtn = el('button', { class: 'submit', type: 'button' }, spec.submitLabel);
|
|
902
915
|
const saveEl = el('span', { class: 'savestate' }, '');
|
|
903
916
|
const hint = el('span', { class: 'hint' }, QS.length && spec.allowPartial ? 'Unanswered questions are returned as skipped.' : '');
|
|
@@ -940,7 +953,9 @@
|
|
|
940
953
|
lines.push('', 'Inline comments (' + data.annotations.length + '):');
|
|
941
954
|
for (const a of data.annotations) {
|
|
942
955
|
const where = (a.target && (a.target.label || a.target.text || a.target.kind)) || a.blockId || 'element';
|
|
943
|
-
|
|
956
|
+
const crop = a.target && a.target.crop;
|
|
957
|
+
const cropNote = crop && crop.path ? ' · crop: ' + crop.path : crop && crop.data ? ' · image crop attached' : '';
|
|
958
|
+
lines.push('- [' + where + ']: ' + a.text + cropNote);
|
|
944
959
|
}
|
|
945
960
|
}
|
|
946
961
|
if (data.blockEdits) lines.push('', 'Edited diagrams: ' + Object.keys(data.blockEdits).join(', '));
|
|
@@ -964,24 +979,43 @@
|
|
|
964
979
|
annotations: data.annotations,
|
|
965
980
|
};
|
|
966
981
|
const text = summarize(data);
|
|
982
|
+
const messageContent = [{ type: 'text', text }];
|
|
983
|
+
for (const a of data.annotations || []) {
|
|
984
|
+
const crop = a && a.target && a.target.crop;
|
|
985
|
+
if (!crop || typeof crop.data !== 'string' || !crop.data) continue;
|
|
986
|
+
messageContent.push({ type: 'image', data: crop.data, mimeType: crop.mime || 'image/png' });
|
|
987
|
+
}
|
|
967
988
|
let messageDelivered = false;
|
|
968
989
|
let contextDelivered = false;
|
|
969
990
|
try {
|
|
970
991
|
// A completed relay form is a user reply, not passive context. Some hosts
|
|
971
992
|
// ACK `ui/update-model-context` without starting a new model turn, so send
|
|
972
993
|
// the transcript as a user message first to wake the agent reliably.
|
|
973
|
-
await request('ui/message', { role: 'user', content:
|
|
994
|
+
await request('ui/message', { role: 'user', content: messageContent });
|
|
974
995
|
messageDelivered = true;
|
|
975
996
|
} catch {
|
|
976
|
-
// Older
|
|
997
|
+
// Older hosts may not advertise image content or may implement the early
|
|
998
|
+
// single-content draft. Preserve the text wake path before giving up.
|
|
999
|
+
try {
|
|
1000
|
+
await request('ui/message', { role: 'user', content: [{ type: 'text', text }] });
|
|
1001
|
+
messageDelivered = true;
|
|
1002
|
+
} catch {
|
|
1003
|
+
try {
|
|
1004
|
+
await request('ui/message', { role: 'user', content: { type: 'text', text } });
|
|
1005
|
+
messageDelivered = true;
|
|
1006
|
+
} catch { /* app-initiated messages unavailable */ }
|
|
1007
|
+
}
|
|
977
1008
|
}
|
|
978
1009
|
try {
|
|
979
1010
|
// Keep the structured payload available to hosts that attach app context.
|
|
980
1011
|
// This is best-effort because context updates are intentionally silent.
|
|
981
|
-
await request('ui/update-model-context', { content:
|
|
1012
|
+
await request('ui/update-model-context', { content: messageContent, structuredContent: structured });
|
|
982
1013
|
contextDelivered = true;
|
|
983
1014
|
} catch {
|
|
984
|
-
|
|
1015
|
+
try {
|
|
1016
|
+
await request('ui/update-model-context', { content: [{ type: 'text', text }], structuredContent: structured });
|
|
1017
|
+
contextDelivered = true;
|
|
1018
|
+
} catch { /* ui/message may still have delivered the result */ }
|
|
985
1019
|
}
|
|
986
1020
|
submitted = true;
|
|
987
1021
|
showDone(messageDelivered, contextDelivered);
|
|
@@ -1029,7 +1063,11 @@
|
|
|
1029
1063
|
seedDefaults();
|
|
1030
1064
|
indexHtmlBlocks(spec);
|
|
1031
1065
|
setStatus('Preparing…');
|
|
1032
|
-
if (Annotate
|
|
1066
|
+
if (Annotate && spec.responseRequired !== false) {
|
|
1067
|
+
Annotate.init({ initial: [], onChange: (a) => { state.annotations = Array.isArray(a) ? a : []; } });
|
|
1068
|
+
} else if (Annotate && typeof Annotate.teardown === 'function') {
|
|
1069
|
+
Annotate.teardown();
|
|
1070
|
+
}
|
|
1033
1071
|
try { await preloadVendors(spec); } catch { /* render anyway; blocks degrade individually */ }
|
|
1034
1072
|
render();
|
|
1035
1073
|
applyDisplayMode();
|
package/src/mcp.js
CHANGED
|
@@ -111,7 +111,7 @@ function tools() {
|
|
|
111
111
|
{
|
|
112
112
|
name: 'relay_show',
|
|
113
113
|
description:
|
|
114
|
-
'Present work to the user on an inline board
|
|
114
|
+
'Present work to the user on an inline display-only board — a plan, architecture diagram, data, diff, prototype, or image — using rich blocks. It returns immediately and does not ask the user to acknowledge. Set responseRequired:true only when feedback or acknowledgement is actually needed.',
|
|
115
115
|
inputSchema: schema,
|
|
116
116
|
_meta: uiToolMeta(),
|
|
117
117
|
},
|
|
@@ -157,7 +157,7 @@ async function buildResult(method, params, clientProtocol) {
|
|
|
157
157
|
},
|
|
158
158
|
serverInfo: { name: 'relay', version: PKG.version },
|
|
159
159
|
instructions:
|
|
160
|
-
'relay renders
|
|
160
|
+
'relay renders boards inline. Call relay_ask to collect decisions/feedback and wait for its user-message submission. Call relay_show to present plans/diagrams/data without waiting; it is display-only unless responseRequired:true is explicit.',
|
|
161
161
|
};
|
|
162
162
|
case 'ping':
|
|
163
163
|
return {};
|
|
@@ -209,9 +209,12 @@ async function callTool(params) {
|
|
|
209
209
|
return { content: [{ type: 'text', text: 'unknown tool: ' + name }], isError: true };
|
|
210
210
|
}
|
|
211
211
|
const args = (params && params.arguments && typeof params.arguments === 'object') ? params.arguments : {};
|
|
212
|
+
const normalizedArgs = name === 'relay_show' && args.responseRequired === undefined
|
|
213
|
+
? { ...args, responseRequired: false }
|
|
214
|
+
: args;
|
|
212
215
|
let spec;
|
|
213
216
|
try {
|
|
214
|
-
spec = normalizeSpec(
|
|
217
|
+
spec = normalizeSpec(normalizedArgs);
|
|
215
218
|
await assertSpecReady(spec);
|
|
216
219
|
} catch (err) {
|
|
217
220
|
const msg = err instanceof CliError ? err.message : String((err && err.message) || err);
|
|
@@ -232,6 +235,9 @@ async function callTool(params) {
|
|
|
232
235
|
// when the board clearly didn't render for the user.
|
|
233
236
|
function boardText(spec) {
|
|
234
237
|
const nQ = spec.questions.length;
|
|
238
|
+
if (spec.responseRequired === false) {
|
|
239
|
+
return `Relay board "${spec.title}" is now displayed to the user. No acknowledgement is requested; continue your work without waiting.`;
|
|
240
|
+
}
|
|
235
241
|
if (!nQ) {
|
|
236
242
|
return `Relay board "${spec.title}" is now displayed to the user. They can review it and acknowledge; any feedback will be delivered back to you.`;
|
|
237
243
|
}
|