@khanglvm/relay 0.13.3 → 0.13.5
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 +10 -8
- package/docs/AGENT.md +2 -1
- package/package.json +1 -1
- package/src/mcp-ui/board.js +23 -9
- package/src/mcp.js +7 -6
- package/src/server.js +12 -3
package/README.md
CHANGED
|
@@ -61,10 +61,11 @@ rly mcp config
|
|
|
61
61
|
|
|
62
62
|
When the agent calls a relay tool, the host renders relay's `ui://relay/board`
|
|
63
63
|
resource in a sandboxed iframe, hands it the board spec, and the user's answers
|
|
64
|
-
flow back to the agent
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
64
|
+
flow back to the agent as a `ui/message` user turn, with
|
|
65
|
+
`ui/update-model-context` used as a best-effort structured context sync —
|
|
66
|
+
markdown, code, diffs, tables, charts, mermaid/graphviz diagrams, images, and
|
|
67
|
+
forms, all in-chat. The classic browser board (`rly ask` / `rly show`) is
|
|
68
|
+
unchanged; pick whichever surface fits.
|
|
68
69
|
|
|
69
70
|
## What it improves
|
|
70
71
|
|
|
@@ -114,10 +115,11 @@ npm test # zero-dep smoke tests (spawns real servers, fake-submits)
|
|
|
114
115
|
- **Same board, postMessage transport.** The inline board reuses relay's block
|
|
115
116
|
renderer (markdown, code, diff, table, chart, mermaid, graphviz, image, html)
|
|
116
117
|
over the MCP Apps JSON-RPC bridge: the spec arrives as the tool result, the
|
|
117
|
-
user's
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
118
|
+
user's submit goes back via `ui/message` so the agent resumes, the structured
|
|
119
|
+
payload is also offered through `ui/update-model-context`, the iframe
|
|
120
|
+
auto-sizes via `ui/notifications/size-changed`, and vendored Chart.js /
|
|
121
|
+
Mermaid / Viz.js load on demand through the host's `resources/read` (no
|
|
122
|
+
`/vendor` route, no server in the sandbox).
|
|
121
123
|
- **One-command setup** — `rly mcp install --target claude|codex` writes the
|
|
122
124
|
host config; `rly mcp config` prints the snippet for any MCP host. The classic
|
|
123
125
|
browser board is untouched.
|
package/docs/AGENT.md
CHANGED
|
@@ -111,7 +111,8 @@ finalizes when your call completes — so the user sees it build, not a blank wa
|
|
|
111
111
|
Both take **the exact same board spec** documented below (the tool `inputSchema`
|
|
112
112
|
*is* this spec). Call the tool with your spec; the host shows the board, the user
|
|
113
113
|
fills it in, and their answers come back to you (answers, per-question notes,
|
|
114
|
-
comment)
|
|
114
|
+
comment) as a user-message turn, with structured context synced when the host
|
|
115
|
+
supports it — read them just as you would the CLI's result JSON. There is **no
|
|
115
116
|
`--detach`/`rly wait` dance, no stdout parsing, and no timeout** in this mode; the
|
|
116
117
|
board stays live until the user submits and the host delivers the result.
|
|
117
118
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@khanglvm/relay",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.5",
|
|
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/src/mcp-ui/board.js
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
// host app (Claude desktop/mobile, Codex, …) as a sandboxed inline iframe.
|
|
5
5
|
// There is no local HTTP server here: every exchange with the host travels over
|
|
6
6
|
// JSON-RPC on window.postMessage — the spec arrives as the tool result, the
|
|
7
|
-
// user's
|
|
8
|
-
// (Chart.js / Mermaid / Viz.js) are pulled through the host's
|
|
7
|
+
// user's final submission goes back as a `ui/message` user turn, and vendored
|
|
8
|
+
// libraries (Chart.js / Mermaid / Viz.js) are pulled through the host's
|
|
9
|
+
// `resources/read`.
|
|
9
10
|
//
|
|
10
11
|
// Rich blocks are rendered by the SAME window.RelayBlocks as the browser board
|
|
11
12
|
// (markdown, code, diff, table, chart, mermaid, graphviz, image, html), so the
|
|
@@ -957,19 +958,30 @@
|
|
|
957
958
|
annotations: data.annotations,
|
|
958
959
|
};
|
|
959
960
|
const text = summarize(data);
|
|
960
|
-
let
|
|
961
|
+
let messageDelivered = false;
|
|
962
|
+
let contextDelivered = false;
|
|
961
963
|
try {
|
|
964
|
+
// A completed relay form is a user reply, not passive context. Some hosts
|
|
965
|
+
// ACK `ui/update-model-context` without starting a new model turn, so send
|
|
966
|
+
// the transcript as a user message first to wake the agent reliably.
|
|
967
|
+
await request('ui/message', { role: 'user', content: { type: 'text', text } });
|
|
968
|
+
messageDelivered = true;
|
|
969
|
+
} catch {
|
|
970
|
+
// Older/leaner hosts may not expose app-initiated messages.
|
|
971
|
+
}
|
|
972
|
+
try {
|
|
973
|
+
// Keep the structured payload available to hosts that attach app context.
|
|
974
|
+
// This is best-effort because context updates are intentionally silent.
|
|
962
975
|
await request('ui/update-model-context', { content: [{ type: 'text', text }], structuredContent: structured });
|
|
963
|
-
|
|
976
|
+
contextDelivered = true;
|
|
964
977
|
} catch {
|
|
965
|
-
//
|
|
966
|
-
try { await request('ui/message', { role: 'user', content: { type: 'text', text } }); delivered = true; } catch { /* give up gracefully */ }
|
|
978
|
+
// If ui/message worked, the agent still receives the submission transcript.
|
|
967
979
|
}
|
|
968
980
|
submitted = true;
|
|
969
|
-
showDone(
|
|
981
|
+
showDone(messageDelivered, contextDelivered);
|
|
970
982
|
}
|
|
971
983
|
|
|
972
|
-
function showDone(
|
|
984
|
+
function showDone(messageDelivered, contextDelivered) {
|
|
973
985
|
// Collapse: leave fullscreen, drop the whole form for a one-line confirmation
|
|
974
986
|
// so the host shrinks the iframe to a small footprint in the transcript.
|
|
975
987
|
if (Annotate) { try { Annotate.teardown(); } catch { /* nothing to tear down */ } }
|
|
@@ -977,8 +989,10 @@
|
|
|
977
989
|
app.replaceChildren(el('div', { class: 'mcp-done' },
|
|
978
990
|
el('span', { class: 'mark' }, '✓'),
|
|
979
991
|
el('span', { class: 'lead' }, QS.length ? 'Submitted' : 'Acknowledged'),
|
|
980
|
-
el('span', { class: 'sub' },
|
|
992
|
+
el('span', { class: 'sub' }, messageDelivered
|
|
981
993
|
? '· sent back to the agent'
|
|
994
|
+
: contextDelivered
|
|
995
|
+
? '· saved; send the agent a message to continue'
|
|
982
996
|
: '· tell the agent you’ve responded')));
|
|
983
997
|
// Report the small height immediately, then again next frame / after layout
|
|
984
998
|
// settles — beats hosts that only grow on debounced size events.
|
package/src/mcp.js
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
// • the tools `relay_ask` / `relay_show` link to it via _meta.ui.resourceUri
|
|
9
9
|
// • calling a tool returns the (normalized) board spec as structuredContent;
|
|
10
10
|
// the host renders the resource in a sandboxed iframe and forwards the spec
|
|
11
|
-
// • the iframe collects the user's answers and sends them back
|
|
12
|
-
//
|
|
11
|
+
// • the iframe collects the user's answers and sends them back as a user
|
|
12
|
+
// message; it also syncs model context when the host supports it
|
|
13
13
|
//
|
|
14
14
|
// Framing is the MCP stdio transport: newline-delimited JSON-RPC, one message
|
|
15
15
|
// per line, never embedded newlines. stdout carries ONLY protocol messages;
|
|
@@ -156,7 +156,7 @@ function buildResult(method, params, clientProtocol) {
|
|
|
156
156
|
},
|
|
157
157
|
serverInfo: { name: 'relay', version: PKG.version },
|
|
158
158
|
instructions:
|
|
159
|
-
'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.
|
|
159
|
+
'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
160
|
};
|
|
161
161
|
case 'ping':
|
|
162
162
|
return {};
|
|
@@ -198,9 +198,10 @@ function readResource(params) {
|
|
|
198
198
|
|
|
199
199
|
// A tool call: normalize the spec and hand it to the host as structuredContent.
|
|
200
200
|
// The host renders ui://relay/board and forwards this spec to the iframe, which
|
|
201
|
-
// collects the answers and returns them via
|
|
202
|
-
//
|
|
203
|
-
// see and fix
|
|
201
|
+
// collects the answers and returns them via a UI-originated user message (with
|
|
202
|
+
// ui/update-model-context as a best-effort context sync). Spec errors come back
|
|
203
|
+
// as an isError tool result (not a protocol error) so the model can see and fix
|
|
204
|
+
// them.
|
|
204
205
|
function callTool(params) {
|
|
205
206
|
const name = params && params.name;
|
|
206
207
|
if (name !== 'relay_ask' && name !== 'relay_show') {
|
package/src/server.js
CHANGED
|
@@ -694,13 +694,22 @@ export async function runBoard({ id, port = 0, open = true, timeoutSec = 1800, q
|
|
|
694
694
|
});
|
|
695
695
|
|
|
696
696
|
// Bind the requested port (reopen/rescue reuse the board's last port so a
|
|
697
|
-
// still-open tab reconnects).
|
|
698
|
-
//
|
|
697
|
+
// still-open tab reconnects). A board just stopped on that port holds its
|
|
698
|
+
// listening socket through a short close grace, so a stop-then-reopen on the
|
|
699
|
+
// same port can briefly hit EADDRINUSE — retry for ~2s to ride that out before
|
|
700
|
+
// giving up. Only then fall back to a random free port rather than failing.
|
|
699
701
|
await new Promise((resolve, reject) => {
|
|
702
|
+
const RETRY_MS = 200;
|
|
703
|
+
const MAX_RETRIES = 10; // ~2s — covers a prior server's ~600ms close grace
|
|
704
|
+
let retries = 0;
|
|
700
705
|
const bind = (p, allowFallback) => {
|
|
701
706
|
const onErr = (e) => {
|
|
702
707
|
if (allowFallback && e && e.code === 'EADDRINUSE' && p !== 0) {
|
|
703
|
-
|
|
708
|
+
if (retries++ < MAX_RETRIES) {
|
|
709
|
+
setTimeout(() => bind(p, true), RETRY_MS); // port freeing up — retry it
|
|
710
|
+
} else {
|
|
711
|
+
bind(0, false); // still busy after retries → random free port
|
|
712
|
+
}
|
|
704
713
|
} else {
|
|
705
714
|
reject(e);
|
|
706
715
|
}
|