feat(chat): view context — tell the assistant what you're looking at
Nightly Build / build (push) Successful in 7m51s
Nightly Build / build (push) Successful in 7m51s
An eye next to the paperclip shares what the user has open with their next
message: the page, the folder being browsed, the file open in the viewer and
any highlighted passage (line numbers where a source view exists), plus which
entity a detail page is about. The bag is client-authored {label, value} pairs
in English — the backend only clamps (chars, never bytes), neutralizes the
harness tag and renders one <system-extra> block per message, deduped
consecutively so it appears exactly when the view changed. On by default,
per-device toggle, hover/tap to preview, a chip on every sent message;
docs/view-context.md for users, an updated harness.md clause for the model.
This commit is contained in:
@@ -508,6 +508,103 @@ export function renderAttachmentChips(host, attachments, { removable = false } =
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/* ── View context: the eye in the composer, the chip in the bubble ───────────── */
|
||||
|
||||
// Whether this device has a pointer that can hover. A mouse gets the panel on
|
||||
// hover and needs no click-away target; a touch screen has no hover, so there
|
||||
// the pill's tap opens the panel and a full-screen overlay closes it — the same
|
||||
// shape as the model dropdown. Read once: hover capability does not change
|
||||
// under a running page in any way worth re-rendering for.
|
||||
const CAN_HOVER = typeof window === 'undefined'
|
||||
|| !window.matchMedia
|
||||
|| window.matchMedia('(hover: hover)').matches;
|
||||
|
||||
/** The literal pairs, as they would appear (and as they were sent). */
|
||||
function renderViewContextItems(items) {
|
||||
return html`
|
||||
<div class="view-ctx-items">
|
||||
${items.map((it) => html`
|
||||
<div class="view-ctx-item">
|
||||
<div class="view-ctx-label">${it.label}</div>
|
||||
<div class="view-ctx-value">${it.value}</div>
|
||||
</div>
|
||||
`)}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* The eye: the composer's view-context control, shared by the desktop copilot
|
||||
* and the mobile chat.
|
||||
*
|
||||
* Always rendered, on or off, empty store or not — it is a privacy control, so
|
||||
* it has to be findable in the same place every time rather than appearing only
|
||||
* once there is something to share. Hovering (or tapping) it shows the literal
|
||||
* `label: value` pairs it would send: that is the verifiable half of §3, and it
|
||||
* is also the only way to debug a contributor without sending a message.
|
||||
*
|
||||
* `host` supplies `_viewContextEnabled`, `_viewContext`, `_viewContextOpen` and
|
||||
* `_toggleViewContext()` — all from `ChatSession`.
|
||||
*/
|
||||
export function renderViewContextPill(host) {
|
||||
const on = !!host._viewContextEnabled;
|
||||
const items = on ? (host._viewContext ?? []) : [];
|
||||
const open = !!host._viewContextOpen;
|
||||
const hover = CAN_HOVER
|
||||
? { enter: () => { host._viewContextOpen = true; }, leave: () => { host._viewContextOpen = false; } }
|
||||
: { enter: () => {}, leave: () => {} };
|
||||
|
||||
return html`
|
||||
<div class="view-ctx-wrap"
|
||||
@mouseenter=${hover.enter}
|
||||
@mouseleave=${hover.leave}>
|
||||
${open && !CAN_HOVER
|
||||
? html`<div class="view-ctx-overlay" @click=${() => { host._viewContextOpen = false; }}></div>`
|
||||
: nothing}
|
||||
${open ? html`
|
||||
<div class="view-ctx-panel">
|
||||
<div class="view-ctx-panel-title">
|
||||
${on ? t('chat.view_context.title') : t('chat.view_context.off_title')}
|
||||
</div>
|
||||
${!on
|
||||
? html`<div class="view-ctx-empty">${t('chat.view_context.off_hint')}</div>`
|
||||
: items.length
|
||||
? renderViewContextItems(items)
|
||||
: html`<div class="view-ctx-empty">${t('chat.view_context.empty')}</div>`}
|
||||
</div>
|
||||
` : nothing}
|
||||
<button
|
||||
class="view-ctx-btn ${on ? 'view-ctx-btn--on' : ''}"
|
||||
type="button"
|
||||
aria-pressed=${on ? 'true' : 'false'}
|
||||
title=${on ? t('chat.view_context.on') : t('chat.view_context.off')}
|
||||
@focus=${() => { host._viewContextOpen = true; }}
|
||||
@blur=${() => { host._viewContextOpen = false; }}
|
||||
@click=${() => { host._toggleViewContext(); host._viewContextOpen = true; }}
|
||||
>
|
||||
<i class="bi ${on ? 'bi-eye' : 'bi-eye-slash'}"></i>
|
||||
${on && items.length ? html`<span class="view-ctx-count">${items.length}</span>` : nothing}
|
||||
</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* shows the sanitized pairs the model was given — never the browser's intent.
|
||||
*/
|
||||
function renderViewContextChip(host, msg) {
|
||||
const items = msg.view_context;
|
||||
if (!items?.length) return nothing;
|
||||
return html`
|
||||
<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>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapsible chain-of-thought block: small, muted, collapsed by default so it
|
||||
* never weighs on the UI. A native <details> — Lit keeps the element stable
|
||||
@@ -528,7 +625,7 @@ export function renderMsg(host, msg) {
|
||||
try {
|
||||
switch (msg.kind) {
|
||||
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)}</div>`;
|
||||
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>`;
|
||||
case 'thinking':
|
||||
return html`
|
||||
<div class="copilot-msg assistant copilot-markdown ${msg.failed ? 'copilot-msg--failed' : ''}">
|
||||
|
||||
Reference in New Issue
Block a user