@khanglvm/relay 0.15.0 → 0.16.0

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 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 comment — the agent replies and the thread grows on the board |
81
+ | Feedback = another wall of text | Click any chart point, diagram node, table cell, or sentence; hold+drag an image area 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,19 @@ npm test # smoke tests with no external services (spawns real servers, fake-
110
111
 
111
112
  ## Changelog
112
113
 
114
+ ### 0.16.0 — native viewer chrome, area comments, and durable agent waits
115
+
116
+ - Viewer toolbars now use browser-native sticky positioning instead of a
117
+ scroll-event counter-translation; open comment composers re-anchor on scroll
118
+ and preserve unsaved per-target drafts.
119
+ - Hold then drag on an image to comment on an exact rectangular area. Relay
120
+ saves the crop beside the browser-board record and returns its path; comparison
121
+ crops also identify and capture the selected Before/After source.
122
+ - Browser boards support `responseRequired:false` / `--display-only`; inline
123
+ `relay_show` is display-only by default. Board and waiter deadlines now default
124
+ to one day (`--timeout 0` removes Relay's deadline), with client-specific
125
+ background/polling guidance in the agent docs.
126
+
113
127
  ### 0.11.0 — render inline inside the Claude & Codex apps (MCP App)
114
128
  - **`rly mcp` — relay as an MCP App** ([SEP-1865](https://modelcontextprotocol.io/seps/1865-mcp-apps-interactive-user-interfaces-for-mcp),
115
129
  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 1800
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 3500 # blocks until submit, prints result JSON
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; the
144
- board stays live until the user submits and the host delivers the result.
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 (no questions; submit button reads "Acknowledge"):
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; hold then drag to comment on an exact area |
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}`) redesigns, before-after fixes |
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,14 @@ 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
+ hold briefly, then drag a rectangle. Relay saves a pixel crop beside the board
554
+ record and returns its local path with the annotation, so the agent can open the
555
+ exact pixels instead of inferring from a whole-image comment. In a comparison,
556
+ the side visible where the hold began is recorded as `before` or `after` and the
557
+ crop comes from that source image. Remote images that prevent browser canvas
558
+ access still return coordinates and a `cropUnavailable` marker.
559
+
537
560
  ### Height rules
538
561
 
539
562
  - `markdown`, `code`: natural flow (no fixed height).
@@ -633,6 +656,8 @@ intro. Annotations are autosaved with the draft and returned in the final result
633
656
  | `text` | `quote`, `prefix` (≤30 chars before), `suffix` (≤30 after) | selecting text in a markdown block |
634
657
  | `html-element` | `ref`, `label`, `detail?` | hovering any element in a custom-HTML block (auto), or a `data-relay-annotate` / `relayKit.commentable()` element |
635
658
  | `image` | `label` | clicking a PlantUML diagram or an image block |
659
+ | `image-point` | `x`, `y`, `label` | clicking an image authored with `pins:true` |
660
+ | `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
661
 
637
662
  Read annotations as first-class feedback — they often carry the sharpest insight
638
663
  (e.g. a user circling the one data point that concerns them, or quoting the exact
@@ -663,14 +688,15 @@ time). Use it instead of guessing timeouts:
663
688
  ```sh
664
689
  rly result b-xxxxx # open board → includes "presence":
665
690
  # {open, seen, visible, focused, secondsSinceActivity, secondsSincePing}
666
- rly wait b-xxxxx --timeout 550 --while-active --idle-grace 180
691
+ rly wait b-xxxxx --timeout 86400 --while-active --idle-grace 3600
667
692
  ```
668
693
 
669
694
  `--while-active` keeps extending the wait as long as the user is demonstrably
670
695
  active (page visible/focused and interaction within `--idle-grace` seconds,
671
- default 180); once they go idle it returns the normal `wait-timeout` JSON,
672
- with `presence` attached so you can decide what to do next. Prefer this over
673
- raising `--timeout`.
696
+ default 180); once the deadline is reached and they are idle it returns the
697
+ normal `wait-timeout` JSON, with `presence` attached. Relay now defaults both a
698
+ board and `rly wait` to 86,400 seconds (one day); pass `--timeout 0` for no Relay
699
+ deadline when the host can safely own a long-running waiter.
674
700
 
675
701
  ### A `timeout` on a detached board is NOT the end
676
702
 
@@ -685,7 +711,7 @@ tells them you stopped waiting and to prompt you afterward. So if you got a
685
711
  so a late submit push-wakes you. A blocking `rly ask` (no `--detach`) still ends
686
712
  hard on timeout, since there's no separate waiter to hand back to.
687
713
 
688
- ## Push-wake get notified instead of polling
714
+ ## Waiting and push-wake across agent clients
689
715
 
690
716
  ```sh
691
717
  rly ask --file spec.json --detach --on-result 'curl -s -X POST localhost:9999/wake -d @-'
@@ -697,7 +723,19 @@ the board reaches a terminal status — submitted, acknowledged, timeout, or
697
723
  cancelled — with the full result JSON piped to stdin and `RLY_BOARD_ID`,
698
724
  `RLY_STATUS`, `RLY_URL` in the environment. `--notify-cmd` does the same from
699
725
  a `wait` that obtains a terminal result. Write a file your harness watches,
700
- hit a webhook — whatever wakes you.
726
+ hit a webhook — whatever wakes you. This is passive only when that client or
727
+ harness has an actual inbound wake mechanism; Relay does not pretend that a
728
+ file write alone starts a new model turn.
729
+
730
+ | Client/surface | Recommended Relay return path |
731
+ |---|---|
732
+ | MCP App (`relay_ask`) | No CLI wait. The inline board stays live and sends `ui/message` on submit, which starts the user-return turn. |
733
+ | MCP App (`relay_show`) | Display-only by default. Present it and continue immediately; no acknowledgement is requested. |
734
+ | 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. |
735
+ | 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. |
736
+ | 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. |
737
+ | 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. |
738
+ | 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
739
 
702
740
  ### Codex browser-board pattern
703
741
 
@@ -709,7 +747,7 @@ submits:
709
747
 
710
748
  ```sh
711
749
  rly ask --file spec.json --detach
712
- rly wait b-xxxxx --timeout 1800 --while-active --idle-grace 300
750
+ rly wait b-xxxxx --timeout 86400 --while-active --idle-grace 3600
713
751
  ```
714
752
 
715
753
  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.15.0",
3
+ "version": "0.16.0",
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",
@@ -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 — zoom+pan; `pins:true` → click-to-drop point comments |
89
- | `compare` | a before/after pair — draggable divider |
88
+ | `image` | screenshots / mockups / renders — zoom+pan; hold+drag → area crop comment; `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 550 # blocks until submit, prints result JSON
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 550 --while-active --idle-grace 180
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 1800 --while-active --idle-grace 300
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 1800`, no --detach) is fine
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,15 @@ 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 supports hold-then-drag area
445
+ feedback without another spec flag. Relay returns an `image-region` target with
446
+ normalized `x`, `y`, `w`, and `h`; browser boards also save the selected pixels
447
+ beside the board and return `target.crop.path`, which the agent should open with
448
+ its image viewer. Comparison targets include `side:"before"|"after"`, and the
449
+ crop comes from that source image rather than the composited slider view.
450
+
421
451
  ### Show a git diff in one step — `rly diff`
422
452
 
423
453
  `rly diff [git args…]` runs `git diff` and opens the result as a diff board —
@@ -449,7 +479,7 @@ mention annotation in the board intro.
449
479
  "id": "a1",
450
480
  "questionId": "q-id or null",
451
481
  "blockId": "b2",
452
- "target": { "kind": "chart-element | mermaid-node | graphviz-node | table-cell | text | html-element | image | image-point | code-line", "..." },
482
+ "target": { "kind": "chart-element | mermaid-node | graphviz-node | table-cell | text | html-element | image | image-point | image-region | code-line", "..." },
453
483
  "text": "user comment",
454
484
  "author": "user",
455
485
  "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) : 1800;
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: `block for the answers with: rly wait ${record.id}`,
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(1, Number.parseInt(args.timeout, 10) || 1) : 3600;
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 1800 --while-active --idle-grace 300\`.
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\` (hold+drag area crops; +\`pins\` for points), \`compare\` (before/after with side-aware area crops), \`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) : 1800,
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-only board (submit button = acknowledge)
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 3600] block until board finishes, print result JSON
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 1800; 0 = none) --port <n> --no-open --detach
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
 
@@ -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
- lines.push('- [' + where + ']: ' + a.text);
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: { type: 'text', text } });
994
+ await request('ui/message', { role: 'user', content: messageContent });
974
995
  messageDelivered = true;
975
996
  } catch {
976
- // Older/leaner hosts may not expose app-initiated messages.
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: [{ type: 'text', text }], structuredContent: structured });
1012
+ await request('ui/update-model-context', { content: messageContent, structuredContent: structured });
982
1013
  contextDelivered = true;
983
1014
  } catch {
984
- // If ui/message worked, the agent still receives the submission transcript.
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) Annotate.init({ initial: [], onChange: (a) => { state.annotations = Array.isArray(a) ? a : []; } });
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 WITHOUT necessarily asking questions — a plan, an architecture diagram, data, a diff, a prototype — using rich blocks (markdown, mermaid, graphviz, chart, table, code, diff, image, html). The board shows a Submit/Acknowledge button. Same spec as relay_ask; typically blocks-only.',
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 interactive boards inline. Call relay_ask to collect decisions/feedback with real form controls, or relay_show to present plans/diagrams/data instead of asking in plain text. When the user submits, relay sends their answers back as a user message so the agent continues.',
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(args);
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
  }