@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.
- package/CHANGELOG.md +12 -0
- package/agent-admin/admin-roster/admin-roster.a2ui.json +171 -0
- package/agent-admin/admin-roster/admin-roster.css +112 -0
- package/agent-admin/admin-roster/admin-roster.d.ts +44 -0
- package/agent-admin/admin-roster/admin-roster.js +202 -0
- package/agent-admin/admin-roster/admin-roster.yaml +167 -0
- package/agent-admin/admin-settings/admin-settings.a2ui.json +156 -0
- package/agent-admin/admin-settings/admin-settings.css +114 -0
- package/agent-admin/admin-settings/admin-settings.d.ts +30 -0
- package/agent-admin/admin-settings/admin-settings.js +219 -0
- package/agent-admin/admin-settings/admin-settings.yaml +153 -0
- package/agent-admin/agent-admin/agent-admin.a2ui.json +135 -0
- package/agent-admin/agent-admin/agent-admin.css +49 -0
- package/agent-admin/agent-admin/agent-admin.d.ts +45 -0
- package/agent-admin/agent-admin/agent-admin.js +257 -0
- package/agent-admin/agent-admin/agent-admin.yaml +162 -0
- package/agent-admin/demo-seeds.js +95 -0
- package/agent-admin/index.js +13 -0
- package/agent-admin/live-persona.js +53 -0
- package/chat/chat-surfaces/chat-surfaces.d.ts +56 -0
- package/chat/chat-surfaces/chat-surfaces.js +242 -0
- package/chat/index.js +1 -0
- package/dist/everything.min.js +179 -108
- package/index.js +1 -0
- package/package.json +26 -5
- package/theme/theme-panel/theme-panel.js +31 -21
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# Edit this file; run `npm run build:components` to regenerate a2ui.json.
|
|
2
|
+
$schema: ../../../../scripts/schemas/component.yaml.schema.json
|
|
3
|
+
name: UIAgentAdmin
|
|
4
|
+
tag: agent-admin-ui
|
|
5
|
+
status: stable
|
|
6
|
+
component: AgentAdmin
|
|
7
|
+
category: container
|
|
8
|
+
version: 1
|
|
9
|
+
description: |
|
|
10
|
+
L4 admin composite (CHAT-HARNESS.md §Components row L4 + §Interfaces 5,
|
|
11
|
+
gh#600 / WCH-5) — persona roster + settings pane + a chat pane, wired to
|
|
12
|
+
the store-swap law: a genuine persona switch (a new `SettingsStore`
|
|
13
|
+
object) resets the conversation, disposes open generative-UI surfaces,
|
|
14
|
+
and mints a fresh session; re-selecting the already-active persona
|
|
15
|
+
(same memoized store reference) resets nothing.
|
|
16
|
+
|
|
17
|
+
Behavior-only orchestrator (mirrors `<chat-shell>`) — stamps no HTML of
|
|
18
|
+
its own. Bespoke light-DOM children: `<admin-roster-ui>`,
|
|
19
|
+
`<admin-settings-ui>`, `<chat-shell>` in EXTERNAL mode (no `proxy-url` —
|
|
20
|
+
this composite drives it via `wireAgentEvents`).
|
|
21
|
+
|
|
22
|
+
Transport-blind (CHAT-HARNESS law 1): the `runTurn` JS property is
|
|
23
|
+
`(session, text) → AsyncIterable<AgentEvent>`; packaged default
|
|
24
|
+
`undefined` falls back to a deterministic, keyless stub that compiles
|
|
25
|
+
the active persona FRESH every turn (`readLivePersona` +
|
|
26
|
+
`compilePersona`) and visibly cites what it read — the live-apply
|
|
27
|
+
probe: edit an entry's content in `<admin-settings-ui>`, the next
|
|
28
|
+
turn's stub output cites the new text immediately, no propagation
|
|
29
|
+
channel.
|
|
30
|
+
|
|
31
|
+
Lives in `packages/web-modules/agent-admin/` alongside
|
|
32
|
+
`<admin-roster-ui>` and `<admin-settings-ui>`.
|
|
33
|
+
|
|
34
|
+
composes:
|
|
35
|
+
- admin-roster-ui
|
|
36
|
+
- admin-settings-ui
|
|
37
|
+
- chat-shell
|
|
38
|
+
|
|
39
|
+
props:
|
|
40
|
+
personas:
|
|
41
|
+
description: |
|
|
42
|
+
Full array of `Persona` objects (from `@adia-ai/persona`) this admin
|
|
43
|
+
surface manages. JSON-attribute or JS-property; the composite derives
|
|
44
|
+
`<admin-roster-ui>`'s summary list from it automatically.
|
|
45
|
+
type: array
|
|
46
|
+
default: []
|
|
47
|
+
required: true
|
|
48
|
+
dynamic: true
|
|
49
|
+
active-id:
|
|
50
|
+
description: |
|
|
51
|
+
The currently-active persona's id. Reflects; setting it programmatically
|
|
52
|
+
(or via `<admin-roster-ui>`'s `persona-change`) runs `setActivePersona`.
|
|
53
|
+
type: string
|
|
54
|
+
default: ""
|
|
55
|
+
reflect: true
|
|
56
|
+
dynamic: true
|
|
57
|
+
renderer:
|
|
58
|
+
description: |
|
|
59
|
+
Optional generative-UI renderer (WCH-4's surface-host registry seam)
|
|
60
|
+
— JS property only, host-injected (private `@genui/renderer` packages
|
|
61
|
+
never ship inside this composite). Omitted, surface events are safely
|
|
62
|
+
dropped.
|
|
63
|
+
type: object
|
|
64
|
+
dynamic: true
|
|
65
|
+
runTurn:
|
|
66
|
+
description: |
|
|
67
|
+
Optional transport override — `(session, text) => AsyncIterable<AgentEvent>`.
|
|
68
|
+
JS property only. Omitted, the composite's own deterministic stub
|
|
69
|
+
drives every turn (see description).
|
|
70
|
+
type: object
|
|
71
|
+
dynamic: true
|
|
72
|
+
|
|
73
|
+
events:
|
|
74
|
+
persona-activated:
|
|
75
|
+
description: A persona finished activating (fired for both a genuine swap and a same-store reconnect).
|
|
76
|
+
detail:
|
|
77
|
+
personaId:
|
|
78
|
+
type: string
|
|
79
|
+
reset:
|
|
80
|
+
type: boolean
|
|
81
|
+
description: True for a genuine store swap (conversation/surfaces/session reset); false for a same-store reconnect.
|
|
82
|
+
persona-imported:
|
|
83
|
+
description: An imported persona (from `<admin-roster-ui>`'s `persona-import`) was appended and activated.
|
|
84
|
+
detail:
|
|
85
|
+
persona:
|
|
86
|
+
type: object
|
|
87
|
+
|
|
88
|
+
slots: {}
|
|
89
|
+
|
|
90
|
+
states:
|
|
91
|
+
- name: idle
|
|
92
|
+
description: Default; one persona active, chat pane external-driven.
|
|
93
|
+
|
|
94
|
+
traits: []
|
|
95
|
+
|
|
96
|
+
tokens:
|
|
97
|
+
--agent-admin-roster-w:
|
|
98
|
+
description: Roster column width.
|
|
99
|
+
default: var(--a-space-12)
|
|
100
|
+
--agent-admin-settings-w:
|
|
101
|
+
description: Settings column width.
|
|
102
|
+
default: calc(var(--a-space-12) * 1.6)
|
|
103
|
+
--agent-admin-border:
|
|
104
|
+
description: Column divider border.
|
|
105
|
+
default: var(--a-border-subtle)
|
|
106
|
+
|
|
107
|
+
a2ui:
|
|
108
|
+
rules:
|
|
109
|
+
- rule: |
|
|
110
|
+
Compose `<agent-admin-ui>` with bespoke `<admin-roster-ui>` +
|
|
111
|
+
`<admin-settings-ui>` + `<chat-shell>` children (chat-shell WITHOUT
|
|
112
|
+
`proxy-url` — external mode; this composite drives it). Set
|
|
113
|
+
`personas` as a JS property.
|
|
114
|
+
reason: Behavior-only orchestrator; the consumer authors the bespoke children.
|
|
115
|
+
- rule: |
|
|
116
|
+
Never bind `<chat-shell proxy-url>` inside `<agent-admin-ui>` — the
|
|
117
|
+
composite drives the chat pane itself via `wireAgentEvents`; a
|
|
118
|
+
`proxy-url` would double-drive turns.
|
|
119
|
+
reason: One transport per turn (CHAT-HARNESS law 1).
|
|
120
|
+
|
|
121
|
+
anti_patterns:
|
|
122
|
+
- wrong: |
|
|
123
|
+
{"component": "AgentAdmin", "personas": [{"id": "a", "label": "A", "seedVersion": 1, "entries": []}]}
|
|
124
|
+
why: Renders nothing without its bespoke children — `<agent-admin-ui>` needs `<admin-roster-ui>` + `<admin-settings-ui>` + `<chat-shell>` authored inside it.
|
|
125
|
+
fix: |
|
|
126
|
+
<agent-admin-ui id="admin">
|
|
127
|
+
<admin-roster-ui></admin-roster-ui>
|
|
128
|
+
<admin-settings-ui></admin-settings-ui>
|
|
129
|
+
<chat-shell><chat-header></chat-header><chat-thread></chat-thread><chat-composer></chat-composer></chat-shell>
|
|
130
|
+
</agent-admin-ui>
|
|
131
|
+
|
|
132
|
+
examples:
|
|
133
|
+
- name: composed-shell
|
|
134
|
+
description: Full agent-admin composition (JS-driven; `personas` with full Entry arrays isn't practically hand-authored as static a2ui JSON).
|
|
135
|
+
a2ui: |
|
|
136
|
+
[{"id": "admin", "component": "AgentAdmin", "children": [
|
|
137
|
+
{"id": "roster", "component": "AdminRoster"},
|
|
138
|
+
{"id": "settings", "component": "AdminSettings"},
|
|
139
|
+
{"id": "chat", "component": "ChatShell"}
|
|
140
|
+
]}]
|
|
141
|
+
|
|
142
|
+
keywords:
|
|
143
|
+
- admin
|
|
144
|
+
- agent
|
|
145
|
+
- persona
|
|
146
|
+
- roster
|
|
147
|
+
- settings
|
|
148
|
+
- chat
|
|
149
|
+
- store-swap
|
|
150
|
+
|
|
151
|
+
synonyms:
|
|
152
|
+
admin:
|
|
153
|
+
- agent-admin
|
|
154
|
+
- persona-admin
|
|
155
|
+
persona:
|
|
156
|
+
- agent
|
|
157
|
+
- profile
|
|
158
|
+
|
|
159
|
+
related:
|
|
160
|
+
- AdminRoster
|
|
161
|
+
- AdminSettings
|
|
162
|
+
- ChatShell
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* demo-seeds — the three personas `playgrounds/agent-admin` ships with.
|
|
3
|
+
*
|
|
4
|
+
* Shared between the playground (imported at a browser-absolute path) and
|
|
5
|
+
* `demo-seeds.test.js` (modality-neutral lint, CHAT-HARNESS law 4 — a
|
|
6
|
+
* persona's prose surfaces INTENT only, never dialect specifics: "A2UI",
|
|
7
|
+
* "envelope", component tag names like "chart-ui" are all disallowed).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { definePersona, seedEntries } from '@adia-ai/persona';
|
|
11
|
+
|
|
12
|
+
export const supportPersona = definePersona({
|
|
13
|
+
id: 'support',
|
|
14
|
+
label: 'Support agent',
|
|
15
|
+
category: 'Customer',
|
|
16
|
+
seedVersion: 1,
|
|
17
|
+
entries: seedEntries([
|
|
18
|
+
{
|
|
19
|
+
id: 'identity',
|
|
20
|
+
kind: 'prompt-section',
|
|
21
|
+
label: 'Identity',
|
|
22
|
+
description: 'Who this agent is.',
|
|
23
|
+
content: 'You are a customer support agent for a software product. You help people solve problems with their account and their orders.',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: 'tone',
|
|
27
|
+
kind: 'prompt-section',
|
|
28
|
+
label: 'Tone',
|
|
29
|
+
description: 'How this agent talks.',
|
|
30
|
+
content: 'Be concise, patient, and friendly. Confirm what the person needs before offering a fix.',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: 'lookup-order',
|
|
34
|
+
kind: 'tool',
|
|
35
|
+
label: 'Look up an order',
|
|
36
|
+
description: 'Find an order by its confirmation number.',
|
|
37
|
+
content: 'Given a confirmation number, return the order status and its items.',
|
|
38
|
+
},
|
|
39
|
+
]),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
export const writerPersona = definePersona({
|
|
43
|
+
id: 'writer',
|
|
44
|
+
label: 'Writer',
|
|
45
|
+
category: 'Content',
|
|
46
|
+
seedVersion: 1,
|
|
47
|
+
entries: seedEntries([
|
|
48
|
+
{
|
|
49
|
+
id: 'identity',
|
|
50
|
+
kind: 'prompt-section',
|
|
51
|
+
label: 'Identity',
|
|
52
|
+
description: 'Who this agent is.',
|
|
53
|
+
content: 'You are a writing assistant. You help people draft, tighten, and proofread short pieces of text.',
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: 'style',
|
|
57
|
+
kind: 'prompt-section',
|
|
58
|
+
label: 'Style',
|
|
59
|
+
description: 'The house voice.',
|
|
60
|
+
content: 'Prefer short sentences and plain words. Avoid jargon unless the person uses it first.',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: 'editing-checklist',
|
|
64
|
+
kind: 'skill',
|
|
65
|
+
label: 'Editing checklist',
|
|
66
|
+
description: 'A repeatable pass over a draft.',
|
|
67
|
+
content: 'Check for a clear opening line, one idea per paragraph, and a direct closing.',
|
|
68
|
+
},
|
|
69
|
+
]),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
export const researcherPersona = definePersona({
|
|
73
|
+
id: 'researcher',
|
|
74
|
+
label: 'Researcher',
|
|
75
|
+
category: 'Content',
|
|
76
|
+
seedVersion: 1,
|
|
77
|
+
entries: seedEntries([
|
|
78
|
+
{
|
|
79
|
+
id: 'identity',
|
|
80
|
+
kind: 'prompt-section',
|
|
81
|
+
label: 'Identity',
|
|
82
|
+
description: 'Who this agent is.',
|
|
83
|
+
content: 'You are a research assistant. You gather facts, summarize sources, and flag open questions.',
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: 'method',
|
|
87
|
+
kind: 'workflow',
|
|
88
|
+
label: 'Research method',
|
|
89
|
+
description: 'How this agent works through a question.',
|
|
90
|
+
content: 'Break the question into parts, look for a source for each part, then summarize what is and is not settled.',
|
|
91
|
+
},
|
|
92
|
+
]),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
export const DEMO_PERSONAS = [supportPersona, writerPersona, researcherPersona];
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @adia-ai/web-modules/agent-admin — cluster barrel.
|
|
3
|
+
*
|
|
4
|
+
* L4 admin composite family (CHAT-HARNESS.md §Components row L4, gh#600 /
|
|
5
|
+
* WCH-5) over `@adia-ai/persona`'s L2 store contract: `<admin-roster-ui>`
|
|
6
|
+
* (persona switch, exactly-one-active), `<admin-settings-ui>` (settings
|
|
7
|
+
* pane over one bound store), `<agent-admin-ui>` (the composition —
|
|
8
|
+
* roster + settings + a chat pane, store-swap law, transport-blind).
|
|
9
|
+
*/
|
|
10
|
+
export { UIAdminRoster } from './admin-roster/admin-roster.js';
|
|
11
|
+
export { UIAdminSettings } from './admin-settings/admin-settings.js';
|
|
12
|
+
export { UIAgentAdmin } from './agent-admin/agent-admin.js';
|
|
13
|
+
export { readLivePersona, entryStoreKey } from './live-persona.js';
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* live-persona — the L4 fresh-read overlay (CHAT-HARNESS law 7: "Live-apply
|
|
3
|
+
* is by fresh read at turn time — no propagation channel").
|
|
4
|
+
*
|
|
5
|
+
* `admin-settings-ui` writes edits straight through a per-persona
|
|
6
|
+
* `SettingsStore` (`@adia-ai/persona`'s `personaStore`) — it never mutates
|
|
7
|
+
* the `Persona` object in memory. `readLivePersona` is the ONE place that
|
|
8
|
+
* re-reads the store and overlays it onto the base persona's entries; the
|
|
9
|
+
* agent-admin composition calls it fresh, immediately before every
|
|
10
|
+
* `compilePersona()`, so the very next turn always compiles whatever is
|
|
11
|
+
* currently in the store — there is no cache, no subscription forwarding
|
|
12
|
+
* content into a second copy of the persona.
|
|
13
|
+
*
|
|
14
|
+
* Key convention MIRRORS `@adia-ai/persona`'s `store.ts` (`entry.<id>.<field>`
|
|
15
|
+
* for `enabled` / `order`) — store.ts doesn't export that helper, so it's
|
|
16
|
+
* kept in lock-step here by hand. `content` is a THIRD field this module
|
|
17
|
+
* owns (persona/store.ts never seeds or reads it) — entry authoring
|
|
18
|
+
* (M-scope 2) persists edited prompt-section/skill/etc. text under the
|
|
19
|
+
* same `entry.<id>.content` key.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** @param {string} entryId @param {'enabled'|'order'|'content'} field */
|
|
23
|
+
export function entryStoreKey(entryId, field) {
|
|
24
|
+
return `entry.${entryId}.${field}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Overlay `store`'s current values onto `persona.entries`, entry by entry.
|
|
29
|
+
* A field the store has never seen falls back to the persona's own seed
|
|
30
|
+
* value — same precedence `personaStore` documents for enabled/order.
|
|
31
|
+
* Never mutates `persona`; always returns a fresh object + fresh entries
|
|
32
|
+
* array so callers can't accidentally hold a stale reference across turns.
|
|
33
|
+
*
|
|
34
|
+
* @param {import('@adia-ai/persona').Persona} persona
|
|
35
|
+
* @param {import('@adia-ai/persona').SettingsStore} store
|
|
36
|
+
* @returns {import('@adia-ai/persona').Persona}
|
|
37
|
+
*/
|
|
38
|
+
export function readLivePersona(persona, store) {
|
|
39
|
+
if (!persona) return persona;
|
|
40
|
+
if (!store) return persona;
|
|
41
|
+
const entries = persona.entries.map((entry) => {
|
|
42
|
+
const enabled = store.get(entryStoreKey(entry.id, 'enabled'));
|
|
43
|
+
const order = store.get(entryStoreKey(entry.id, 'order'));
|
|
44
|
+
const content = store.get(entryStoreKey(entry.id, 'content'));
|
|
45
|
+
return {
|
|
46
|
+
...entry,
|
|
47
|
+
enabled: typeof enabled === 'boolean' ? enabled : entry.enabled,
|
|
48
|
+
order: typeof order === 'number' ? order : entry.order,
|
|
49
|
+
content: typeof content === 'string' ? content : entry.content,
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
return { ...persona, entries };
|
|
53
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The L3 surface-host registry over genui-system `attachSurface`
|
|
3
|
+
* (CHAT-HARNESS.md §Interfaces 4).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** Renderer seam — only the subset of `@genui/renderer`'s `Renderer` this
|
|
7
|
+
* module calls. The real `Renderer` type satisfies this structurally. */
|
|
8
|
+
export interface SurfaceRegistryRenderer {
|
|
9
|
+
handleFrame(raw: string): void;
|
|
10
|
+
readonly surfaces: ReadonlyMap<string, unknown>;
|
|
11
|
+
attachSurface(surfaceId: string, root: Element): { detach(): void; [key: string]: unknown };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface SurfaceHost {
|
|
15
|
+
surfaceId: string;
|
|
16
|
+
root: Element;
|
|
17
|
+
handle: { detach(): void; [key: string]: unknown };
|
|
18
|
+
bubbleEl: Element;
|
|
19
|
+
status: 'open' | 'closed';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface SurfaceRegistry {
|
|
23
|
+
/** Fresh id ⇒ mount into `turnBubbleEl`; known id ⇒ return the original host. */
|
|
24
|
+
route(surfaceId: string, turnBubbleEl: Element): SurfaceHost;
|
|
25
|
+
/** Protocol-level delete + non-removable "Closed." annotation. Idempotent. */
|
|
26
|
+
close(surfaceId: string): SurfaceHost | undefined;
|
|
27
|
+
/** Apply one wire envelope line for `surfaceId`, routing/closing as needed. */
|
|
28
|
+
feed(surfaceId: string, line: string, turnBubbleEl: Element | null): SurfaceHost | undefined;
|
|
29
|
+
get(surfaceId: string): SurfaceHost | undefined;
|
|
30
|
+
disposeAll(): void;
|
|
31
|
+
readonly hosts: ReadonlyMap<string, SurfaceHost>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function createSurfaceRegistry(opts: {
|
|
35
|
+
renderer: SurfaceRegistryRenderer;
|
|
36
|
+
doc?: Document;
|
|
37
|
+
}): SurfaceRegistry;
|
|
38
|
+
|
|
39
|
+
/** CHAT-HARNESS law 3 — code-owned progress labels, never model text. */
|
|
40
|
+
export const PROGRESS_LABELS: Record<string, string>;
|
|
41
|
+
|
|
42
|
+
export function wireAgentEvents(
|
|
43
|
+
chatShell: Element & {
|
|
44
|
+
appendChunk?(text: string): void;
|
|
45
|
+
appendMessage?(msg: { role: string; content?: string; render?: boolean }): Element;
|
|
46
|
+
startStreaming?(): void;
|
|
47
|
+
stopStreaming?(): void;
|
|
48
|
+
streaming?: boolean;
|
|
49
|
+
},
|
|
50
|
+
registry: SurfaceRegistry,
|
|
51
|
+
opts: {
|
|
52
|
+
events: AsyncIterable<{ type: string; [key: string]: unknown }>;
|
|
53
|
+
statusEl?: { textContent: string } | null;
|
|
54
|
+
isProgressStage?: (stage: unknown) => boolean;
|
|
55
|
+
},
|
|
56
|
+
): Promise<void>;
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* chat-surfaces — the L3 surface-host registry over genui-system
|
|
3
|
+
* `attachSurface` (CHAT-HARNESS.md §Interfaces 4, gh#599 / WCH-4).
|
|
4
|
+
*
|
|
5
|
+
* `<chat-thread>` bookkeeping: `Map<surfaceId, host>`.
|
|
6
|
+
* - A FRESH surfaceId mounts a new host inline in the CREATING turn's
|
|
7
|
+
* bubble (the `chatShell.appendMessage()` element passed in as
|
|
8
|
+
* `turnBubbleEl`).
|
|
9
|
+
* - A KNOWN surfaceId (open OR closed) always routes back to that
|
|
10
|
+
* ORIGINAL host — persistent surface identity across turns. A later
|
|
11
|
+
* turn that updates surface S renders into S's original bubble, it
|
|
12
|
+
* never mounts a second one.
|
|
13
|
+
* - `close(surfaceId)` is a PROTOCOL-level delete (a real `deleteSurface`
|
|
14
|
+
* envelope through the renderer — the same path a server-sent delete
|
|
15
|
+
* rides, `SurfaceStore#deleteSurface`, idempotent) plus a visible,
|
|
16
|
+
* NON-REMOVABLE "Closed." annotation appended to the original bubble.
|
|
17
|
+
* History is never silently erased — the annotation stays even if the
|
|
18
|
+
* bubble is re-rendered around it (callers must APPEND, never replace).
|
|
19
|
+
*
|
|
20
|
+
* Dependency-injected renderer (CHAT-HARNESS law 1 — one-method transport,
|
|
21
|
+
* packaged seam undefined): this module never imports `@genui/renderer`
|
|
22
|
+
* itself. `@genui/*` are private, unpublished workspace packages — a real
|
|
23
|
+
* `@adia-ai/web-modules` npm consumer could never resolve a bare `@genui/…`
|
|
24
|
+
* specifier. The HOST (a demo page, a consumer app) builds the renderer
|
|
25
|
+
* (`createRenderer` + its own `WidgetAdapter`) and hands it in; this module
|
|
26
|
+
* only ever calls the five methods `Renderer` exposes.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const CLOSED_ANNOTATION_TEXT = 'Closed.';
|
|
30
|
+
const PROTOCOL_VERSION = 'v1.0';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {object} opts
|
|
34
|
+
* @param {{handleFrame(raw: string): void, surfaces: ReadonlyMap<string, unknown>, attachSurface(surfaceId: string, root: Element): {detach(): void}}} opts.renderer
|
|
35
|
+
* @param {Document} [opts.doc]
|
|
36
|
+
*/
|
|
37
|
+
export function createSurfaceRegistry({ renderer, doc = document }) {
|
|
38
|
+
if (!renderer) throw new Error('createSurfaceRegistry: opts.renderer is required');
|
|
39
|
+
|
|
40
|
+
/** @type {Map<string, {surfaceId: string, root: Element, handle: object, bubbleEl: Element, status: 'open'|'closed'}>} */
|
|
41
|
+
const hosts = new Map();
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Fresh id ⇒ mount a new host into `turnBubbleEl`. Known id (open or
|
|
45
|
+
* closed) ⇒ return the ORIGINAL host untouched — never re-mounts, never
|
|
46
|
+
* moves to a different bubble.
|
|
47
|
+
*/
|
|
48
|
+
function route(surfaceId, turnBubbleEl) {
|
|
49
|
+
const existing = hosts.get(surfaceId);
|
|
50
|
+
if (existing) return existing;
|
|
51
|
+
|
|
52
|
+
if (!turnBubbleEl) {
|
|
53
|
+
throw new Error(`createSurfaceRegistry.route: no bubble element for fresh surfaceId "${surfaceId}"`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Island posture: the root is the ONLY thing this registry adds to the
|
|
57
|
+
// bubble — no class/style/chrome on the bubble itself.
|
|
58
|
+
const root = doc.createElement('div');
|
|
59
|
+
root.setAttribute('data-surface-root', surfaceId);
|
|
60
|
+
turnBubbleEl.appendChild(root);
|
|
61
|
+
|
|
62
|
+
const handle = renderer.attachSurface(surfaceId, root);
|
|
63
|
+
const host = { surfaceId, root, handle, bubbleEl: turnBubbleEl, status: 'open' };
|
|
64
|
+
hosts.set(surfaceId, host);
|
|
65
|
+
return host;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function annotateClosed(host) {
|
|
69
|
+
const note = doc.createElement('div');
|
|
70
|
+
note.setAttribute('data-surface-closed', host.surfaceId);
|
|
71
|
+
// No removal affordance is ever wired to this element — it is
|
|
72
|
+
// permanent, visible history (CHAT-HARNESS §Interfaces 4).
|
|
73
|
+
note.textContent = CLOSED_ANNOTATION_TEXT;
|
|
74
|
+
host.bubbleEl.appendChild(note);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Protocol-level delete: rides `deleteSurface` through the renderer
|
|
79
|
+
* (idempotent — deleting an already-closed or unknown id is a no-op on
|
|
80
|
+
* the renderer side), marks the host closed, and appends the Closed
|
|
81
|
+
* annotation. Returns the host record (or undefined for an id this
|
|
82
|
+
* registry never routed).
|
|
83
|
+
*/
|
|
84
|
+
function close(surfaceId) {
|
|
85
|
+
const host = hosts.get(surfaceId);
|
|
86
|
+
if (!host) return undefined;
|
|
87
|
+
if (host.status === 'closed') return host;
|
|
88
|
+
|
|
89
|
+
renderer.handleFrame(JSON.stringify({
|
|
90
|
+
version: PROTOCOL_VERSION,
|
|
91
|
+
deleteSurface: { surfaceId },
|
|
92
|
+
}));
|
|
93
|
+
host.status = 'closed';
|
|
94
|
+
annotateClosed(host);
|
|
95
|
+
return host;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Feed one 'surface' AgentEvent line for `surfaceId` into the renderer,
|
|
100
|
+
* routing/mounting/closing as needed. `line` is one raw wire envelope
|
|
101
|
+
* (createSurface / updateComponents / updateDataModel / deleteSurface).
|
|
102
|
+
*
|
|
103
|
+
* - `deleteSurface` line ⇒ apply it, then close() bookkeeping (no
|
|
104
|
+
* second envelope is sent — the line already IS the delete).
|
|
105
|
+
* - anything else ⇒ apply it, then route() if the surface came up
|
|
106
|
+
* live. A fresh id whose createSurface line failed validation never
|
|
107
|
+
* becomes live in `renderer.surfaces`, so nothing is mounted for it.
|
|
108
|
+
*/
|
|
109
|
+
function feed(surfaceId, line, turnBubbleEl) {
|
|
110
|
+
let envelope = null;
|
|
111
|
+
try {
|
|
112
|
+
envelope = JSON.parse(line);
|
|
113
|
+
} catch {
|
|
114
|
+
// Malformed line — let the renderer's own diagnostic path handle it.
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (envelope && Object.hasOwn(envelope, 'deleteSurface')) {
|
|
118
|
+
renderer.handleFrame(line);
|
|
119
|
+
const host = hosts.get(surfaceId);
|
|
120
|
+
if (host && host.status !== 'closed') {
|
|
121
|
+
host.status = 'closed';
|
|
122
|
+
annotateClosed(host);
|
|
123
|
+
}
|
|
124
|
+
return host;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
renderer.handleFrame(line);
|
|
128
|
+
if (renderer.surfaces.has(surfaceId)) {
|
|
129
|
+
return route(surfaceId, turnBubbleEl);
|
|
130
|
+
}
|
|
131
|
+
return hosts.get(surfaceId);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function get(surfaceId) {
|
|
135
|
+
return hosts.get(surfaceId);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Page/component teardown — detaches every open host's rendering from
|
|
139
|
+
* its Root (island cleanup) without sending any protocol delete; this
|
|
140
|
+
* is NOT the same operation as `close()` and leaves no annotation. */
|
|
141
|
+
function disposeAll() {
|
|
142
|
+
for (const host of hosts.values()) {
|
|
143
|
+
host.handle.detach();
|
|
144
|
+
}
|
|
145
|
+
hosts.clear();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return { route, close, feed, get, disposeAll, hosts };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** CHAT-HARNESS law 3 — labels rendered from a CODE-OWNED table, never
|
|
152
|
+
* model text. Keyed on `@adia-ai/agent`'s closed `ProgressStage` vocabulary
|
|
153
|
+
* (`PROGRESS_STAGES`); a stage this table has no entry for renders nothing
|
|
154
|
+
* rather than a raw enum value. */
|
|
155
|
+
export const PROGRESS_LABELS = {
|
|
156
|
+
sent: 'Sending…',
|
|
157
|
+
started: 'Thinking…',
|
|
158
|
+
reasoning: 'Reasoning…',
|
|
159
|
+
content: 'Writing…',
|
|
160
|
+
validating: 'Validating…',
|
|
161
|
+
retry: 'Retrying…',
|
|
162
|
+
tool: 'Using a tool…',
|
|
163
|
+
done: 'Done',
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Consumes ONE `AsyncIterable<AgentEvent>` for the turn already opened on
|
|
168
|
+
* `chatShell` (the caller has already appended the user message and an
|
|
169
|
+
* empty assistant draft, and called `chatShell.startStreaming()` — mirrors
|
|
170
|
+
* `playgrounds/agent-chat`'s existing submit handler). Transport-blind
|
|
171
|
+
* (CHAT-HARNESS law 1): this function never fetches, never opens a
|
|
172
|
+
* socket — it only folds the event stream it's handed.
|
|
173
|
+
*
|
|
174
|
+
* @param {import('../chat-shell/chat-shell.js').ChatShell} chatShell
|
|
175
|
+
* @param {ReturnType<typeof createSurfaceRegistry>} registry
|
|
176
|
+
* @param {object} opts
|
|
177
|
+
* @param {AsyncIterable<object>} opts.events — the AgentEvent stream
|
|
178
|
+
* @param {{textContent: string}} [opts.statusEl] — rendered from PROGRESS_LABELS
|
|
179
|
+
* @param {(stage: unknown) => boolean} [opts.isProgressStage] — closed-vocabulary
|
|
180
|
+
* guard; defaults to accepting any key PROGRESS_LABELS has (out-of-vocab drop).
|
|
181
|
+
*/
|
|
182
|
+
export async function wireAgentEvents(chatShell, registry, opts) {
|
|
183
|
+
const { events, statusEl = null, isProgressStage = (s) => Object.hasOwn(PROGRESS_LABELS, s) } = opts ?? {};
|
|
184
|
+
if (!events) throw new Error('wireAgentEvents: opts.events (AsyncIterable<AgentEvent>) is required');
|
|
185
|
+
|
|
186
|
+
const currentBubble = () => {
|
|
187
|
+
const thread = chatShell.querySelector?.('chat-thread');
|
|
188
|
+
return thread?.querySelector('[data-role="assistant"]:last-child [data-bubble]') ?? null;
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
for await (const event of events) {
|
|
192
|
+
switch (event.type) {
|
|
193
|
+
case 'text':
|
|
194
|
+
chatShell.appendChunk?.(event.text);
|
|
195
|
+
break;
|
|
196
|
+
|
|
197
|
+
case 'progress': {
|
|
198
|
+
if (!isProgressStage(event.stage)) break; // closed-vocab drop (law 3)
|
|
199
|
+
if (statusEl) statusEl.textContent = PROGRESS_LABELS[event.stage] ?? '';
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
case 'surface': {
|
|
204
|
+
const bubble = currentBubble();
|
|
205
|
+
registry.feed(event.surfaceId, event.line, bubble);
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
case 'tool_use':
|
|
210
|
+
chatShell.appendMessage?.({
|
|
211
|
+
role: 'assistant',
|
|
212
|
+
content: `🔧 \`${event.name}(${JSON.stringify(event.input)})\``,
|
|
213
|
+
render: true,
|
|
214
|
+
});
|
|
215
|
+
chatShell.appendMessage?.({ role: 'assistant', content: '' });
|
|
216
|
+
break;
|
|
217
|
+
|
|
218
|
+
case 'tool_result':
|
|
219
|
+
chatShell.appendMessage?.({
|
|
220
|
+
role: event.isError ? 'error' : 'assistant',
|
|
221
|
+
content: `↩ \`${event.name}\` → ${String(event.output).slice(0, 300)}`,
|
|
222
|
+
render: true,
|
|
223
|
+
});
|
|
224
|
+
chatShell.appendMessage?.({ role: 'assistant', content: '' });
|
|
225
|
+
break;
|
|
226
|
+
|
|
227
|
+
case 'error':
|
|
228
|
+
chatShell.appendMessage?.({ role: 'error', content: event.error?.message ?? String(event.error) });
|
|
229
|
+
chatShell.stopStreaming?.();
|
|
230
|
+
break;
|
|
231
|
+
|
|
232
|
+
case 'done':
|
|
233
|
+
chatShell.stopStreaming?.();
|
|
234
|
+
break;
|
|
235
|
+
|
|
236
|
+
default:
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (chatShell.streaming) chatShell.stopStreaming?.();
|
|
242
|
+
}
|
package/chat/index.js
CHANGED
|
@@ -2,3 +2,4 @@ export { ChatShell } from './chat-shell/chat-shell.js';
|
|
|
2
2
|
export { ChatThread } from './chat-thread/chat-thread.js';
|
|
3
3
|
export { ChatComposer } from './chat-composer/chat-composer.js';
|
|
4
4
|
export { ChatSidebar } from './chat-sidebar/chat-sidebar.js';
|
|
5
|
+
export { createSurfaceRegistry, wireAgentEvents, PROGRESS_LABELS } from './chat-surfaces/chat-surfaces.js';
|