feat(chat): view context — tell the assistant what you're looking at
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:
Daniele
2026-08-23 20:53:30 +01:00
parent 488c702517
commit 505f2e95c1
42 changed files with 2096 additions and 122 deletions
+27 -1
View File
@@ -1,8 +1,14 @@
import { html, nothing } from 'lit';
import { LightElement } from '../../lib/base.js';
import { t } from '../../lib/i18n.js';
import { setSlice, clearSlice } from '../../lib/view-context.js';
import '../shared/file-explorer.js';
/// The view-context slice this board owns (see `lib/view-context.js`). The
/// board element is dropped by the host whenever the page closes or returns to
/// the list, so `disconnectedCallback` is the whole cleanup story.
const VIEW_SLICE = 'entity@projects';
/// A project's detail page: header + description, then two tabs — **Files** (the
/// shared `<file-explorer>`, pointed at the project folder) and **Sharing**
/// (member picker with read/write, mirroring the shared-folders UI).
@@ -33,9 +39,23 @@ export class ProjectBoardSection extends LightElement {
disconnectedCallback() {
window.removeEventListener('locale-changed', this.__onLocaleChanged);
clearSlice(VIEW_SLICE);
super.disconnectedCallback();
}
// Say which project this is, and which tab is showing. The tab matters because
// the explorer keeps its `path` slice standing while hidden behind *Sharing*:
// without this, the context would describe a folder and not that the user is
// looking at the member list.
_publishViewContext() {
const p = this._project;
if (!p) return;
setSlice(VIEW_SLICE, [
{ label: 'Open project', value: p.root_path ? `${p.name} — folder ${p.root_path}` : String(p.name) },
{ label: 'Open tab', value: this._tab === 'sharing' ? 'Sharing' : 'Files' },
]);
}
async load(projectId, tab) {
this._projectId = projectId;
this._project = null;
@@ -49,6 +69,7 @@ export class ProjectBoardSection extends LightElement {
if (!projRes.ok) throw new Error(`HTTP ${projRes.status}`);
this._project = await projRes.json();
if (usersRes.ok) this._users = await usersRes.json();
this._publishViewContext();
} catch (e) {
this._error = e.message;
}
@@ -57,7 +78,10 @@ export class ProjectBoardSection extends LightElement {
async _reload() {
try {
const res = await fetch(`/api/projects/${this._projectId}`);
if (res.ok) this._project = await res.json();
if (res.ok) {
this._project = await res.json();
this._publishViewContext();
}
} catch { /* transient */ }
}
@@ -121,11 +145,13 @@ export class ProjectBoardSection extends LightElement {
// Switch the visible tab without reloading (host back/forward sync).
setTab(tab) {
this._tab = tab === 'sharing' ? 'sharing' : 'files';
this._publishViewContext();
}
_selectTab(tab) {
if (tab === this._tab) return;
this._tab = tab;
this._publishViewContext();
this.dispatchEvent(new CustomEvent('project-tab-change', {
detail: { tab }, bubbles: true, composed: true,
}));