@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.
@@ -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
  }