fix(view-context): a corner mark on the bubble, not a row under it
Nightly Build / build (push) Successful in 9s

The sent-message proof was a collapsed <details> under the user bubble: a
row of height in every bubble that carried view context, for something that
is evidence about the message rather than part of it. It also rendered a
blank line, since the bubble is white-space: pre-wrap and the template left
a newline before the element.

Now a faint eye in the bubble's bottom-right corner, revealing the pairs on
hover (on focus for keyboard and touch). Deliberately not expandable, so
scrolling back through a conversation cannot grow a second layout, and the
popover opens downwards rather than over the message it belongs to.

The i18n key stays alive as the mark's aria-label; docs and changelog wording
updated to match.
This commit is contained in:
Daniele
2026-08-23 23:36:32 +01:00
parent 8361a238c7
commit fc226aacab
4 changed files with 75 additions and 32 deletions
+1 -1
View File
@@ -29,7 +29,7 @@ release PR may merge — and a section is closed at the commit that bumps it.
assistant reading through it. Like an attachment, what the eye sends goes to the AI assistant reading through it. Like an attachment, what the eye sends goes to the AI
provider together with your message — hover it (or tap it) to read exactly what would provider together with your message — hover it (or tap it) to read exactly what would
go out, click it to stop sharing; the choice is remembered on this device, and every go out, click it to stop sharing; the choice is remembered on this device, and every
message shows a chip with what it carried. Very long highlights are trimmed, with a sent message keeps a faint eye in its corner that shows, on hover, what it carried. Very long highlights are trimmed, with a
note saying how much was left out — the assistant can still read the whole file itself. note saying how much was left out — the assistant can still read the whole file itself.
On by default. On by default.
- Several conversations per source: open extra chats with `+`, and the tab bar you left - Several conversations per source: open extra chats with `+`, and the tab bar you left
+1 -1
View File
@@ -12,7 +12,7 @@ Exactly one snapshot per message, covering whatever applies at that moment:
- **A highlighted passage** — if the user selected text in the viewer, the selected text itself, with its line numbers when they are looking at the source (a plain-text file, or the editor view of a Markdown file). - **A highlighted passage** — if the user selected text in the viewer, the selected text itself, with its line numbers when they are looking at the source (a plain-text file, or the editor view of a Markdown file).
- **Which thing a detail page is about** — which project (and which of its tabs), which member, connector, plugin, conversation, tool call or LLM request; also the active section in Tasks or Models, the open agent in Background agents, and a search typed in the Marketplace. - **Which thing a detail page is about** — which project (and which of its tabs), which member, connector, plugin, conversation, tool call or LLM request; also the active section in Tasks or Models, the open agent in Background agents, and a search typed in the Marketplace.
Hover the eye (or tap it, on a touch screen) to read exactly what would be sent with the next message. Every sent message shows a small chip with what it carried, which can be opened to read the actual values. Hover the eye (or tap it, on a touch screen) to read exactly what would be sent with the next message. Every sent message keeps a small eye in its corner; hovering it (or tapping it, on a touch screen) shows the actual values that message carried.
## Control and privacy ## Control and privacy
+14 -10
View File
@@ -591,18 +591,22 @@ export function renderViewContextPill(host) {
* The proof, in the sent bubble: what this message actually carried. Rendered * The proof, in the sent bubble: what this message actually carried. Rendered
* from the server's echo (and, after a reload, from the REST history), so it * from the server's echo (and, after a reload, from the REST history), so it
* shows the sanitized pairs the model was given — never the browser's intent. * shows the sanitized pairs the model was given — never the browser's intent.
*
* A mark in the bubble's corner, not a row under the message: this is evidence
* about a message, not part of it, and a collapsed row still cost a line of
* height in every bubble that carried one. Deliberately **not** expandable —
* the pairs show on hover (on focus for keyboard and touch) and there is no
* state to leave open, so a conversation scrolled back through never grows a
* second layout.
*
* Written on one line on purpose: the bubble renders `white-space: pre-wrap`, so
* the newline and indent a formatted template would leave before the element are
* a text node the bubble actually shows — a blank line under the message.
*/ */
function renderViewContextChip(host, msg) { function renderViewContextMark(host, msg) {
const items = msg.view_context; const items = msg.view_context;
if (!items?.length) return nothing; if (!items?.length) return nothing;
return html` return html`<div class="view-ctx-mark" tabindex="0" aria-label=${t('chat.view_context.chip', { n: items.length })}><i class="bi bi-eye"></i><div class="view-ctx-pop">${renderViewContextItems(items)}</div></div>`;
<details class="view-ctx-chip">
<summary>
<i class="bi bi-eye"></i>
<span>${t('chat.view_context.chip', { n: items.length })}</span>
</summary>
${renderViewContextItems(items)}
</details>`;
} }
/** /**
@@ -625,7 +629,7 @@ export function renderMsg(host, msg) {
try { try {
switch (msg.kind) { switch (msg.kind) {
case 'user': case 'user':
return html`<div class="copilot-msg user ${msg.failed ? 'copilot-msg--failed' : ''}" style="white-space:pre-wrap">${msg.failed ? failedBadge() : nothing}${msg.content}${renderAttachmentChips(host, msg.attachments)}${renderViewContextChip(host, msg)}</div>`; return html`<div class="copilot-msg user ${msg.view_context?.length ? 'copilot-msg--view-ctx' : ''} ${msg.failed ? 'copilot-msg--failed' : ''}" style="white-space:pre-wrap">${msg.failed ? failedBadge() : nothing}${msg.content}${renderAttachmentChips(host, msg.attachments)}${renderViewContextMark(host, msg)}</div>`;
case 'thinking': case 'thinking':
return html` return html`
<div class="copilot-msg assistant copilot-markdown ${msg.failed ? 'copilot-msg--failed' : ''}"> <div class="copilot-msg assistant copilot-markdown ${msg.failed ? 'copilot-msg--failed' : ''}">
+59 -20
View File
@@ -889,33 +889,72 @@
line-height: 1.35; line-height: 1.35;
} }
/* The chip inside a sent user bubble: collapsed by default, same muted weight as /* The mark in a sent user bubble's corner: it is evidence about the message, not
the reasoning block — it is evidence, not content. */ part of it, so it costs the bubble a corner rather than a row of its own. Kept
.view-ctx-chip { faint on purpose — it should be findable when looked for and invisible when
margin-top: 0.4rem; not. Hover (focus, on touch and for the keyboard) reveals the pairs; there is
font-size: 0.75rem; no expanded state, by design. */
white-space: normal; .copilot-msg.user.copilot-msg--view-ctx {
position: relative;
/* Room for the mark, so a last line can never run under it. */
padding-bottom: 1.35rem;
} }
.view-ctx-chip > summary { .view-ctx-mark {
position: absolute;
right: 0.5rem;
bottom: 0.25rem;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 0.3rem; font-size: 0.8rem;
padding: 0.2rem 0.45rem; line-height: 1;
border: 1px solid var(--toolbar-border); color: inherit;
border-radius: 0.5rem; opacity: 0.4;
color: var(--placeholder-color); cursor: default;
cursor: pointer; transition: opacity 0.12s;
list-style: none;
line-height: 1.2;
} }
.view-ctx-chip > summary::-webkit-details-marker { display: none; } .view-ctx-mark:hover,
.view-ctx-chip > summary:hover { border-color: var(--accent); color: var(--accent); } .view-ctx-mark:focus { opacity: 0.9; outline: none; }
.view-ctx-mark:focus-visible { opacity: 0.9; outline: 2px solid currentColor; outline-offset: 2px; border-radius: 0.2rem; }
.view-ctx-chip .view-ctx-items { /* Below the bubble, right-aligned with it: above would cover the message the
margin-top: 0.4rem; mark belongs to. */
padding-left: 0.15rem; .view-ctx-pop {
position: absolute;
top: calc(100% + 6px);
right: 0;
z-index: 20;
display: block;
width: max-content;
max-width: min(26rem, 70vw);
max-height: 16rem;
overflow-y: auto;
padding: 0.5rem 0.65rem;
border: 1px solid var(--toolbar-border);
border-radius: 0.5rem;
background: var(--msg-assistant-bg);
color: var(--msg-assistant-text);
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.12);
font-size: 0.75rem;
/* The bubble is pre-wrap; the popover formats its own values. */
white-space: normal;
text-align: left;
opacity: 0;
visibility: hidden;
pointer-events: none;
transition: opacity 0.12s;
}
/* Interactive only while shown: `:hover` applies to ancestors of the hovered
element, so letting the pointer onto the popover keeps the mark hovered — which
is what makes a long, scrollable value readable. Hidden, it must not eat a
click on the bubble underneath. */
.view-ctx-mark:hover .view-ctx-pop,
.view-ctx-mark:focus .view-ctx-pop {
opacity: 1;
visibility: visible;
pointer-events: auto;
} }
/* ── Tool card "view details" (eye) ────────────────────────────────────────── */ /* ── Tool card "view details" (eye) ────────────────────────────────────────── */