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:
@@ -0,0 +1,213 @@
|
||||
/**
|
||||
* Route → one English sentence about the page the user is on.
|
||||
*
|
||||
* ── THESE STRINGS ARE NOT TRANSLATED, AND MUST NEVER BE ──────────────────────
|
||||
* They are not interface copy: they are sent to the LLM as part of the message,
|
||||
* inside the harness block, and every system prompt around them is in English.
|
||||
* Routing them through `t()` would make an Italian user send "Pagina file" to a
|
||||
* model reading an English prompt — worse than saying nothing. This is exactly
|
||||
* the kind of thing someone "fixes" by mistake six months from now; it is not a
|
||||
* missing translation, it is the design.
|
||||
*
|
||||
* The i18n rule for this feature splits on the reader: labels the *user* reads
|
||||
* (the eye toggle, the chip in the bubble) are translated like any other UI
|
||||
* string; the text the *model* reads lives here, in English.
|
||||
*
|
||||
* Each entry says what the page shows and what can be done on it — one or two
|
||||
* lines. Where `docs/` has a page for the feature, the sentence ends with a
|
||||
* pointer to it, so the assistant can read the real documentation instead of
|
||||
* guessing; the pointed-at file must exist (a pointer to a missing page sends
|
||||
* the agent into a dead end).
|
||||
*/
|
||||
|
||||
// The label of the route slice. Constant, so the frontend and the eye tooltip
|
||||
// cannot disagree on what to call it.
|
||||
export const ROUTE_LABEL = 'Open page';
|
||||
|
||||
// route id → { title, what, doc? }
|
||||
// `title` is the page's name as the user sees it in the menu; `what` is the
|
||||
// sentence; `doc` is a path under the workspace's read-only `docs/` mount.
|
||||
export const ROUTE_DESCRIPTIONS = {
|
||||
home: {
|
||||
title: 'Chat',
|
||||
what: 'the assistant chat, which is also the app\'s home page — the conversation fills the page instead of sitting in a side panel.',
|
||||
},
|
||||
inbox: {
|
||||
title: 'Inbox',
|
||||
what: 'everything raised by background work and waiting for an answer: approval requests, questions from background agents, and sign-in prompts from connectors.',
|
||||
doc: 'docs/tasks.md',
|
||||
},
|
||||
dashboard: {
|
||||
title: 'Dashboard',
|
||||
what: 'instance status, LLM usage charts, the pending inbox items and a short guide.',
|
||||
},
|
||||
tasks: {
|
||||
title: 'Task Manager',
|
||||
what: 'background work: what is running now, recurring (cron) jobs, one-off scheduled runs, and the history of past runs.',
|
||||
doc: 'docs/tasks.md',
|
||||
},
|
||||
projects: {
|
||||
title: 'Projects',
|
||||
what: 'the projects this user is a member of — shared workspaces, each with its own folder, chat and member list; a project can be opened, created or shared from here.',
|
||||
doc: 'docs/projects.md',
|
||||
},
|
||||
files: {
|
||||
title: 'Files',
|
||||
what: 'the file browser over everything this user can reach: their home, both memory stores, shared folders, projects, skills and docs.',
|
||||
doc: 'docs/files.md',
|
||||
},
|
||||
models: {
|
||||
title: 'Models',
|
||||
what: 'the admin page for the models the instance uses — language, transcription, text-to-speech and image generation.',
|
||||
},
|
||||
providers: {
|
||||
title: 'LLM providers',
|
||||
what: 'the admin page for the LLM provider accounts and their endpoints and keys.',
|
||||
},
|
||||
approval: {
|
||||
title: 'Security',
|
||||
what: 'the admin page for security groups and approval rules — which tools an agent may use freely, which need a human to approve them, and which are denied.',
|
||||
},
|
||||
agents: {
|
||||
title: 'Agents',
|
||||
what: 'the agents installed on this instance: what each one is for, its model and its settings.',
|
||||
doc: 'docs/agents.md',
|
||||
},
|
||||
users: {
|
||||
title: 'Users',
|
||||
what: 'the admin directory of the people on this instance; each person\'s page holds their profile and what they may use (connectors, plugins, security).',
|
||||
doc: 'docs/access.md',
|
||||
},
|
||||
roles: {
|
||||
title: 'Roles',
|
||||
what: 'the admin page for roles — the permissions, default assistant and interface mode a group of people gets.',
|
||||
},
|
||||
'shared-folders': {
|
||||
title: 'Shared folders',
|
||||
what: 'the admin page for the folders shared across the instance, and who may read or write each one.',
|
||||
doc: 'docs/shared-folders.md',
|
||||
},
|
||||
connectors: {
|
||||
title: 'Connectors',
|
||||
what: 'the connectors (MCP servers) available here, which ones this user has turned on, and — for an admin — adding or removing them.',
|
||||
doc: 'docs/connectors.md',
|
||||
},
|
||||
connector: {
|
||||
title: 'Connector detail',
|
||||
what: 'one connector\'s own page: its configuration, its sign-in or pairing state, and a button to test it.',
|
||||
doc: 'docs/connectors.md',
|
||||
},
|
||||
marketplace: {
|
||||
title: 'Connector marketplace',
|
||||
what: 'the catalogue of connectors that can be installed on this instance, with their versions and updates.',
|
||||
doc: 'docs/connectors.md',
|
||||
},
|
||||
plugins: {
|
||||
title: 'Plugins',
|
||||
what: 'the admin status board of the installed plugins — one card each, with an enable switch, a health indicator and a link to its settings.',
|
||||
doc: 'docs/access.md',
|
||||
},
|
||||
'plugin-detail': {
|
||||
title: 'Plugin detail',
|
||||
what: 'one plugin\'s admin page: its instance-wide settings and a read-only list of who currently has access to it.',
|
||||
doc: 'docs/access.md',
|
||||
},
|
||||
profile: {
|
||||
title: 'Profile',
|
||||
what: 'this user\'s own account page: display name, avatar, interface language and password.',
|
||||
},
|
||||
config: {
|
||||
title: 'Config',
|
||||
what: 'the admin page for instance-wide settings, such as the default interface language, the compaction model and debug mode.',
|
||||
doc: 'docs/settings.md',
|
||||
},
|
||||
'llm-requests': {
|
||||
title: 'LLM requests',
|
||||
what: 'the debug log of the requests sent to the LLM providers, with the payload of each one.',
|
||||
},
|
||||
session: {
|
||||
title: 'Conversation detail',
|
||||
what: 'the full record of one conversation, tool calls included.',
|
||||
},
|
||||
'system-agents': {
|
||||
title: 'Background agents',
|
||||
what: 'the agents that run on a schedule (event triage, the memory lints, the conversation review): what each does, its settings, and this user\'s own run history.',
|
||||
doc: 'docs/system-agents.md',
|
||||
},
|
||||
file_viewer: {
|
||||
title: 'File viewer',
|
||||
what: 'one file from the user\'s workspace, opened for reading.',
|
||||
doc: 'docs/files.md',
|
||||
},
|
||||
tool_detail: {
|
||||
title: 'Tool call detail',
|
||||
what: 'the full record of one tool call: its arguments, its result and how long it took.',
|
||||
},
|
||||
};
|
||||
|
||||
// A plugin-contributed page (`#plugin/<plugin_id>/<page_id>`). The route only
|
||||
// carries ids — the page's own title is the plugin's to publish, and the
|
||||
// contributor slice (T6) is what adds it.
|
||||
function describePluginRoute(route) {
|
||||
const m = route.match(/^plugin\/([^/?]+)\/([^/?]+)$/);
|
||||
if (!m) return null;
|
||||
return `Plugin page (#${route}) — a page contributed by the "${m[1]}" plugin (page "${m[2]}").`;
|
||||
}
|
||||
|
||||
/**
|
||||
* The sentence for a page id, or `null` for an unknown route.
|
||||
*
|
||||
* Shape: `Title (#route) — what it is. More in docs/x.md.` The hash is part of
|
||||
* the sentence on purpose: it is the same string the user sees in the address
|
||||
* bar, so a follow-up question about "this page" and the URL they might paste
|
||||
* refer to the same thing.
|
||||
*/
|
||||
export function describeRoute(page) {
|
||||
if (!page) return null;
|
||||
if (page.startsWith('plugin/')) return describePluginRoute(page);
|
||||
const entry = ROUTE_DESCRIPTIONS[page];
|
||||
if (!entry) return null;
|
||||
const where = page === 'home' ? 'the home page' : `#${page}`;
|
||||
const doc = entry.doc ? ` More in ${entry.doc}.` : '';
|
||||
return `${entry.title} (${where}) — ${entry.what}${doc}`;
|
||||
}
|
||||
|
||||
/** The `route` slice: zero or one item, ready for `setSlice('route', …)`. */
|
||||
export function routeSliceFor(page) {
|
||||
const value = describeRoute(page);
|
||||
return value ? [{ label: ROUTE_LABEL, value }] : [];
|
||||
}
|
||||
|
||||
// ── The mobile shell ─────────────────────────────────────────────────────────
|
||||
// `mobile-app.js` routes a fixed set of sections of its own (`#chat`, `#inbox`,
|
||||
// …) and never goes through `pageFromHash`, so the desktop table above cannot
|
||||
// describe them. The shell claims the route slice (`claimRouteProvider` in
|
||||
// `view-context.js`) and renders it from here — same sentence shape, same rule
|
||||
// as the rest of this file: English, and never through t().
|
||||
const MOBILE_SECTIONS = {
|
||||
chat: { title: 'Chat', what: 'the assistant chat' },
|
||||
inbox: { title: 'Inbox', what: 'everything raised by background work and waiting for an answer: approval requests, questions from background agents, and sign-in prompts from connectors' },
|
||||
projects: { title: 'Projects', what: 'the projects this user is a member of; opening one opens its chat' },
|
||||
notifications:{ title: 'Notifications',what: 'a placeholder section — there is nothing here yet' },
|
||||
settings: { title: 'Settings', what: 'this user\'s own account page: display name, interface language and password' },
|
||||
file_viewer: { title: 'File viewer', what: 'one file from the user\'s workspace, opened for reading' },
|
||||
tool_detail: { title: 'Tool call detail', what: 'the full record of one tool call: its arguments, its result and how long it took' },
|
||||
};
|
||||
|
||||
/**
|
||||
* The mobile route slice, for the shell's current section. `projectId` /
|
||||
* `projectLabel` are set when the chat is bound to a project
|
||||
* (`#chat/project-<id>`); the label arrives asynchronously, so a project chat
|
||||
* may briefly read as the bare id.
|
||||
*/
|
||||
export function mobileRouteSliceFor({ section, projectId, projectLabel } = {}) {
|
||||
const entry = MOBILE_SECTIONS[section];
|
||||
if (!entry) return [];
|
||||
let where = `#${section}`;
|
||||
let extra = '';
|
||||
if (section === 'chat' && projectId) {
|
||||
where = `#chat/project-${projectId}`;
|
||||
extra = `, bound to the chat of the project "${projectLabel ?? projectId}"`;
|
||||
}
|
||||
return [{ label: ROUTE_LABEL, value: `${entry.title} (${where}, mobile app) — ${entry.what}${extra}.` }];
|
||||
}
|
||||
Reference in New Issue
Block a user