eleven / messenger Public

web: quote machine output on notices; keep emphasis markers out of hrefs (#530)

Two polish items from live chip use:

- A failed op's notice read as raw provider JSON dumped into the bubble.
  Notices gain a `quote` field: prose stays prose ("That didn't work.") and
  the raw error renders beneath it as a set-off quotation — rule, muted,
  wrapping; deliberately NOT monospaced (it's a quote, not code). Plain
  textContent, capped, no links.

- The URL tokenizer swallowed trailing emphasis markers into hrefs, so a
  bot's "**github.com/apps/x**" produced a link ending in **. Emphasis
  markers join the trailing-punctuation strip. Honest limit, in a comment and
  the test: the stars now render as literal text around a clean link — making
  them BOLD across a link is beyond this hand-rolled pass (the real Markdown
  parser question, tracked separately).
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell 568066d456d0a38d312e7d9bc85f4cf74b829a68 parent 4f17aac
5 files changed, +49 −6
  • test/format.test.mjs +16 −0
  • web/static/apps.js +10 −1
  • web/static/format.js +7 −2
  • web/static/shell.js +7 −3
  • web/static/style.css +9 −0
diff --git a/test/format.test.mjs b/test/format.test.mjs
index 83301c6..1d04d9e 100644
--- a/test/format.test.mjs
+++ b/test/format.test.mjs
@@ -440,5 +440,21 @@ await test("changelogParagraphs handles no marker, blank, and non-string", () =>
assert.deepEqual(changelogParagraphs(undefined), []);
});
+await test("emphasis markers around a URL never ride into the href", () => {
+ // A bot writing "**github.com/apps/eleven-messenger**" gets a clean link,
+ // stars left as literal text (bolding ACROSS a link needs the real Markdown
+ // parser, tracked separately).
+ const toks = tokenizeText("go to **github.com/apps/eleven-messenger** and install");
+ const link = toks.find((t) => t.href);
+ assert.ok(link, "URL still linkifies next to emphasis markers");
+ assert.equal(link.text, "github.com/apps/eleven-messenger");
+ assert.ok(!link.href.includes("*"), "no emphasis markers in the href");
+ assert.equal(toks.map((t) => t.text).join(""), "go to **github.com/apps/eleven-messenger** and install");
+ const toks2 = tokenizeText("see https://docs.github.com/rest/issues#list**");
+ const link2 = toks2.find((t) => t.href);
+ assert.equal(link2.text, "https://docs.github.com/rest/issues#list");
+ assert.ok(!link2.href.endsWith("*"));
+});
+
console.log(failures === 0 ? "\nALL PASSED" : `\n${failures} FAILED`);
process.exit(failures === 0 ? 0 : 1);
diff --git a/web/static/apps.js b/web/static/apps.js
index 7a9b1e8..02dd14d 100644
--- a/web/static/apps.js
+++ b/web/static/apps.js
@@ -1466,13 +1466,22 @@ function fillFullNotice(row, spec = {}, opts = {}) {
// A chips-only notice (a persisted bubble's live actions) has no bubble of
// its own — the text it belongs to is already in the feed as real history.
- if (text || spec.img) {
+ if (text || spec.img || spec.quote) {
const bubble = document.createElement("div");
bubble.className = "local-notice-bubble";
const body = document.createElement("div");
body.className = "local-notice-body";
renderNoticeBody(text || "", body);
bubble.appendChild(body);
+ // Quoted machine output (a provider's error line): set off visually as a
+ // quotation under the prose — never dumped into the body where it reads as
+ // the notice's own words. Plain textContent: it's evidence, not content.
+ if (spec.quote) {
+ const q = document.createElement("div");
+ q.className = "local-notice-quote";
+ q.textContent = String(spec.quote).slice(0, 600);
+ bubble.appendChild(q);
+ }
// An optional preview image (spec.img). Only ever an IN-CODE same-origin URL
// (e.g. the setup conversation previewing /api/server-avatar) — never user text.
if (spec.img) {
diff --git a/web/static/format.js b/web/static/format.js
index df2a8d2..0ff6dcd 100644
--- a/web/static/format.js
+++ b/web/static/format.js
@@ -6,8 +6,13 @@
// and abbreviations (node.js, index.html, e.g., main.go) aren't turned into links.
const URL_RE = /https?:\/\/[^\s<]+|www\.[^\s<]+|[a-z0-9][a-z0-9-]*(?:\.[a-z0-9-]+)+(?:\/[^\s<]*)?/gi;
-// Trailing punctuation that's almost certainly sentence punctuation, not URL.
-const TRAILING = /[.,!?;:'")\]}]+$/;
+// Trailing punctuation that's almost certainly sentence punctuation, not URL —
+// including emphasis markers: a bot writing "**github.com/apps/x**" must not
+// get the closing ** swallowed into the href. (The stars still render as
+// literal text rather than bolding across the link — emphasis can't span a
+// link token in this hand-rolled pass; the honest fix is the real Markdown
+// parser, tracked separately.)
+const TRAILING = /[.,!?;:'")\]}*_~`]+$/;
const SCHEME_RE = /^https?:\/\//i;
const WWW_RE = /^www\./i;
diff --git a/web/static/shell.js b/web/static/shell.js
index 4e45888..fe12ca8 100644
--- a/web/static/shell.js
+++ b/web/static/shell.js
@@ -239,9 +239,13 @@ const CHIP_RUNNERS = {
service_op: (a) => {
const threadId = activeId;
requestOp(a.service, a.op, a.args, a.access, 120000, "chip").then((res) =>
- // On failure the raw error lands here instantly; the bot follows up
- // in-thread with a plain-language explanation (the src:"chip" tag).
- appendNotice(threadId, { text: res.ok ? summarizeOpResult(a, res) : `Couldn't run that: ${res.error}` })
+ // On failure: a plain-words line with the raw error QUOTED under it
+ // (never dumped into the prose), and the bot follows up in-thread with
+ // an explanation (the src:"chip" tag).
+ appendNotice(
+ threadId,
+ res.ok ? { text: summarizeOpResult(a, res) } : { text: "That didn't work.", quote: res.error }
+ )
);
return a.access === "write" ? "Confirmed ✓" : "Requested ✓";
},
diff --git a/web/static/style.css b/web/static/style.css
index 631e988..cf0dcf5 100644
--- a/web/static/style.css
+++ b/web/static/style.css
@@ -977,6 +977,15 @@ input:focus, textarea:focus { outline: 2px solid var(--accent); border-color: tr
border-radius: 5px;
}
.local-notice-body a { color: var(--accent); }
+/* Quoted machine output on a notice (a provider's error): a quotation, set off
+ with a rule and muted — readable prose framing above, evidence below. Not
+ monospaced: it's a quote, not code. */
+.local-notice-quote {
+ margin-top: 8px; padding: 6px 10px;
+ border-left: 3px solid color-mix(in srgb, var(--muted) 45%, transparent);
+ font-size: 13px; color: var(--muted);
+ white-space: pre-wrap; overflow-wrap: anywhere;
+}
/* Optional preview image on a notice (in-code same-origin URLs only — e.g. the
setup conversation previewing the just-uploaded server photo). Fixed box so
the feed's scroll position is right BEFORE the image loads. */