@adia-ai/web-modules 0.8.25 → 0.8.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,153 @@
1
+ # Edit this file; run `npm run build:components` to regenerate a2ui.json.
2
+ $schema: ../../../../scripts/schemas/component.yaml.schema.json
3
+ name: UIAdminSettings
4
+ tag: admin-settings-ui
5
+ status: stable
6
+ component: AdminSettings
7
+ category: container
8
+ version: 1
9
+ description: |
10
+ L4 settings pane over ONE bound per-persona store (CHAT-HARNESS.md
11
+ §Components row L4 + §Interfaces 5, gh#600 / WCH-5). Sections group by
12
+ `@adia-ai/persona`'s `KNOWN_KINDS` — DISPLAY grouping only; this
13
+ composite never switches on `entry.kind` for logic (law 7: Entry is one
14
+ generic primitive). Each Entry renders through the same row: label +
15
+ description, an enabled toggle, and a collapsible content textarea
16
+ (authoring). Both write through `store.set(...)` immediately — the
17
+ store is the state, persisted per persona.
18
+
19
+ `persona` and `store` are plain JS properties (no attribute path);
20
+ `store` is an opaque `SettingsStore` object from
21
+ `@adia-ai/persona`'s `personaStore`.
22
+
23
+ Lives in `packages/web-modules/agent-admin/` alongside
24
+ `<admin-roster-ui>` and `<agent-admin-ui>`.
25
+
26
+ composes:
27
+ - switch-ui
28
+ - textarea-ui
29
+ - text-ui
30
+ - icon-ui
31
+
32
+ props:
33
+ persona:
34
+ description: |
35
+ The `Persona` (from `@adia-ai/persona`) whose entries this pane
36
+ renders, grouped by `KNOWN_KINDS`. JS property only — no attribute.
37
+ type: object
38
+ required: true
39
+ dynamic: true
40
+ store:
41
+ description: |
42
+ The `SettingsStore` bound to `persona` (typically
43
+ `personaStore(persona)`). Toggle + content edits write through it
44
+ immediately; external `store.set()` calls (e.g. a live-apply probe)
45
+ re-render this pane via `store.subscribe`. JS property only.
46
+ type: object
47
+ required: true
48
+ dynamic: true
49
+
50
+ events:
51
+ entry-change:
52
+ description: An entry's `enabled` or `content` field was written through the store.
53
+ detail:
54
+ entryId:
55
+ type: string
56
+ field:
57
+ type: string
58
+ description: "'enabled' or 'content'."
59
+ value:
60
+ type: string
61
+ description: The new value (boolean for enabled, string for content).
62
+
63
+ slots: {}
64
+
65
+ states:
66
+ - name: idle
67
+ description: Default; renders `persona`'s entries grouped by kind.
68
+ - name: empty
69
+ description: No `persona` set.
70
+
71
+ traits: []
72
+
73
+ tokens:
74
+ --admin-settings-bg:
75
+ description: Panel background.
76
+ default: var(--a-bg)
77
+ --admin-settings-fg:
78
+ description: Primary foreground.
79
+ default: var(--a-fg)
80
+ --admin-settings-fg-muted:
81
+ description: Secondary foreground (description, empty state).
82
+ default: var(--a-fg-muted)
83
+ --admin-settings-px:
84
+ description: Horizontal padding.
85
+ default: var(--a-space-4)
86
+ --admin-settings-py:
87
+ description: Vertical padding.
88
+ default: var(--a-space-4)
89
+ --admin-settings-gap:
90
+ description: Vertical gap between header and sections.
91
+ default: var(--a-space-5)
92
+ --admin-settings-section-gap:
93
+ description: Gap between a section's heading and its entries.
94
+ default: var(--a-space-2)
95
+ --admin-settings-entry-gap:
96
+ description: Gap between sibling entries in a section.
97
+ default: var(--a-space-1)
98
+ --admin-settings-border:
99
+ description: Entry card border.
100
+ default: var(--a-border-subtle)
101
+ --admin-settings-radius:
102
+ description: Entry card border radius.
103
+ default: var(--a-radius-md)
104
+
105
+ a2ui:
106
+ rules:
107
+ - rule: |
108
+ Use <admin-settings-ui> for the canonical per-persona settings
109
+ pane. Set both `persona` and `store` as JS properties — neither
110
+ has an attribute form.
111
+ reason: One composite replaces a hand-rolled per-kind settings UI.
112
+ - rule: |
113
+ Never author per-kind branches around this composite (a
114
+ prompt-section-specific control, a skill-specific control, …) —
115
+ `KNOWN_KINDS` is display grouping only; a seventh kind must render
116
+ through the SAME entry row with zero new code.
117
+ reason: Law 7 — Entry is one generic primitive across all kinds.
118
+
119
+ anti_patterns:
120
+ - wrong: |
121
+ {"component": "AdminSettings"}
122
+ why: Renders "No persona selected." — `persona` and `store` must be set as JS properties before use.
123
+ fix: |
124
+ const el = document.querySelector('admin-settings-ui');
125
+ el.persona = myPersona;
126
+ el.store = personaStore(myPersona);
127
+
128
+ examples:
129
+ - name: bound-to-a-persona
130
+ description: Settings pane bound to a persona + its store (JS-driven; not expressible as static a2ui JSON since `store` is an opaque object).
131
+ a2ui: |
132
+ [{"id": "settings", "component": "AdminSettings"}]
133
+
134
+ keywords:
135
+ - admin
136
+ - settings
137
+ - persona
138
+ - entry
139
+ - store
140
+ - agent
141
+
142
+ synonyms:
143
+ settings:
144
+ - config
145
+ - preferences
146
+ - entries
147
+ persona:
148
+ - agent
149
+ - profile
150
+
151
+ related:
152
+ - AdminRoster
153
+ - AgentAdmin
@@ -0,0 +1,135 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://adiaui.dev/a2ui/v0_9/components/AgentAdmin.json",
4
+ "title": "AgentAdmin",
5
+ "description": "L4 admin composite (CHAT-HARNESS.md §Components row L4 + §Interfaces 5,\ngh#600 / WCH-5) — persona roster + settings pane + a chat pane, wired to\nthe store-swap law: a genuine persona switch (a new `SettingsStore`\nobject) resets the conversation, disposes open generative-UI surfaces,\nand mints a fresh session; re-selecting the already-active persona\n(same memoized store reference) resets nothing.\n\nBehavior-only orchestrator (mirrors `<chat-shell>`) — stamps no HTML of\nits own. Bespoke light-DOM children: `<admin-roster-ui>`,\n`<admin-settings-ui>`, `<chat-shell>` in EXTERNAL mode (no `proxy-url` —\nthis composite drives it via `wireAgentEvents`).\n\nTransport-blind (CHAT-HARNESS law 1): the `runTurn` JS property is\n`(session, text) → AsyncIterable<AgentEvent>`; packaged default\n`undefined` falls back to a deterministic, keyless stub that compiles\nthe active persona FRESH every turn (`readLivePersona` +\n`compilePersona`) and visibly cites what it read — the live-apply\nprobe: edit an entry's content in `<admin-settings-ui>`, the next\nturn's stub output cites the new text immediately, no propagation\nchannel.\n\nLives in `packages/web-modules/agent-admin/` alongside\n`<admin-roster-ui>` and `<admin-settings-ui>`.\n",
6
+ "type": "object",
7
+ "allOf": [
8
+ {
9
+ "$ref": "common_types.json#/$defs/ComponentCommon"
10
+ },
11
+ {
12
+ "$ref": "common_types.json#/$defs/CatalogComponentCommon"
13
+ }
14
+ ],
15
+ "properties": {
16
+ "active-id": {
17
+ "description": "The currently-active persona's id. Reflects; setting it programmatically\n(or via `<admin-roster-ui>`'s `persona-change`) runs `setActivePersona`.\n",
18
+ "$ref": "common_types.json#/$defs/DynamicString"
19
+ },
20
+ "component": {
21
+ "const": "AgentAdmin"
22
+ },
23
+ "personas": {
24
+ "description": "Full array of `Persona` objects (from `@adia-ai/persona`) this admin\nsurface manages. JSON-attribute or JS-property; the composite derives\n`<admin-roster-ui>`'s summary list from it automatically.\n",
25
+ "$ref": "common_types.json#/$defs/DynamicStringList"
26
+ },
27
+ "renderer": {
28
+ "description": "Optional generative-UI renderer (WCH-4's surface-host registry seam)\n— JS property only, host-injected (private `@genui/renderer` packages\nnever ship inside this composite). Omitted, surface events are safely\ndropped.\n",
29
+ "type": "object"
30
+ },
31
+ "runTurn": {
32
+ "description": "Optional transport override — `(session, text) => AsyncIterable<AgentEvent>`.\nJS property only. Omitted, the composite's own deterministic stub\ndrives every turn (see description).\n",
33
+ "type": "object"
34
+ }
35
+ },
36
+ "required": [
37
+ "component",
38
+ "personas"
39
+ ],
40
+ "unevaluatedProperties": false,
41
+ "x-adiaui": {
42
+ "anti_patterns": [
43
+ {
44
+ "fix": "<agent-admin-ui id=\"admin\">\n <admin-roster-ui></admin-roster-ui>\n <admin-settings-ui></admin-settings-ui>\n <chat-shell><chat-header></chat-header><chat-thread></chat-thread><chat-composer></chat-composer></chat-shell>\n</agent-admin-ui>\n",
45
+ "why": "Renders nothing without its bespoke children — `<agent-admin-ui>` needs `<admin-roster-ui>` + `<admin-settings-ui>` + `<chat-shell>` authored inside it.",
46
+ "wrong": "{\"component\": \"AgentAdmin\", \"personas\": [{\"id\": \"a\", \"label\": \"A\", \"seedVersion\": 1, \"entries\": []}]}\n"
47
+ }
48
+ ],
49
+ "category": "container",
50
+ "composes": [
51
+ "admin-roster-ui",
52
+ "admin-settings-ui",
53
+ "chat-shell"
54
+ ],
55
+ "events": {
56
+ "persona-activated": {
57
+ "description": "A persona finished activating (fired for both a genuine swap and a same-store reconnect).",
58
+ "detail": {
59
+ "personaId": {
60
+ "type": "string"
61
+ },
62
+ "reset": {
63
+ "description": "True for a genuine store swap (conversation/surfaces/session reset); false for a same-store reconnect.",
64
+ "type": "boolean"
65
+ }
66
+ }
67
+ },
68
+ "persona-imported": {
69
+ "description": "An imported persona (from `<admin-roster-ui>`'s `persona-import`) was appended and activated.",
70
+ "detail": {
71
+ "persona": {
72
+ "type": "object"
73
+ }
74
+ }
75
+ }
76
+ },
77
+ "examples": [
78
+ {
79
+ "description": "Full agent-admin composition (JS-driven; `personas` with full Entry arrays isn't practically hand-authored as static a2ui JSON).",
80
+ "a2ui": "[{\"id\": \"admin\", \"component\": \"AgentAdmin\", \"children\": [\n {\"id\": \"roster\", \"component\": \"AdminRoster\"},\n {\"id\": \"settings\", \"component\": \"AdminSettings\"},\n {\"id\": \"chat\", \"component\": \"ChatShell\"}\n]}]\n",
81
+ "name": "composed-shell"
82
+ }
83
+ ],
84
+ "keywords": [
85
+ "admin",
86
+ "agent",
87
+ "persona",
88
+ "roster",
89
+ "settings",
90
+ "chat",
91
+ "store-swap"
92
+ ],
93
+ "name": "UIAgentAdmin",
94
+ "related": [
95
+ "AdminRoster",
96
+ "AdminSettings",
97
+ "ChatShell"
98
+ ],
99
+ "slots": {},
100
+ "states": [
101
+ {
102
+ "description": "Default; one persona active, chat pane external-driven.",
103
+ "name": "idle"
104
+ }
105
+ ],
106
+ "status": "stable",
107
+ "synonyms": {
108
+ "admin": [
109
+ "agent-admin",
110
+ "persona-admin"
111
+ ],
112
+ "persona": [
113
+ "agent",
114
+ "profile"
115
+ ]
116
+ },
117
+ "tag": "agent-admin-ui",
118
+ "tokens": {
119
+ "--agent-admin-border": {
120
+ "description": "Column divider border.",
121
+ "default": "var(--a-border-subtle)"
122
+ },
123
+ "--agent-admin-roster-w": {
124
+ "description": "Roster column width.",
125
+ "default": "var(--a-space-12)"
126
+ },
127
+ "--agent-admin-settings-w": {
128
+ "description": "Settings column width.",
129
+ "default": "calc(var(--a-space-12) * 1.6)"
130
+ }
131
+ },
132
+ "traits": [],
133
+ "version": 1
134
+ }
135
+ }
@@ -0,0 +1,49 @@
1
+ /* ═══════════════════════════════════════════════════════════════
2
+ agent-admin-ui — Module CSS
3
+
4
+ Behavior-only orchestrator (mirrors chat-shell.css) — this file is
5
+ LAYOUT ONLY, reading bespoke light-DOM child tags directly
6
+ (<admin-roster-ui>, <admin-settings-ui>, <chat-shell>); each child
7
+ carries its own chrome via its own scoped stylesheet.
8
+ ═══════════════════════════════════════════════════════════════ */
9
+
10
+ @scope (agent-admin-ui) {
11
+
12
+ :where(:scope) {
13
+ --agent-admin-roster-w: var(--a-space-12);
14
+ --agent-admin-settings-w: calc(var(--a-space-12) * 1.6);
15
+ --agent-admin-border: var(--a-border-subtle);
16
+ }
17
+
18
+ :scope {
19
+ display: grid;
20
+ grid-template-columns: var(--agent-admin-roster-w) var(--agent-admin-settings-w) 1fr;
21
+ min-height: 0;
22
+ }
23
+
24
+ :scope > admin-roster-ui {
25
+ border-right: 1px solid var(--agent-admin-border);
26
+ overflow-y: auto;
27
+ }
28
+
29
+ :scope > admin-settings-ui {
30
+ border-right: 1px solid var(--agent-admin-border);
31
+ overflow-y: auto;
32
+ }
33
+
34
+ :scope > chat-shell {
35
+ min-width: 0;
36
+ }
37
+
38
+ @media (max-width: 900px) {
39
+ :scope {
40
+ grid-template-columns: 1fr;
41
+ grid-auto-rows: auto;
42
+ }
43
+ :scope > admin-roster-ui,
44
+ :scope > admin-settings-ui {
45
+ border-right: none;
46
+ border-bottom: 1px solid var(--agent-admin-border);
47
+ }
48
+ }
49
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * `<agent-admin-ui>` — L4 admin composite: persona roster + settings pane
3
+ * + a chat pane, wired to the store-swap law (CHAT-HARNESS.md
4
+ * §Components row L4 + §Interfaces 5, gh#600 / WCH-5).
5
+ *
6
+ * Hand-authored alongside `<name>.yaml`; regenerate when the props /
7
+ * events shape changes.
8
+ */
9
+
10
+ import { UIElement } from '../../../web-components/core/element.js';
11
+ import type { Persona, SettingsStore } from '@adia-ai/persona';
12
+ import type { AgentEvent } from '@adia-ai/agent';
13
+
14
+ export type AgentAdminRunTurn = (session: unknown, text: string) => AsyncIterable<AgentEvent>;
15
+
16
+ export interface AgentAdminPersonaActivatedDetail {
17
+ personaId: string;
18
+ reset: boolean;
19
+ }
20
+ export type AgentAdminPersonaActivatedEvent = CustomEvent<AgentAdminPersonaActivatedDetail>;
21
+
22
+ export interface AgentAdminPersonaImportedDetail {
23
+ persona: Persona;
24
+ }
25
+ export type AgentAdminPersonaImportedEvent = CustomEvent<AgentAdminPersonaImportedDetail>;
26
+
27
+ export class UIAgentAdmin extends UIElement {
28
+ personas: Persona[];
29
+ 'active-id': string;
30
+ renderer: unknown;
31
+ runTurn: AgentAdminRunTurn | null;
32
+ readonly activePersona: Persona | null;
33
+ readonly activeStore: SettingsStore | null;
34
+
35
+ setActivePersona(id: string): { genuineSwap: boolean };
36
+ send(text: string): Promise<void>;
37
+
38
+ addEventListener<K extends keyof HTMLElementEventMap>(
39
+ type: K,
40
+ listener: (this: UIAgentAdmin, ev: HTMLElementEventMap[K]) => unknown,
41
+ options?: boolean | AddEventListenerOptions,
42
+ ): void;
43
+ addEventListener(type: 'persona-activated', listener: (ev: AgentAdminPersonaActivatedEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
44
+ addEventListener(type: 'persona-imported', listener: (ev: AgentAdminPersonaImportedEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
45
+ }
@@ -0,0 +1,257 @@
1
+ /**
2
+ * <agent-admin-ui> — L4 admin composite: persona roster + settings pane +
3
+ * a chat pane, wired to the store-swap law (CHAT-HARNESS.md §Components
4
+ * row L4 + §Interfaces 5, gh#600 / WCH-5).
5
+ *
6
+ * Behavior-only orchestrator (mirrors <chat-shell>'s pattern): stamps no
7
+ * HTML of its own. Bespoke light-DOM children, author-supplied:
8
+ *
9
+ * <agent-admin-ui id="admin">
10
+ * <admin-roster-ui></admin-roster-ui>
11
+ * <admin-settings-ui></admin-settings-ui>
12
+ * <chat-shell>
13
+ * <chat-header><chat-status slot="status"></chat-status></chat-header>
14
+ * <chat-thread><chat-empty>...</chat-empty></chat-thread>
15
+ * <chat-composer><chat-input-ui></chat-input-ui></chat-composer>
16
+ * </chat-shell>
17
+ * </agent-admin-ui>
18
+ * <script>
19
+ * const admin = document.querySelector('#admin');
20
+ * admin.personas = [supportPersona, writerPersona]; // @adia-ai/persona Persona[]
21
+ * admin['active-id'] = 'support';
22
+ * </script>
23
+ *
24
+ * STORE-SWAP LAW (CHAT-HARNESS §Interfaces 5): one `SettingsStore` per
25
+ * persona id, memoized for the composite's lifetime. `setActivePersona`
26
+ * compares the resolved store's OBJECT REFERENCE against the currently
27
+ * bound one — a genuine reference change (switching to a different
28
+ * persona) resets the chat conversation (`chat-shell.clear()`), disposes
29
+ * every open generative-UI surface (`disposeAll()`), and mints a fresh
30
+ * session; re-selecting the ALREADY-active persona resolves to the SAME
31
+ * memoized store reference and resets nothing.
32
+ *
33
+ * TRANSPORT-BLIND (law 1): `runTurn(session, text) → AsyncIterable<AgentEvent>`
34
+ * is the only channel to a model. Packaged default is `undefined` — the
35
+ * composite falls back to a deterministic, keyless stub that compiles the
36
+ * ACTIVE persona fresh (via `readLivePersona` + `compilePersona`) on every
37
+ * call and visibly cites what it read, proving live-apply
38
+ * (fresh-read-at-turn-time, M-scope 3): edit an entry's content in
39
+ * `<admin-settings-ui>`, the very NEXT turn's stub output cites the new
40
+ * text — there is no propagation channel, no cache.
41
+ *
42
+ * `<admin-roster-ui>`'s surface-agnostic generative-UI hosting rides
43
+ * `chat/chat-surfaces.js`'s `createSurfaceRegistry` + `wireAgentEvents`
44
+ * (WCH-4 / gh#599) — the `renderer` prop is host-injected (private,
45
+ * unpublished `@genui/renderer` packages never ship inside this
46
+ * composite); omitted, surface events are safely dropped.
47
+ *
48
+ * Per ADR-0027 — a consumer imports <admin-roster-ui>, <admin-settings-ui>,
49
+ * and <chat-shell> (+ its own bespoke children) alongside this module.
50
+ */
51
+
52
+ import { UIElement } from '../../../web-components/core/element.js';
53
+ import { compilePersona, personaStore } from '@adia-ai/persona';
54
+ import { createSession, composeSystem } from '@adia-ai/agent';
55
+ import { createSurfaceRegistry, wireAgentEvents } from '../../chat/chat-surfaces/chat-surfaces.js';
56
+ import { readLivePersona } from '../live-persona.js';
57
+
58
+ const NOOP_REGISTRY = Object.freeze({
59
+ route: () => undefined,
60
+ close: () => undefined,
61
+ feed: () => undefined,
62
+ get: () => undefined,
63
+ disposeAll: () => {},
64
+ hosts: new Map(),
65
+ });
66
+
67
+ class UIAgentAdmin extends UIElement {
68
+ static properties = {
69
+ 'active-id': { type: String, default: '', reflect: true },
70
+ };
71
+
72
+ static template = () => null;
73
+
74
+ #personas = [];
75
+ #renderer = null;
76
+ #runTurn = null;
77
+ #roster = null;
78
+ #settings = null;
79
+ #chat = null;
80
+ #storesByPersonaId = new Map();
81
+ #activePersona = null;
82
+ #activeStore = null;
83
+ #session = null;
84
+ #surfaceRegistry = NOOP_REGISTRY;
85
+
86
+ get personas() { return this.#personas; }
87
+ set personas(value) {
88
+ this.#personas = Array.isArray(value) ? value : [];
89
+ this.#syncRosterSummaries();
90
+ this.#activateInitialIfNeeded();
91
+ }
92
+
93
+ get renderer() { return this.#renderer; }
94
+ set renderer(value) {
95
+ this.#renderer = value ?? null;
96
+ this.#surfaceRegistry = value ? createSurfaceRegistry({ renderer: value }) : NOOP_REGISTRY;
97
+ }
98
+
99
+ get runTurn() { return this.#runTurn; }
100
+ set runTurn(fn) { this.#runTurn = typeof fn === 'function' ? fn : null; }
101
+
102
+ /** The currently-bound persona (post live-apply overlay is computed
103
+ * fresh per turn, not cached here — see #defaultRunTurn). */
104
+ get activePersona() { return this.#activePersona; }
105
+ /** The currently-bound SettingsStore — same object across a same-id
106
+ * reconnect, a new object across a genuine persona switch. */
107
+ get activeStore() { return this.#activeStore; }
108
+
109
+ attributeChangedCallback(name, oldVal, newVal) {
110
+ if (name === 'personas') {
111
+ if (newVal == null) { this.personas = []; }
112
+ else {
113
+ try {
114
+ const parsed = JSON.parse(newVal);
115
+ this.personas = Array.isArray(parsed) ? parsed : [];
116
+ } catch { this.personas = []; }
117
+ }
118
+ return;
119
+ }
120
+ super.attributeChangedCallback(name, oldVal, newVal);
121
+ }
122
+
123
+ connected() {
124
+ this.#roster = this.querySelector('admin-roster-ui');
125
+ this.#settings = this.querySelector('admin-settings-ui');
126
+ this.#chat = this.querySelector('chat-shell');
127
+
128
+ this.#roster?.addEventListener('persona-change', this.#onPersonaChange);
129
+ this.#roster?.addEventListener('persona-import', this.#onPersonaImport);
130
+ this.#chat?.addEventListener('submit', this.#onChatSubmit);
131
+
132
+ this.#syncRosterSummaries();
133
+ this.#activateInitialIfNeeded();
134
+ }
135
+
136
+ #activateInitialIfNeeded() {
137
+ if (!this.isConnected || this.#activePersona) return;
138
+ const initialId = this['active-id'] || this.#personas[0]?.id || '';
139
+ if (initialId) this.setActivePersona(initialId);
140
+ }
141
+
142
+ disconnected() {
143
+ this.#roster?.removeEventListener('persona-change', this.#onPersonaChange);
144
+ this.#roster?.removeEventListener('persona-import', this.#onPersonaImport);
145
+ this.#chat?.removeEventListener('submit', this.#onChatSubmit);
146
+ }
147
+
148
+ // ── the store-swap law ──────────────────────────────────────────
149
+
150
+ /**
151
+ * @param {string} id
152
+ * @returns {{ genuineSwap: boolean }} — mostly for tests; the reset side
153
+ * effects already happened by the time this returns.
154
+ */
155
+ setActivePersona(id) {
156
+ const persona = this.#personas.find((p) => p.id === id);
157
+ if (!persona) return { genuineSwap: false };
158
+
159
+ const store = this.#storeFor(persona);
160
+ const genuineSwap = store !== this.#activeStore;
161
+
162
+ this.#activePersona = persona;
163
+ this.#activeStore = store;
164
+ this['active-id'] = id;
165
+ if (this.#roster) this.#roster['active-id'] = id;
166
+ if (this.#settings) {
167
+ this.#settings.persona = persona;
168
+ this.#settings.store = store;
169
+ }
170
+
171
+ if (genuineSwap) {
172
+ this.#chat?.clear?.();
173
+ this.#surfaceRegistry.disposeAll();
174
+ this.#session = createSession();
175
+ this.dispatchEvent(new CustomEvent('persona-activated', {
176
+ bubbles: true,
177
+ detail: { personaId: id, reset: true },
178
+ }));
179
+ } else {
180
+ this.dispatchEvent(new CustomEvent('persona-activated', {
181
+ bubbles: true,
182
+ detail: { personaId: id, reset: false },
183
+ }));
184
+ }
185
+ return { genuineSwap };
186
+ }
187
+
188
+ #storeFor(persona) {
189
+ let store = this.#storesByPersonaId.get(persona.id);
190
+ if (!store) {
191
+ store = personaStore(persona);
192
+ this.#storesByPersonaId.set(persona.id, store);
193
+ }
194
+ return store;
195
+ }
196
+
197
+ #syncRosterSummaries() {
198
+ if (!this.#roster) return;
199
+ this.#roster.personas = this.#personas.map((p) => ({ id: p.id, label: p.label, category: p.category }));
200
+ }
201
+
202
+ // ── turns ───────────────────────────────────────────────────────
203
+
204
+ async send(text) {
205
+ if (!this.#chat || !this.#activePersona || !text) return;
206
+ this.#chat.appendMessage({ role: 'user', content: text });
207
+ this.#chat.appendMessage({ role: 'assistant', content: '' });
208
+ this.#chat.startStreaming();
209
+ const statusEl = this.#chat.querySelector('chat-status');
210
+ const events = this.#runTurn
211
+ ? this.#runTurn(this.#session, text)
212
+ : this.#defaultRunTurn(text);
213
+ await wireAgentEvents(this.#chat, this.#surfaceRegistry, { events, statusEl });
214
+ }
215
+
216
+ /** Fresh-read-at-turn-time (law 7 / M-scope 3): every call re-reads the
217
+ * bound store and re-compiles — no cache, no propagation channel. */
218
+ async *#defaultRunTurn(text) {
219
+ const live = readLivePersona(this.#activePersona, this.#activeStore);
220
+ const compiled = compilePersona(live);
221
+ const { text: systemText } = composeSystem(compiled.prompt);
222
+ yield { type: 'progress', stage: 'started' };
223
+ yield {
224
+ type: 'text',
225
+ text: `[stub — persona "${live.label}" (${live.id})]\n${systemText}\n\nYou said: ${text}`,
226
+ };
227
+ yield { type: 'progress', stage: 'done' };
228
+ yield { type: 'done' };
229
+ }
230
+
231
+ // ── event delegation ────────────────────────────────────────────
232
+
233
+ #onPersonaChange = (e) => {
234
+ const id = e.detail?.personaId;
235
+ if (id) this.setActivePersona(id);
236
+ };
237
+
238
+ #onPersonaImport = (e) => {
239
+ const persona = e.detail?.persona;
240
+ if (!persona) return;
241
+ this.#personas = [...this.#personas, persona];
242
+ this.#syncRosterSummaries();
243
+ this.setActivePersona(persona.id);
244
+ this.dispatchEvent(new CustomEvent('persona-imported', { bubbles: true, detail: { persona } }));
245
+ };
246
+
247
+ #onChatSubmit = (e) => {
248
+ const text = e.detail?.text;
249
+ if (text) this.send(text);
250
+ };
251
+ }
252
+
253
+ if (!customElements.get('agent-admin-ui')) {
254
+ customElements.define('agent-admin-ui', UIAgentAdmin);
255
+ }
256
+
257
+ export { UIAgentAdmin };