rename agents/main→assistant, role-based default entry agent
Nightly Build / build (push) Successful in 6m31s

This commit is contained in:
2026-07-21 21:40:06 +01:00
parent 17f5769e0d
commit 8ff64cbddc
22 changed files with 204 additions and 44 deletions
+2 -2
View File
@@ -313,7 +313,7 @@ export function renderAgent(msg) {
<div class="copilot-agent-header">
<i class="bi bi-${icon}"></i>
<span>
<strong>${msg.parent_agent_id ?? 'main'}</strong>
<strong>${msg.parent_agent_id ?? 'assistant'}</strong>
<i class="bi bi-arrow-right mx-1" style="font-size:0.7rem"></i>
<strong>${msg.agent_id}</strong>
</span>
@@ -334,7 +334,7 @@ export function renderAgentEnd(msg) {
<span>
<strong>${msg.agent_id}</strong>
<i class="bi bi-arrow-right mx-1" style="font-size:0.7rem"></i>
<strong>${msg.parent_agent_id ?? 'main'}</strong>
<strong>${msg.parent_agent_id ?? 'assistant'}</strong>
</span>
<span class="copilot-agent-badge done">${t('copilot.agent_finished')}</span>
</div>
+40 -6
View File
@@ -4,6 +4,9 @@ import { LightElement } from '../lib/base.js';
import { t } from '../lib/i18n.js';
const ADMIN_ID = 'admin';
// Source-driven chat agent (needs a project run-context): never a personal default,
// so it is excluded from the role assistant picker (mirrors the server-side guard).
const PROJECT_COORDINATOR_ID = 'project-coordinator';
export class RolesPage extends LightElement {
@@ -12,6 +15,7 @@ export class RolesPage extends LightElement {
_open: { state: true },
_roles: { state: true },
_groups: { state: true },
_agents: { state: true },
_error: { state: true },
_modal: { state: true }, // null | { mode: 'create'|'edit', role?, form }
};
@@ -22,6 +26,7 @@ export class RolesPage extends LightElement {
this._open = false;
this._roles = null;
this._groups = null;
this._agents = null;
this._error = null;
this._modal = null;
}
@@ -45,14 +50,19 @@ export class RolesPage extends LightElement {
async _load() {
this._error = null;
try {
const [rRes, gRes] = await Promise.all([
const [rRes, gRes, aRes] = await Promise.all([
fetch('/api/roles'),
fetch('/api/tool-permission-groups'),
fetch('/api/agents'),
]);
if (!rRes.ok) throw new Error(`HTTP ${rRes.status}`);
if (!gRes.ok) throw new Error(`HTTP ${gRes.status}`);
if (!aRes.ok) throw new Error(`HTTP ${aRes.status}`);
this._roles = await rRes.json();
this._groups = await gRes.json();
// Only `type:chat` agents are entry agents; project-coordinator is source-driven.
this._agents = (await aRes.json())
.filter(a => a.type === 'chat' && a.id !== PROJECT_COORDINATOR_ID);
} catch (e) {
this._error = e.message;
}
@@ -76,12 +86,26 @@ export class RolesPage extends LightElement {
} catch { return []; }
}
_mergeAttrs(attrs, uiMode, allowedGroups) {
// The role's default entry agent (data-driven, §0.1). Empty means "fall back to the
// instance default assistant" — the server resolver handles it, so we store nothing.
_attrsChatAgent(attrs) {
try { const a = JSON.parse(attrs || '{}').chat_agent; return typeof a === 'string' ? a : ''; }
catch { return ''; }
}
// Display name for a chat-agent id (falls back to the id, or the default label when unset).
_agentName(id) {
if (!id) return t('roles.form.assistant_default');
return this._agents?.find(a => a.id === id)?.name ?? id;
}
_mergeAttrs(attrs, uiMode, allowedGroups, chatAgent) {
let o = {};
try { o = JSON.parse(attrs || '{}') ?? {}; } catch { o = {}; }
if (uiMode === 'simple') o.ui_mode = 'simple'; else delete o.ui_mode;
const extras = Array.isArray(allowedGroups) ? allowedGroups.filter(Boolean) : [];
if (extras.length) o.permission_groups = extras; else delete o.permission_groups;
if (chatAgent) o.chat_agent = chatAgent; else delete o.chat_agent;
const keys = Object.keys(o);
return keys.length ? JSON.stringify(o) : null;
}
@@ -89,7 +113,7 @@ export class RolesPage extends LightElement {
_openCreate() {
this._modal = {
mode: 'create',
form: { id: '', label: '', permission_group: this._groups?.[0]?.id ?? 'default', attrs: '', ui_mode: 'full', allowed_groups: [] },
form: { id: '', label: '', permission_group: this._groups?.[0]?.id ?? 'default', attrs: '', ui_mode: 'full', allowed_groups: [], chat_agent: '' },
};
}
@@ -97,7 +121,7 @@ export class RolesPage extends LightElement {
this._modal = {
mode: 'edit',
role,
form: { label: role.label, permission_group: role.permission_group, attrs: role.attrs ?? '', ui_mode: this._attrsUiMode(role.attrs), allowed_groups: this._attrsAllowedGroups(role.attrs) },
form: { label: role.label, permission_group: role.permission_group, attrs: role.attrs ?? '', ui_mode: this._attrsUiMode(role.attrs), allowed_groups: this._attrsAllowedGroups(role.attrs), chat_agent: this._attrsChatAgent(role.attrs) },
};
}
@@ -129,7 +153,7 @@ export class RolesPage extends LightElement {
id: form.id.trim(),
label: form.label.trim(),
permission_group: form.permission_group,
attrs: this._mergeAttrs(form.attrs, form.ui_mode, form.allowed_groups),
attrs: this._mergeAttrs(form.attrs, form.ui_mode, form.allowed_groups, form.chat_agent),
}),
});
if (!res.ok) throw new Error(await res.text());
@@ -146,7 +170,7 @@ export class RolesPage extends LightElement {
body: JSON.stringify({
label: form.label.trim(),
permission_group: form.permission_group,
attrs: this._mergeAttrs(form.attrs, form.ui_mode, form.allowed_groups),
attrs: this._mergeAttrs(form.attrs, form.ui_mode, form.allowed_groups, form.chat_agent),
}),
});
if (!res.ok) throw new Error(await res.text());
@@ -226,6 +250,14 @@ export class RolesPage extends LightElement {
</select>
<div class="form-text" style="font-size:.75rem">${unsafeHTML(t('roles.form.interface_hint'))}</div>
</div>
<div class="mb-3">
<label class="form-label">${t('roles.form.assistant')}</label>
<select class="form-select" @change=${e => this._patch('chat_agent', e.target.value)}>
<option value="" ?selected=${!form.chat_agent}>${t('roles.form.assistant_default')}</option>
${(this._agents ?? []).map(a => html`<option value=${a.id} ?selected=${form.chat_agent === a.id}>${a.name}</option>`)}
</select>
<div class="form-text" style="font-size:.75rem">${t('roles.form.assistant_hint')}</div>
</div>
<div class="mb-3">
<label class="form-label">${t('roles.form.attrs')} <span class="text-muted">${t('roles.form.attrs_hint')}</span></label>
<input class="form-control font-monospace" placeholder=${t('roles.form.attrs_ph')} .value=${form.attrs}
@@ -275,6 +307,7 @@ export class RolesPage extends LightElement {
<th>${t('roles.col.label')}</th>
<th>${t('roles.col.group')}</th>
<th>${t('roles.col.interface')}</th>
<th>${t('roles.col.assistant')}</th>
<th></th>
</tr>
</thead>
@@ -289,6 +322,7 @@ export class RolesPage extends LightElement {
<td>${this._attrsUiMode(r.attrs) === 'simple'
? html`<span class="badge" style="background:var(--accent-soft);color:var(--accent)">${t('roles.badge.simple')}</span>`
: html`<span class="badge bg-secondary">${t('roles.badge.full')}</span>`}</td>
<td>${this._agentName(this._attrsChatAgent(r.attrs))}</td>
<td>
<div class="um-actions">
<button class="um-btn-icon" title=${isAdmin ? t('roles.tooltip.locked') : t('roles.tooltip.edit')}
+6 -2
View File
@@ -118,7 +118,7 @@ export default {
'llmr.empty': 'No requests found.',
'llmr.total': '{n} rows',
'llmr.filter.agent_id': 'Agent ID',
'llmr.filter.agent_ph': 'e.g. main',
'llmr.filter.agent_ph': 'e.g. assistant',
'llmr.filter.source': 'Source',
'llmr.filter.source_ph': 'e.g. web, tic, cron',
'llmr.filter.from': 'From',
@@ -556,7 +556,7 @@ export default {
'approval.form.source': 'Source',
'approval.form.source_any': 'Any',
'approval.form.agent_id': 'Agent ID',
'approval.form.agent_id_ph': 'main (empty = any)',
'approval.form.agent_id_ph': 'assistant (empty = any)',
'approval.form.note': 'Note',
'approval.form.note_ph': 'Short description…',
'approval.form.cancel': 'Cancel',
@@ -656,6 +656,7 @@ export default {
'roles.col.label': 'Label',
'roles.col.group': 'Permission group',
'roles.col.interface': 'Interface',
'roles.col.assistant': 'Assistant',
'roles.badge.simple': 'Simple',
'roles.badge.full': 'Full',
@@ -674,6 +675,9 @@ export default {
'roles.form.interface_full': 'Full — all pages',
'roles.form.interface_simple': 'Simple — chat only',
'roles.form.interface_hint': 'Members with the simple interface see only chat and inbox. Stored as <code>ui_mode</code> in the attrs JSON below.',
'roles.form.assistant': 'Default assistant',
'roles.form.assistant_default': 'Default (Assistant)',
'roles.form.assistant_hint': 'The agent members of this role chat with by default. Overridable per person later.',
'roles.form.attrs': 'Attrs',
'roles.form.attrs_hint': '(JSON, optional)',
'roles.form.attrs_ph': '{}',
+6 -2
View File
@@ -118,7 +118,7 @@ export default {
'llmr.empty': 'Aucune requête trouvée.',
'llmr.total': '{n} lignes',
'llmr.filter.agent_id': 'ID de l\'agent',
'llmr.filter.agent_ph': 'ex. main',
'llmr.filter.agent_ph': 'ex. assistant',
'llmr.filter.source': 'Source',
'llmr.filter.source_ph': 'ex. web, tic, cron',
'llmr.filter.from': 'De',
@@ -556,7 +556,7 @@ export default {
'approval.form.source': 'Source',
'approval.form.source_any': 'Toute',
'approval.form.agent_id': 'ID de l\'agent',
'approval.form.agent_id_ph': 'main (vide = toute)',
'approval.form.agent_id_ph': 'assistant (vide = toute)',
'approval.form.note': 'Note',
'approval.form.note_ph': 'Brève description…',
'approval.form.cancel': 'Annuler',
@@ -656,6 +656,7 @@ export default {
'roles.col.label': 'Libellé',
'roles.col.group': 'Groupe de permissions',
'roles.col.interface': 'Interface',
'roles.col.assistant': 'Assistant',
'roles.badge.simple': 'Simple',
'roles.badge.full': 'Complet',
@@ -674,6 +675,9 @@ export default {
'roles.form.interface_full': 'Complet — toutes les pages',
'roles.form.interface_simple': 'Simple — discussion uniquement',
'roles.form.interface_hint': 'Les membres avec l\'interface simple voient seulement la discussion et la boîte de réception. Stocké comme <code>ui_mode</code> dans le JSON d\'attributs ci-dessous.',
'roles.form.assistant': 'Assistant par défaut',
'roles.form.assistant_default': 'Par défaut (Assistant)',
'roles.form.assistant_hint': 'L\'agent avec lequel les membres de ce rôle discutent par défaut. Remplaçable par personne plus tard.',
'roles.form.attrs': 'Attributs',
'roles.form.attrs_hint': '(JSON, facultatif)',
'roles.form.attrs_ph': '{}',
+6 -2
View File
@@ -118,7 +118,7 @@ export default {
'llmr.empty': 'Nessuna richiesta trovata.',
'llmr.total': '{n} righe',
'llmr.filter.agent_id': 'ID Agente',
'llmr.filter.agent_ph': 'es. main',
'llmr.filter.agent_ph': 'es. assistant',
'llmr.filter.source': 'Sorgente',
'llmr.filter.source_ph': 'es. web, tic, cron',
'llmr.filter.from': 'Da',
@@ -556,7 +556,7 @@ export default {
'approval.form.source': 'Origine',
'approval.form.source_any': 'Qualsiasi',
'approval.form.agent_id': 'ID agente',
'approval.form.agent_id_ph': 'main (vuoto = qualsiasi)',
'approval.form.agent_id_ph': 'assistant (vuoto = qualsiasi)',
'approval.form.note': 'Nota',
'approval.form.note_ph': 'Breve descrizione…',
'approval.form.cancel': 'Annulla',
@@ -656,6 +656,7 @@ export default {
'roles.col.label': 'Etichetta',
'roles.col.group': 'Gruppo di permessi',
'roles.col.interface': 'Interfaccia',
'roles.col.assistant': 'Assistente',
'roles.badge.simple': 'Semplice',
'roles.badge.full': 'Completa',
@@ -674,6 +675,9 @@ export default {
'roles.form.interface_full': 'Completa — tutte le pagine',
'roles.form.interface_simple': 'Semplice — solo chat',
'roles.form.interface_hint': 'I membri con interfaccia semplice vedono solo chat e richieste. Memorizzato come <code>ui_mode</code> nell\'attrs JSON qui sotto.',
'roles.form.assistant': 'Assistente predefinito',
'roles.form.assistant_default': 'Predefinito (Assistente)',
'roles.form.assistant_hint': 'L\'agente con cui i membri di questo ruolo parlano di default. In futuro sovrascrivibile per persona.',
'roles.form.attrs': 'Attrs',
'roles.form.attrs_hint': '(JSON, opzionale)',
'roles.form.attrs_ph': '{}',