From 390fd7f573a720faf3eec15f0aa0bef481276bf8 Mon Sep 17 00:00:00 2001 From: Implementer Date: Wed, 14 Oct 2026 05:43:00 +0200 Subject: [PATCH 1/3] A link's question waits right after the prompt line A question a link carries is a question about to be asked, and the place a question is asked is the prompt; the card stood under the notice instead, above what the panel offers to try. It now stands right after the intro's prompt line, where a typed question would stand, so the hello, the notice and the menus read in the order they always do and the question is the last thing in the log, as it already is in a tab that holds a conversation. The menus are read from the model file after the card is drawn and stand above it, so the log goes to its end once they are in; without that they pushed the question out of sight. The map's sentence follows. chat.js is a synced file, so every site's committed copy is stale: a minor. Verified: npm test (1074 pass), conventions-check and conventions-format pass; the card was seen in Chromium after the prompt line and in view, at desk and phone width, on a local preview of companygraph.io's homepage carrying this chat.js. Process: Delivery Phase: Implement Track: Code Co-Authored-By: Claude Opus 5.5 --- assets/chat.js | 16 ++++++++++------ docs/design-system/assets.md | 2 +- test/chat-ask.test.mjs | 32 ++++++++++++++++++++++++++------ 3 files changed, 37 insertions(+), 13 deletions(-) diff --git a/assets/chat.js b/assets/chat.js index 650e6b7..70ae08a 100644 --- a/assets/chat.js +++ b/assets/chat.js @@ -1872,9 +1872,10 @@ var introPick = null; // The question a link carries, waiting: a card the visitor acts on, built from the strings and // never from markup, so whatever the link says is shown as text. Send hands it to send() as if - // typed, Edit puts it in the input to change, and Discard drops it. Under the notice while the - // intro stands, since that is what the visitor reads first; at the end of a conversation the - // tab already holds, since the intro is then out of sight above it. + // typed, Edit puts it in the input to change, and Discard drops it. It stands right after the + // intro's prompt line, where a question the visitor typed would stand, so the hello, the notice + // and what the panel offers read in the order they always do and the question is the last thing + // in the log; at the end of a conversation the tab already holds, for the same reason. function askCard(){ var s = strings(langNow()).ask, card = el("div", "rbchat-ask"), row = el("div", "rbchat-ask-row"); card.setAttribute("role", "group"); card.setAttribute("aria-label", s.label); @@ -1892,9 +1893,10 @@ askEl = card; return card; } + // Wherever the card stands it is the last thing in the log, so the log's end is where it shows. function showAsk(){ - if (!linkAsk || (askEl && askEl.isConnected)) return; - log.appendChild(askCard()); + if (!linkAsk) return; + if (!(askEl && askEl.isConnected)) log.appendChild(askCard()); log.scrollTop = log.scrollHeight; } @@ -1919,11 +1921,11 @@ hello.appendChild(el("span", "rbchat-h2", lines[1])); introEl.appendChild(hello); notice = el("p", "rbchat-notice"); introEl.appendChild(notice); writeNotice(); - if (linkAsk && !messages.length) introEl.appendChild(askCard()); var groups = el("div", "rbchat-groups"); introEl.appendChild(groups); var promptLine = el("p", "rbchat-prompt-line", "› " + s.prompt + " "); // The block cursor waits here while the intro plays, and hands over to the command line. promptLine.appendChild(el("span", "rbchat-cur")); introEl.appendChild(promptLine); + if (linkAsk && !messages.length) introEl.appendChild(askCard()); log.insertBefore(introEl, log.firstChild); if (!play || still()) finishIntro(); facts(function(f){ @@ -1956,6 +1958,8 @@ // A restored conversation has moved past the intro: its menus are spent, and the rows a // number picks stay those of the menu after the last answer. if (messages.length) spendIntro(); else { menuRows = rows.concat(picked).map(function(r){ return r[0]; }); keysLine(); } + // The menus are drawn above a waiting question and would push it out of sight. + if (askEl && askEl.parentNode === introEl) log.scrollTop = log.scrollHeight; if (!introEl.classList.contains("rbchat-still")) playIntro(); }); }); diff --git a/docs/design-system/assets.md b/docs/design-system/assets.md index 4d8e232..8c6b635 100644 --- a/docs/design-system/assets.md +++ b/docs/design-system/assets.md @@ -75,7 +75,7 @@ A scrollbar inside the panel is the terminal's too: `--t-dim` on no track, set o A fresh conversation opens on the intro, and its notice is the one line there that is not a menu: it carries a rail in `--t-part` ([Tokens](tokens.md#token-families)) and opens, in the panel's ink, with the sentence that an AI answers and not a person, which the EU AI Act asks a chat to say before the first message; where the message goes and the privacy page follow in the dim of a comment. The sentences are the widget's own in both languages, in `chat.js`. -An address can ask for the chat, so a post can send its reader to the panel or to one answer: `?chat=open` opens the panel, and `?ask=` opens it with a question waiting, with `?chat=open` or without it. The page reads either once and takes it out of the address, so a reload or a shared address does not ask again. Nothing is sent on arrival, as the intro's notice says: the question waits in `.rbchat-ask`, a card under that notice and above what the panel offers to try, or at the end of the log where the tab already holds a conversation, and Send hands it to the chat as if typed, Edit puts it in the input and Discard drops it. On a desk Send takes the focus, so Enter sends; on a touch screen nothing is focused, so no keyboard rises over the page. The question is trimmed and set as text, never as markup, and one that is empty, too long for a message or not decodable opens the panel without a card. +An address can ask for the chat, so a post can send its reader to the panel or to one answer: `?chat=open` opens the panel, and `?ask=` opens it with a question waiting, with `?chat=open` or without it. The page reads either once and takes it out of the address, so a reload or a shared address does not ask again. Nothing is sent on arrival, as the intro's notice says: the question waits in `.rbchat-ask`, a card right after the intro's prompt line, where a typed question would stand, or at the end of the log where the tab already holds a conversation, and Send hands it to the chat as if typed, Edit puts it in the input and Discard drops it. On a desk Send takes the focus, so Enter sends; on a touch screen nothing is focused, so no keyboard rises over the page. The question is trimmed and set as text, never as markup, and one that is empty, too long for a message or not decodable opens the panel without a card. Where the chat service checks its answers, a claim the evidence does not carry is marked once the answer has finished, and only where its probability passes the threshold the service sends: a dotted underline, `--t-part` for a claim carried in part and `--t-bad` for the rest. Hover or keyboard focus shows its note in the panel's own tooltip box, the one under the header's buttons, with a name and one line; a tap does not, and on a phone the line under the answer says how many statements are not fully backed. With no threshold nothing is marked. diff --git a/test/chat-ask.test.mjs b/test/chat-ask.test.mjs index a314b04..b2aa074 100644 --- a/test/chat-ask.test.mjs +++ b/test/chat-ask.test.mjs @@ -1,5 +1,5 @@ // A link that asks the chat a question, in Chromium: a page carrying chat.js is loaded with -// ?ask=, the panel opens with the question waiting under the notice, and nothing reaches the +// ?ask=, the panel opens with the question waiting after the prompt line, and nothing reaches the // chat endpoint until the visitor sends it. The pure part, linkQuestion(), is in chat.test.mjs. import { test, before, after } from "node:test"; import assert from "node:assert/strict"; @@ -15,6 +15,12 @@ const asset = (f) => fs.readFileSync(path.join(PKG, "assets", f)); const PAGE = `

