@khanglvm/relay 0.14.2 → 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 +18 -3
- package/docs/AGENT.md +66 -23
- package/package.json +1 -1
- package/skills/relay/SKILL.md +48 -12
- package/src/cli.js +42 -21
- package/src/mcp-ui/board.js +46 -8
- package/src/mcp.js +9 -3
- package/src/server.js +310 -95
- package/src/spec.js +5 -0
- package/src/store.js +15 -0
- package/src/ui/annotate.js +73 -26
- package/src/ui/app.js +100 -36
- package/src/ui/blocks.css +63 -11
- package/src/ui/blocks.js +353 -48
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
|
}
|