@sphoro/bugcatch-web-sdk 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/src/widget.js CHANGED
@@ -14,7 +14,7 @@ import {
14
14
  import { draftKey, draftOperation, readDraft } from './drafts.js';
15
15
  import { readSession } from './session.js';
16
16
  import { sendMessage } from './updates.js';
17
- import { matchReferences, validateReference } from './references.js';
17
+ import { readReferences, validateReference } from './references.js';
18
18
 
19
19
  const POSITIONS = {
20
20
  'bottom-right': 'bottom:20px;right:20px;',
@@ -503,6 +503,14 @@ textarea { min-height: 88px; resize: vertical; }
503
503
  .refs .err { color: var(--bc-danger); font-weight: 550; }
504
504
  .refs .link { margin-top: 10px; font-size: 12px; color: var(--bc-muted); }
505
505
  .refs .link:hover { color: var(--bc-fg); }
506
+ .refs .chips.cands button { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-weight: 550; }
507
+ /* "We read your ID" — under the description, while they type. */
508
+ .ref-found {
509
+ display: flex; align-items: center; gap: 6px;
510
+ margin: -6px 0 14px; font-size: 12px; line-height: 1.45; color: var(--bc-muted);
511
+ }
512
+ .ref-found svg { width: 14px; height: 14px; flex: none; color: var(--bc-ok); }
513
+ .ref-found b { font-weight: 600; color: var(--bc-fg); }
506
514
 
507
515
  /* ---------- severity ---------- */
508
516
  .seg { display: grid; grid-template-columns: repeat(4, 1fr); gap: 3px; padding: 3px; background: var(--bc-subtle); border: 1px solid var(--bc-border); border-radius: var(--bc-r-md); }
@@ -856,6 +864,10 @@ button.quiet:hover { color: var(--bc-accent); }
856
864
  background: var(--bc-accent-soft); border-color: var(--bc-accent-line);
857
865
  }
858
866
  .assistant.resolved { background: color-mix(in srgb, var(--bc-ok) 9%, var(--bc-bg)); border-color: color-mix(in srgb, var(--bc-ok) 35%, var(--bc-border)); }
867
+ /* On a call: the call's own card is what they are watching. This one states
868
+ what is happening and stops moving — no spinner, no travelling bar. */
869
+ .assistant.calling { background: var(--bc-bg); }
870
+ .assistant.calling .as-progress i { transition: none; }
859
871
  .as-body.entering { animation: bc-rise 360ms var(--bc-ease) both; }
860
872
  .as-top { display: flex; gap: 11px; align-items: center; }
861
873
  .as-orb {
@@ -1049,6 +1061,7 @@ button.quiet:hover { color: var(--bc-accent); }
1049
1061
  animation: bc-rise 240ms var(--bc-ease) both;
1050
1062
  }
1051
1063
  .livecall .who { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1064
+ .livecall .call-kept { font-size: 11px; opacity: .75; }
1052
1065
  .livecall .secs { font-variant-numeric: tabular-nums; color: var(--bc-muted); }
1053
1066
  .livecall .act {
1054
1067
  padding: 5px 11px; border-radius: 999px; border: 1px solid var(--bc-border);
@@ -1750,9 +1763,16 @@ function openPanel({ root, host, dock, client, onClose }) {
1750
1763
  // answer, is a reasonable thing to be asked. Most reporters never see it: the
1751
1764
  // loan number was in the sentence.
1752
1765
  let refConfig = { policy: 'off', fields: [] };
1753
- let refChoice = null; // which field they are answering
1766
+ let refOpen = false;
1767
+ let refChoice = null; // which field they are answering; null = not picked yet
1754
1768
  let refValue = '';
1755
1769
  let refError = '';
1770
+ // What we read in their text but could not place for sure, offered as taps:
1771
+ // [{ value, kinds: [{ key, label }] }]. `refMode` says why the card is open —
1772
+ // 'confirm' when the text had two answers, 'suggest' when it had an unlabelled
1773
+ // one, 'ask' when it had none.
1774
+ let refCandidates = [];
1775
+ let refMode = 'ask';
1756
1776
  // Set once they have answered or explicitly skipped, so Send stops asking.
1757
1777
  let refSettled = false;
1758
1778
  // Set once we have said "that does not look right" about the current value.
@@ -1763,13 +1783,18 @@ function openPanel({ root, host, dock, client, onClose }) {
1763
1783
  let refWarned = false;
1764
1784
 
1765
1785
  client.referenceFields()
1766
- .then((cfg) => { refConfig = cfg; })
1786
+ .then((cfg) => { refConfig = cfg; showFound(); })
1767
1787
  .catch(() => { /* off is the safe answer to every failure */ });
1768
1788
 
1769
- /** What the report already carries, read out of what they typed. */
1770
- function referencesInText() {
1771
- if (refConfig.policy === 'off' || !refConfig.fields.length) return [];
1772
- return matchReferences(refConfig.fields, [titleInput.value, descInput.value]);
1789
+ /** What the text says: found (sure), conflicts (ask which), loose (offer). */
1790
+ function readText() {
1791
+ if (refConfig.policy === 'off' || !refConfig.fields.length) return { found: [], conflicts: [], loose: [] };
1792
+ return readReferences(refConfig.fields, [titleInput.value, descInput.value]);
1793
+ }
1794
+
1795
+ /** Sure enough to send without a question: something found, nothing contested. */
1796
+ function textAnswers(read = readText()) {
1797
+ return read.found.length > 0 && read.conflicts.length === 0;
1773
1798
  }
1774
1799
 
1775
1800
  /** The answer to send, or an empty list. */
@@ -1778,50 +1803,127 @@ function openPanel({ root, host, dock, client, onClose }) {
1778
1803
  return [{ key: refChoice, value: refValue.trim().slice(0, 120) }];
1779
1804
  }
1780
1805
 
1806
+ // A quiet line under the description while they type, so somebody who put
1807
+ // their loan number in the sentence can see it was read and does not go
1808
+ // looking for a box to type it into again.
1809
+ const foundNote = document.createElement('p');
1810
+ foundNote.className = 'ref-found';
1811
+ foundNote.setAttribute('aria-live', 'polite');
1812
+ foundNote.hidden = true;
1813
+ descInput.closest('.field').after(foundNote);
1814
+ let foundTimer;
1815
+ function showFound() {
1816
+ const read = readText();
1817
+ if (textAnswers(read)) {
1818
+ const list = read.found.slice(0, 2).map((r) => `${escapeHtml(r.label)} <b>${escapeHtml(shortId(r.value))}</b>`).join(', ');
1819
+ foundNote.innerHTML = `${ICONS.check}<span>Found your ${list} — no need to add it again.</span>`;
1820
+ foundNote.hidden = false;
1821
+ // They have answered in the text; a card still asking would be not listening.
1822
+ if (refOpen && !answeredReference().length) { refOpen = false; drawRefs(); }
1823
+ } else {
1824
+ foundNote.hidden = true;
1825
+ foundNote.innerHTML = '';
1826
+ }
1827
+ }
1828
+ const queueFound = () => { clearTimeout(foundTimer); foundTimer = setTimeout(showFound, 250); };
1829
+ titleInput.addEventListener('input', queueFound);
1830
+ descInput.addEventListener('input', queueFound);
1831
+
1832
+ /** A UUID is long; the chip shows its ends, the title shows all of it. */
1833
+ function shortId(value) {
1834
+ return value.length > 20 ? `${value.slice(0, 8)}…${value.slice(-4)}` : value;
1835
+ }
1836
+
1837
+ function candidateFor(value) {
1838
+ const v = value.trim().toLowerCase();
1839
+ return refCandidates.find((c) => c.value.toLowerCase() === v) || null;
1840
+ }
1841
+
1842
+ /** Pick a candidate: its value, and its kind when only one kind fits. */
1843
+ function takeCandidate(c) {
1844
+ refValue = c.value;
1845
+ if (c.kinds.length === 1) refChoice = c.kinds[0].key;
1846
+ else if (!c.kinds.some((k) => k.key === refChoice)) refChoice = null;
1847
+ refError = '';
1848
+ refWarned = false;
1849
+ }
1850
+
1781
1851
  function drawRefs() {
1782
- if (!refChoice) { refsBox.hidden = true; refsBox.innerHTML = ''; return; }
1783
- const field = refConfig.fields.find((f) => f.key === refChoice) || refConfig.fields[0];
1852
+ if (!refOpen) { refsBox.hidden = true; refsBox.innerHTML = ''; return; }
1853
+ const field = refConfig.fields.find((f) => f.key === refChoice) || null;
1784
1854
  const optional = refConfig.policy !== 'required';
1855
+ const current = candidateFor(refValue);
1856
+ // Kinds worth offering: the ones the picked value could be, when we know;
1857
+ // every kind otherwise. Never zero — a guess of ours must not lock out the
1858
+ // right answer.
1859
+ const kinds = current && current.kinds.length > 1
1860
+ ? refConfig.fields.filter((f) => current.kinds.some((k) => k.key === f.key))
1861
+ : refConfig.fields;
1862
+ // Two loan numbers: the kind is not in question, only which one — so the
1863
+ // kind chips would be a second question nobody needs to answer.
1864
+ const sharedKind = refCandidates.length > 1
1865
+ && refCandidates.every((c) => c.kinds.length === 1 && c.kinds[0].key === refCandidates[0].kinds[0].key);
1866
+
1867
+ const head = {
1868
+ confirm: refCandidates.length > 1
1869
+ ? ['Which one is this about?', 'We found more than one ID in your message. Pick the one this problem is about.']
1870
+ : ['Is this the right ID?', 'This could be more than one kind of ID. Tell us which it is.'],
1871
+ suggest: ['Is this your ID?', 'We spotted this in your message. Tell us what it is so we can find your record.'],
1872
+ ask: optional
1873
+ ? ['One more thing', 'So we can find your record — this helps us answer faster.']
1874
+ : ['We need one detail', 'We need one of these to look this up.'],
1875
+ }[refMode];
1785
1876
 
1786
1877
  refsBox.hidden = false;
1787
1878
  // A handful of kinds read best as chips to tap; a long list goes back to
1788
1879
  // a select so the card does not turn into a wall of pills.
1789
- const many = refConfig.fields.length > 6;
1880
+ const many = kinds.length > 6;
1790
1881
  refsBox.innerHTML = `
1791
1882
  <div class="ask-head">
1792
1883
  <span class="avatar" aria-hidden="true">${ICONS.sparkle}</span>
1793
1884
  <div>
1794
- <strong>${optional ? 'One more thing' : 'We need one detail'}</strong>
1795
- <p class="why">${escapeHtml(
1796
- optional
1797
- ? 'So we can find your record — this helps us answer faster.'
1798
- : 'We need one of these to look this up.',
1799
- )}</p>
1885
+ <strong>${escapeHtml(head[0])}</strong>
1886
+ <p class="why">${escapeHtml(head[1])}</p>
1800
1887
  </div>
1801
1888
  </div>
1802
- ${refConfig.fields.length > 1 && !many ? `
1889
+ ${refCandidates.length > 1 ? `
1890
+ <div class="chips cands" role="group" aria-label="IDs found in your message">
1891
+ ${refCandidates.map((c, i) => `
1892
+ <button type="button" data-ref-cand="${i}" title="${escapeHtml(c.value)}"
1893
+ aria-pressed="${!!current && current === c}">${escapeHtml(shortId(c.value))}</button>
1894
+ `).join('')}
1895
+ </div>` : ''}
1896
+ ${kinds.length > 1 && !many && !sharedKind ? `
1803
1897
  <div class="chips" role="group" aria-label="Which detail you are giving">
1804
- ${refConfig.fields.map((f) => `
1805
- <button type="button" data-ref-kind="${escapeHtml(f.key)}" aria-pressed="${f.key === field.key}">${escapeHtml(f.label)}</button>
1898
+ ${kinds.map((f) => `
1899
+ <button type="button" data-ref-kind="${escapeHtml(f.key)}" aria-pressed="${!!field && f.key === field.key}">${escapeHtml(f.label)}</button>
1806
1900
  `).join('')}
1807
1901
  </div>` : ''}
1808
1902
  <div class="row">
1809
1903
  ${many ? `
1810
1904
  <select class="ref-kind" aria-label="Which detail you are giving">
1811
- ${refConfig.fields.map((f) => `
1812
- <option value="${escapeHtml(f.key)}" ${f.key === field.key ? 'selected' : ''}>${escapeHtml(f.label)}</option>
1905
+ ${field ? '' : '<option value="" selected disabled>What is it?</option>'}
1906
+ ${kinds.map((f) => `
1907
+ <option value="${escapeHtml(f.key)}" ${field && f.key === field.key ? 'selected' : ''}>${escapeHtml(f.label)}</option>
1813
1908
  `).join('')}
1814
1909
  </select>` : ''}
1815
- <input class="ref-value${refError ? ' invalid' : ''}" type="${field.inputMode === 'email' ? 'email' : 'text'}"
1816
- inputmode="${escapeHtml(field.inputMode === 'numeric' || field.inputMode === 'tel' ? 'numeric' : 'text')}"
1910
+ <input class="ref-value${refError ? ' invalid' : ''}" type="${field?.inputMode === 'email' ? 'email' : 'text'}"
1911
+ inputmode="${escapeHtml(field?.inputMode === 'numeric' || field?.inputMode === 'tel' ? 'numeric' : 'text')}"
1817
1912
  value="${escapeHtml(refValue)}"
1818
- placeholder="${escapeHtml(field.placeholder || field.label)}"
1819
- aria-label="${escapeHtml(field.label)}" ${refError ? 'aria-invalid="true"' : ''} />
1913
+ placeholder="${escapeHtml(field ? (field.placeholder || field.label) : 'Your ID')}"
1914
+ aria-label="${escapeHtml(field ? field.label : 'Your ID')}" ${refError ? 'aria-invalid="true"' : ''} />
1820
1915
  </div>
1821
- ${field.hint ? `<p class="hint">${escapeHtml(field.hint)}</p>` : ''}
1916
+ ${field?.hint ? `<p class="hint">${escapeHtml(field.hint)}</p>` : ''}
1822
1917
  ${refError ? `<p class="field-err" role="alert">${ICONS.alert}<span>${escapeHtml(refError)}</span></p>` : ''}
1823
- ${optional ? '<button type="button" class="link" data-act="ref-skip">I don\'t have this — send without it</button>' : ''}`;
1918
+ ${optional ? `<button type="button" class="link" data-act="ref-skip">${
1919
+ refMode === 'ask' ? 'I don\'t have this — send without it' : 'Not sure — send as it is'}</button>` : ''}`;
1824
1920
 
1921
+ refsBox.querySelectorAll('[data-ref-cand]').forEach((chip) => {
1922
+ chip.addEventListener('click', () => {
1923
+ takeCandidate(refCandidates[Number(chip.dataset.refCand)]);
1924
+ drawRefs();
1925
+ });
1926
+ });
1825
1927
  refsBox.querySelectorAll('[data-ref-kind]').forEach((chip) => {
1826
1928
  chip.addEventListener('click', () => {
1827
1929
  refChoice = chip.dataset.refKind;
@@ -1851,7 +1953,9 @@ function openPanel({ root, host, dock, client, onClose }) {
1851
1953
  refsBox.querySelector('[data-act="ref-skip"]')?.addEventListener('click', () => {
1852
1954
  // Skipping is answering. Without this the panel asks again on the next
1853
1955
  // press and the reporter is in a loop with a button that does nothing.
1956
+ // The text still goes; the server reads it either way.
1854
1957
  refSettled = true;
1958
+ refOpen = false;
1855
1959
  refChoice = null;
1856
1960
  refValue = '';
1857
1961
  refError = '';
@@ -1860,23 +1964,54 @@ function openPanel({ root, host, dock, client, onClose }) {
1860
1964
  say('');
1861
1965
  });
1862
1966
 
1863
- value.focus();
1967
+ // Focus where the next decision is: the kind, when that is all that is
1968
+ // missing; the box otherwise.
1969
+ const next = !field && refValue ? refsBox.querySelector('[data-ref-kind], .ref-kind') : null;
1970
+ (next || value).focus();
1971
+ }
1972
+
1973
+ /**
1974
+ * Open the question, pre-filled with the best of what the text offered.
1975
+ *
1976
+ * Conflicts first — the text said two things and only the reporter knows
1977
+ * which — then loose IDs, which are probably the answer and only need a
1978
+ * name. Nothing, and it is the plain question it always was.
1979
+ */
1980
+ function openRefs(read) {
1981
+ refCandidates = read.conflicts.length ? read.conflicts : read.loose.slice(0, 4);
1982
+ refMode = read.conflicts.length ? 'confirm' : read.loose.length ? 'suggest' : 'ask';
1983
+ refOpen = true;
1984
+ if (refCandidates.length) {
1985
+ // One contested value: pre-fill it so they only choose what it is. Two
1986
+ // or more: pick nothing, because picking is the question.
1987
+ if (refCandidates.length === 1) takeCandidate(refCandidates[0]);
1988
+ else {
1989
+ refValue = '';
1990
+ const [first] = refCandidates;
1991
+ const same = refCandidates.every((c) => c.kinds.length === 1 && c.kinds[0].key === first.kinds[0].key);
1992
+ refChoice = same ? first.kinds[0].key : null;
1993
+ }
1994
+ } else {
1995
+ refChoice = refChoice || refConfig.fields[0].key;
1996
+ }
1997
+ drawRefs();
1864
1998
  }
1865
1999
 
1866
2000
  /**
1867
2001
  * Whether Send should stop and ask.
1868
2002
  *
1869
- * True at most once per report. The question is skippable on an `optional`
1870
- * project and not on a `required` one, and on both the answer to "have we
1871
- * already got one" is read from the text every time — somebody who types the
1872
- * loan number into the description after being asked has answered the
1873
- * question, and being asked again would be the widget not listening.
2003
+ * Never when the text answered it outright. Once when the text said two
2004
+ * things or something unlabelled — on every policy, because sending the
2005
+ * wrong record's id with confidence is worse than one extra tap. And the
2006
+ * plain question on `optional`/`required` projects as before.
1874
2007
  */
1875
2008
  function needsReference() {
1876
2009
  if (refConfig.policy === 'off' || !refConfig.fields.length) return false;
1877
- if (referencesInText().length) return false;
1878
2010
  if (answeredReference().length) return false;
1879
- return !refSettled || refConfig.policy === 'required';
2011
+ const read = readText();
2012
+ if (textAnswers(read)) return false;
2013
+ if (refSettled && refConfig.policy !== 'required') return false;
2014
+ return read;
1880
2015
  }
1881
2016
 
1882
2017
  /* ---------- send ---------- */
@@ -1894,9 +2029,18 @@ function openPanel({ root, host, dock, client, onClose }) {
1894
2029
  // costs one extra press on projects that want an identifier, and it is the
1895
2030
  // only shape that does not put paperwork in front of a person who came here
1896
2031
  // to say something is broken.
1897
- if (needsReference()) {
1898
- refChoice = refChoice || refConfig.fields[0].key;
2032
+ //
2033
+ // First, though: a value in the box but no kind picked. Asked rather than
2034
+ // guessed — a wrong kind joins on the wrong table.
2035
+ if (refOpen && refValue.trim() && !refChoice) {
2036
+ refError = 'Pick what this ID is.';
1899
2037
  drawRefs();
2038
+ return;
2039
+ }
2040
+
2041
+ const read = needsReference();
2042
+ if (read) {
2043
+ if (!refOpen) openRefs(read); else drawRefs();
1900
2044
  say('');
1901
2045
  return;
1902
2046
  }
@@ -2639,7 +2783,8 @@ function openPanel({ root, host, dock, client, onClose }) {
2639
2783
  <div class="as-body${previous && previous !== a.phase ? ' entering' : ''}">
2640
2784
  <div class="as-top">
2641
2785
  <span class="as-orb" aria-hidden="true">${a.phase === 'analyzing' ? '<span class="as-spin"></span>'
2642
- : a.phase === 'resolved' ? ICONS.check : ICONS.sparkle}</span>
2786
+ : a.phase === 'calling' ? ICONS.phone
2787
+ : a.phase === 'resolved' ? ICONS.check : ICONS.sparkle}</span>
2643
2788
  <div class="as-text">
2644
2789
  <div class="as-title">${escapeHtml(a.title)}</div>
2645
2790
  <div class="as-detail">${escapeHtml(a.detail)}</div>
@@ -2664,6 +2809,9 @@ function openPanel({ root, host, dock, client, onClose }) {
2664
2809
  }
2665
2810
 
2666
2811
  // While it works, a "thinking" bubble sits where its next message will land.
2812
+ // Never during a call: somebody is on the phone about this report, nothing
2813
+ // is being analysed (the analysis waits for the transcript), and a bubble
2814
+ // typing away underneath a live call is the assistant talking over them.
2667
2815
  if (thread) {
2668
2816
  const pinned = atLatest(thread);
2669
2817
  const typing = thread.querySelector('.typing-row');
@@ -2677,6 +2825,10 @@ function openPanel({ root, host, dock, client, onClose }) {
2677
2825
 
2678
2826
  if (a.phase === 'analyzing') {
2679
2827
  assistantTimer = setTimeout(() => refreshThread(bugId), 3000);
2828
+ } else if (a.phase === 'calling') {
2829
+ // Slower: the call's own card is what the reporter is watching, and this
2830
+ // only has to notice when the call ends and the analysis begins.
2831
+ assistantTimer = setTimeout(() => refreshThread(bugId), 10000);
2680
2832
  } else if (a.phase === 'needs_info' && previous !== 'needs_info') {
2681
2833
  body.querySelector('.composer textarea')?.focus();
2682
2834
  }
@@ -2753,8 +2905,9 @@ function openPanel({ root, host, dock, client, onClose }) {
2753
2905
  <div class="staged" hidden></div>
2754
2906
  <div class="row">
2755
2907
  <div class="tools">
2908
+ ${cfg.attachments === false ? '' : `
2756
2909
  <button class="tool" type="button" data-act="attach" title="Attach a file" aria-label="Attach a file">${ICONS.paperclip}</button>
2757
- <button class="tool" type="button" data-act="voice" title="Record a voice note" aria-label="Record a voice note">${ICONS.mic}</button>
2910
+ <button class="tool" type="button" data-act="voice" title="Record a voice note" aria-label="Record a voice note">${ICONS.mic}</button>`}
2758
2911
  <button class="tool" type="button" data-act="livecall" title="Call the team" aria-label="Call the team" hidden>${ICONS.phone}</button>
2759
2912
  </div>
2760
2913
  <textarea rows="1" placeholder="Write a reply…" aria-label="Write a reply"></textarea>
@@ -2834,7 +2987,11 @@ function openPanel({ root, host, dock, client, onClose }) {
2834
2987
  }
2835
2988
  });
2836
2989
 
2837
- composer.querySelector('[data-act="attach"]').addEventListener('click', () => picker.click());
2990
+ // Absent when the host set `attachments: false` — which the report form
2991
+ // has always honoured and this composer did not, so a host keeping files
2992
+ // and microphone audio outside its compliance perimeter still had both
2993
+ // offered in the follow-up thread.
2994
+ composer.querySelector('[data-act="attach"]')?.addEventListener('click', () => picker.click());
2838
2995
  picker.addEventListener('change', () => {
2839
2996
  say('');
2840
2997
  for (const file of picker.files) {
@@ -2847,12 +3004,12 @@ function openPanel({ root, host, dock, client, onClose }) {
2847
3004
  picker.value = '';
2848
3005
  });
2849
3006
 
2850
- if (!voiceSupported()) {
3007
+ if (voiceBtn && !voiceSupported()) {
2851
3008
  voiceBtn.disabled = true;
2852
3009
  voiceBtn.title = 'This browser cannot record audio';
2853
3010
  }
2854
3011
 
2855
- voiceBtn.addEventListener('click', async () => {
3012
+ voiceBtn?.addEventListener('click', async () => {
2856
3013
  if (recorder) {
2857
3014
  const handle = recorder;
2858
3015
  recorder = null;
@@ -2971,6 +3128,7 @@ function openPanel({ root, host, dock, client, onClose }) {
2971
3128
  <span class="call-dot" aria-hidden="true"></span>
2972
3129
  <span class="who" role="status" aria-live="polite">Calling the team…</span>
2973
3130
  <span class="secs" hidden></span>
3131
+ ${handle?.recorded ? '<span class="call-kept">This call is recorded.</span>' : ''}
2974
3132
  <button class="act" type="button" data-act="mute" hidden>Mute</button>
2975
3133
  <button class="act danger" type="button" data-act="hangup">Cancel</button>`;
2976
3134
  body.insertBefore(bar, thread);
@@ -3167,13 +3325,14 @@ function messageHtml(message, state = '') {
3167
3325
  }
3168
3326
  if (state.includes('cont')) who = '';
3169
3327
 
3170
- // "Is this the problem you are facing?" — the confirm question the
3171
- // investigation asks (bugcatch-workers check-loop). One tap answers it; the
3172
- // buttons only show on the latest message, so an answered question has none.
3173
- const confirm = automated && /reply "yes" or "no"/i.test(body)
3328
+ // The assistant's one answer ends by inviting a correction (check-rules
3329
+ // confirmMessage). One tap sends it, and what comes back goes to the person
3330
+ // who now owns the report rather than to another automated round — so the
3331
+ // wording is "that's right" and "that's not it", not a yes/no question.
3332
+ const confirm = automated && /it goes to the same person/i.test(body)
3174
3333
  ? `<div class="quick" role="group" aria-label="Answer">
3175
- <button type="button" data-quick="Yes, this is the problem">${ICONS.check}Yes, that's it</button>
3176
- <button type="button" data-quick="No, that is not the problem">No, it's something else</button>
3334
+ <button type="button" data-quick="Yes, this is the problem">${ICONS.check}That's right</button>
3335
+ <button type="button" data-quick="No, that is not the problem">That's not it</button>
3177
3336
  </div>`
3178
3337
  : '';
3179
3338