A page.

`; +// A page whose model fills both of the intro's menus, as a site's does. +const MODEL = { commit: "ffb11a52dc8a5ff2a46cbbd43ab8be8930797f6f", repo: "companygraph/mental-model", entities: [ + { id: "processes/answering", type: "process", name: "Answering" }, + ...["A", "B", "C"].map((n) => ({ id: "kpis/" + n, type: "kpi", name: n })), + ...["What is an owner?", "How does the chat answer a question?", "Which rules does every instance pass?"].map((n, i) => ({ id: "questions/" + i, type: "question", name: n })) +], edges: [] }; // What each POST to the chat carried, so a test can say nothing was sent, or exactly what was. const posted = []; @@ -33,7 +39,7 @@ before(async () => { }); return; } - const files = { "/": ["text/html", PAGE], "/chat.js": ["text/javascript", asset("chat.js")], "/chat.css": ["text/css", asset("chat.css")] }; + const files = { "/": ["text/html", PAGE], "/full": ["text/html", PAGE.replace(" defer>", ' data-questions="/model.json" defer>')], "/model.json": ["application/json", JSON.stringify(MODEL)], "/chat.js": ["text/javascript", asset("chat.js")], "/chat.css": ["text/css", asset("chat.css")] }; const hit = files[url]; if (!hit) { res.writeHead(404); res.end(); return; } res.writeHead(200, { "content-type": hit[0] }); res.end(hit[1]); @@ -45,7 +51,7 @@ before(async () => { after(async () => { await browser.close(); await new Promise((r) => server.close(r)); }); const Q = "show me the organization of CompanyGraph"; -const link = (q, before = "") => `/${before ? before + "&" : "?"}ask=${encodeURIComponent(q)}`; +const link = (q, before = "", at = "/") => `${at}${before ? before + "&" : "?"}ask=${encodeURIComponent(q)}`; async function arrive(address, options = {}) { posted.length = 0; @@ -63,18 +69,18 @@ const card = (page) => page.evaluate(() => { open: !!panel && !panel.hidden, shown: !!c, question: c ? c.querySelector(".rbchat-ask-q").textContent : null, - order: kids.filter((k) => ["rbchat-notice", "rbchat-ask", "rbchat-groups"].includes(k)), + order: kids.filter((k) => ["rbchat-notice", "rbchat-ask", "rbchat-groups", "rbchat-prompt-line"].includes(k)), focused: document.activeElement && document.activeElement.className, address: location.pathname + location.search + location.hash, }; }); -test("a link with ?ask= opens the panel with the question waiting under the notice, sends nothing, and drops it from the address", async () => { +test("a link with ?ask= opens the panel with the question waiting after the prompt line, sends nothing, and drops it from the address", async () => { const { page, context } = await arrive(link(Q)); await page.waitForSelector(".rbchat-ask"); const s = await card(page); assert.deepEqual([s.open, s.shown, s.question], [true, true, Q]); - assert.deepEqual(s.order, ["rbchat-notice", "rbchat-ask", "rbchat-groups"], "under the notice that says nothing is sent, above what the panel offers"); + assert.deepEqual(s.order, ["rbchat-notice", "rbchat-groups", "rbchat-prompt-line", "rbchat-ask"], "after the prompt line, where a typed question would stand"); assert.equal(s.address, "/"); assert.match(s.focused, /rbchat-ask-go/, "on a desk Send has the focus, so Enter sends"); await page.waitForTimeout(300); @@ -85,6 +91,20 @@ test("a link with ?ask= opens the panel with the question waiting under the noti await context.close(); }); +test("the question stands after the prompt line once the menus are drawn above it, and in view", async () => { + const { page, context } = await arrive(link(Q, "", "/full")); + await page.waitForSelector(".rbchat-intro.rbchat-still .rbchat-row"); + await page.waitForSelector(".rbchat-ask"); + const s = await page.evaluate(() => { + const intro = document.querySelector(".rbchat-intro"), c = document.querySelector(".rbchat-ask").getBoundingClientRect(), l = document.querySelector(".rbchat-log").getBoundingClientRect(); + return { rows: intro.querySelectorAll(".rbchat-row").length, last: intro.lastElementChild.className.split(" ")[0], before: intro.lastElementChild.previousElementSibling.className.split(" ")[0], inView: c.top >= l.top - 1 && c.bottom <= l.bottom + 1 }; + }); + assert.ok(s.rows >= 4, `both menus drawn: ${s.rows}`); + assert.deepEqual([s.before, s.last], ["rbchat-prompt-line", "rbchat-ask"]); + assert.equal(s.inView, true, "the menus drawn above the card pushed it out of sight"); + await context.close(); +}); + test("the question keeps the address's other parameters, with or without ?chat=open", async () => { for (const [address, rest] of [[link(Q, "?lang=de") + "#x", "/?lang=de#x"], [`/?chat=open&ask=${encodeURIComponent(Q)}&theme=dark`, "/?theme=dark"]]) { const { page, context } = await arrive(address); From 0ce4c5c4f4f14c25d70abd9e3e80af85c9611ea5 Mon Sep 17 00:00:00 2001 From: Implementer Date: Wed, 14 Oct 2026 05:44:13 +0200 Subject: [PATCH 2/3] The chat panel's size is kept for the browser, not the tab A link opened in a new tab found the panel at the stylesheet's size: the size a visitor gave it was kept in the tab's storage, and a new tab starts with none. It is kept in localStorage now, as lang and theme are, under the same key, chat-size, and with no migration, so a size given before this is given once more. The conversation stays the tab's. The theme-boot block reads the size for the first paint's frame from the same place and goes to v6. Each of the three models holds chat-size as a stored item with the mechanism session-storage and each privacy page is drawn from it, so that entry is owed its change in every model before a site takes this; no page check would report the difference, because none resizes the panel. chat.js and the block are synced, so every site's committed copy is stale: a minor. Verified: npm test (1075 pass), conventions-check and conventions-format pass; on a local preview of companygraph.io a panel sized in one tab opened at that size in a second tab on another page, with chat-size in localStorage and none in sessionStorage. Process: Delivery Phase: Implement Track: Code Co-Authored-By: Claude Opus 5.5 --- assets/chat.js | 17 +++++++++-------- blocks/theme-boot.js | 9 +++++---- test/chat-terminal.test.mjs | 22 ++++++++++++++++++++++ test/first-paint.test.mjs | 5 +++-- versions.json | 2 +- 5 files changed, 40 insertions(+), 15 deletions(-) diff --git a/assets/chat.js b/assets/chat.js index 70ae08a..c50c6a2 100644 --- a/assets/chat.js +++ b/assets/chat.js @@ -714,10 +714,11 @@ // rendered, each answer with the entities it cited. Every access is wrapped, because a // browser with site data blocked throws on the first read and the chat still has to work. var STORE_KEY = "chat"; - // How big the panel is, kept beside the conversation and for the same lifetime: a size is a - // choice about this tab's reading, not a preference to carry to the next visit, and the - // conversation it frames goes when the tab goes. Its own key, because it outlives any one - // conversation: a visitor who sizes the panel, empties it and asks again keeps the size. + // How big the panel is, kept in `localStorage` as `lang` and `theme` are: a size is a choice + // about how this visitor reads the chat on this screen, so it holds in every tab and on the + // next visit, where the conversation it frames goes when its tab goes. Its own key, because it + // is no part of any conversation: a link opened in a new tab finds the panel at the size the + // visitor gave it, with nothing else carried over. var SIZE_KEY = "chat-size"; var MIN_W = 320, MIN_H = 260, STEP = 24; function stored(){ @@ -725,10 +726,10 @@ catch (e) { return null; } } function storedSize(){ - try { var raw = sessionStorage.getItem(SIZE_KEY); var v = raw ? JSON.parse(raw) : null; return v && v.w && v.h ? v : null; } + try { var raw = localStorage.getItem(SIZE_KEY); var v = raw ? JSON.parse(raw) : null; return v && v.w && v.h ? v : null; } catch (e) { return null; } } - function keepSize(w, h){ try { sessionStorage.setItem(SIZE_KEY, JSON.stringify({ w: w, h: h })); } catch (e) {} } + function keepSize(w, h){ try { localStorage.setItem(SIZE_KEY, JSON.stringify({ w: w, h: h })); } catch (e) {} } // Where the visitor was reading, kept with the conversation so the back button returns them // to it and not to the end. A place is a turn and a distance into its bubble, not a pixel @@ -2115,7 +2116,7 @@ // something to clear: an empty panel shows no control for emptying it. A conversation has no // length at which it must start over, so this is the only door; its reset takes the turns, // the log and the tab's copy together; the panel's size stays, being a choice - // about this tab's reading rather than part of the conversation. + // about how the visitor reads rather than part of the conversation. newBtn = el("button", "rbchat-new"); newBtn.type = "button"; newBtn.hidden = true; // The glyph is an arrow come back round, not a plus: a plus beside the cross read as "add", // and what the button does is start over. The note under it names the action for a pointer @@ -2210,7 +2211,7 @@ // Double-click gives the stylesheet's size back and forgets the stored one, as the stage's // gutter does: otherwise the only way back is to drag until it looks right again. grip.addEventListener("dblclick", function(){ - try { sessionStorage.removeItem(SIZE_KEY); } catch (e) {} + try { localStorage.removeItem(SIZE_KEY); } catch (e) {} unsize(); }); grip.addEventListener("keydown", function(ev){ diff --git a/blocks/theme-boot.js b/blocks/theme-boot.js index 47f3671..caa2165 100644 --- a/blocks/theme-boot.js +++ b/blocks/theme-boot.js @@ -1,4 +1,4 @@ - /* ─── theme boot · v5 · {{variant}} ──────────────────────────────────── + /* ─── theme boot · v6 · {{variant}} ──────────────────────────────────── Set the theme before anything paints. Generated from @robertblust/design — editing it here does nothing, because the next `npm run design` overwrites it. @@ -41,13 +41,14 @@ try { if (window.top !== window && /[?&]embed(&|$)/.test(location.search)) document.documentElement.setAttribute("data-embed", ""); } catch (e) {} - // A conversation the tab keeps open: the keys are chat.js's, `chat` and `chat-size`, read - // the way it reads them. Never in a frame, which takes no chat of its own. + // A conversation the tab keeps open: the keys are chat.js's, `chat` in the tab's storage and + // `chat-size` in the browser's, read the way it reads them. Never in a frame, which takes no + // chat of its own. try { var r = document.documentElement, c = JSON.parse(sessionStorage.getItem("chat") || "null"); if (c && c.open && window.top === window) { r.setAttribute("data-chat-waiting", location.host); - var z = JSON.parse(sessionStorage.getItem("chat-size") || "null"); + var z = JSON.parse(localStorage.getItem("chat-size") || "null"); if (z && z.w && z.h) { r.style.setProperty("--rbchat-kept-w", z.w + "px"); r.style.setProperty("--rbchat-kept-h", z.h + "px"); } } } catch (e) {} diff --git a/test/chat-terminal.test.mjs b/test/chat-terminal.test.mjs index 4610856..e40e1b4 100644 --- a/test/chat-terminal.test.mjs +++ b/test/chat-terminal.test.mjs @@ -610,3 +610,25 @@ test("the banner names the core and the model's commit, each linked, and the mod assert.equal(await broken.p.$(".rbchat-versions"), null); await broken.close(); }); + +test("a size given to the panel is the browser's: another tab opens the panel at it, and a double-click on the corner forgets it", async () => { + const context = await browser.newContext({ viewport: { width: 1280, height: 900 } }); + const size = (p) => p.$eval("section.rbchat", (x) => { const r = x.getBoundingClientRect(); return { w: Math.round(r.width), h: Math.round(r.height) }; }); + const kept = (p) => p.evaluate(() => ({ local: JSON.parse(localStorage.getItem("chat-size")), session: sessionStorage.getItem("chat-size") })); + // The family's reset, as every site's page carries it: the panel's size includes its border. + const arrive = async () => { const p = await context.newPage(); await p.goto(base + "/"); await p.waitForSelector(".rbchat-open", { state: "attached" }); await p.addStyleTag({ content: "*{box-sizing:border-box}" }); await open(p); return p; }; + const first = await arrive(); + const stock = await size(first); + await first.focus(".rbchat-grip"); + await first.keyboard.press("ArrowLeft"); await first.keyboard.press("ArrowLeft"); await first.keyboard.press("ArrowDown"); + const sized = await size(first); + assert.deepEqual(sized, { w: stock.w + 48, h: stock.h - 24 }); + assert.deepEqual(await kept(first), { local: sized, session: null }, "the size is kept where every tab reads it, and nowhere else"); + // A second tab has a session of its own and the same local storage. + const second = await arrive(); + assert.deepEqual(await size(second), sized); + await second.dblclick(".rbchat-grip"); + assert.deepEqual(await size(second), stock); + assert.deepEqual(await kept(second), { local: null, session: null }); + await context.close(); +}); diff --git a/test/first-paint.test.mjs b/test/first-paint.test.mjs index f89b80f..e5114c5 100644 --- a/test/first-paint.test.mjs +++ b/test/first-paint.test.mjs @@ -27,12 +27,13 @@ before(async () => { }); after(async () => { await browser.close(); await new Promise((r) => server.close(r)); }); -// A tab whose chat is kept open, as the page before this one left it. +// A tab whose chat is kept open, as the page before this one left it, in a browser that keeps +// the size a visitor once gave the panel. async function tabWithChat(stored, size) { const context = await browser.newContext({ viewport: { width: 1300, height: 900 } }); await context.addInitScript(([stored, size]) => { if (stored) sessionStorage.setItem("chat", JSON.stringify(stored)); - if (size) sessionStorage.setItem("chat-size", JSON.stringify(size)); + if (size) localStorage.setItem("chat-size", JSON.stringify(size)); }, [stored, size]); return { page: await context.newPage(), close: () => context.close() }; } diff --git a/versions.json b/versions.json index f8e249c..36ff290 100644 --- a/versions.json +++ b/versions.json @@ -11,7 +11,7 @@ "fit": "v1", "rests": "v1", "runtime": "v10", - "themeBoot": "v5", + "themeBoot": "v6", "theme": "v5", "navFit": "v3", "processes": "v1", From 1fe9b58157c4cfd43200f38d70c95ba792e2da2c Mon Sep 17 00:00:00 2001 From: Implementer Date: Wed, 14 Oct 2026 05:45:24 +0200 Subject: [PATCH 3/3] A link's question keeps the focus when the page sets its language On a desk the card's Send takes the focus so that Enter sends the question, and on the sites it did not hold it: a page sets its language after the panel has opened, the intro is redrawn in that language, and the focused button went with the card it stood in. The redraw now gives the focus back to the same button of the new card. Where nothing in the card held it, as on a touch screen, nothing takes it, so no keyboard rises. chat.js is a synced file, so every site's committed copy is stale: a minor. Verified: npm test (1076 pass), conventions-check and conventions-format pass; on a local preview of companygraph.io's homepage carrying this chat.js the focus stayed on Send with and without ?lang=de, where on companygraph.io as released it is on the body. Process: Delivery Phase: Implement Track: Code Co-Authored-By: Claude Opus 5.5 --- assets/chat.js | 8 +++++++- test/chat-ask.test.mjs | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/assets/chat.js b/assets/chat.js index c50c6a2..2ea2913 100644 --- a/assets/chat.js +++ b/assets/chat.js @@ -2085,7 +2085,13 @@ for (var b = 0; b < labels.length; b++) labels[b].textContent = strings(answerLang(turns, langNow())).askNext; } // A language switch redraws the intro in the new language, finished, where the log holds one. - if (introEl && log) { var keep_ = log.scrollTop; introEl.parentNode && introEl.parentNode.removeChild(introEl); introEl = null; intro(false); log.scrollTop = keep_; } + // A waiting question is redrawn with it, and the button that held the focus holds it again: + // a page sets its language after the panel has opened, and Enter still has to send. + if (introEl && log) { + var keep_ = log.scrollTop, held = askEl && askEl.contains(document.activeElement) ? Array.prototype.indexOf.call(askEl.querySelectorAll("button"), document.activeElement) : -1; + introEl.parentNode && introEl.parentNode.removeChild(introEl); introEl = null; intro(false); log.scrollTop = keep_; + if (held >= 0 && askEl) askEl.querySelectorAll("button")[held].focus({ preventScroll: true }); + } // The chips standing under an answer are drawn again from the titles they were picked as, so a // model question reads in the new language and a number key sends what its row now shows. if (qBox && qPicked && qPicked.length) { var was_ = qPicked.slice(); hideQuestions(); offer(was_); } diff --git a/test/chat-ask.test.mjs b/test/chat-ask.test.mjs index b2aa074..66517bc 100644 --- a/test/chat-ask.test.mjs +++ b/test/chat-ask.test.mjs @@ -105,6 +105,25 @@ test("the question stands after the prompt line once the menus are drawn above i await context.close(); }); +test("a language switch redraws the card and leaves the focus where it was: on Send on a desk, nowhere on a phone", async () => { + const german = async (page) => { + await page.evaluate(() => { document.documentElement.lang = "de"; }); + await page.waitForFunction(() => /^Der Link/.test(document.querySelector(".rbchat-ask-from").textContent)); + return page.evaluate(() => { const a = document.activeElement; return a === document.querySelector(".rbchat-ask-go") ? "send" : a.tagName; }); + }; + const desk = await arrive(link(Q)); + await desk.page.waitForSelector(".rbchat-ask"); + assert.equal(await german(desk.page), "send", "the redrawn card lost the focus, so Enter no longer sends"); + await desk.page.keyboard.press("Enter"); + await desk.page.waitForSelector(".rbchat-assistant"); + assert.deepEqual(posted.map((x) => x.messages), [[{ role: "user", content: Q }]]); + await desk.context.close(); + const phone = await arrive(link(Q), { hasTouch: true, isMobile: true, viewport: { width: 390, height: 844 } }); + await phone.page.waitForSelector(".rbchat-ask"); + assert.equal(await german(phone.page), "BODY"); + await phone.context.close(); +}); + test("the question keeps the address's other parameters, with or without ?chat=open", async () => { for (const [address, rest] of [[link(Q, "?lang=de") + "#x", "/?lang=de#x"], [`/?chat=open&ask=${encodeURIComponent(Q)}&theme=dark`, "/?theme=dark"]]) { const { page, context } = await arrive(address);