@happyvertical/smrt-svelte 0.51.5 → 0.51.7
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.
|
@@ -35,6 +35,31 @@ identity and action content belongs in `topLeftCorner` / `topRightCorner`,
|
|
|
35
35
|
wrapped in `ShellCorner side="left"` or `ShellCorner side="right"` so it follows
|
|
36
36
|
the corresponding shell track as that edge expands and collapses.
|
|
37
37
|
|
|
38
|
+
## AssistantDock recipe (#2904)
|
|
39
|
+
|
|
40
|
+
`ShellDockTool` also hosts `@happyvertical/smrt-chat/svelte`'s `AssistantDock`
|
|
41
|
+
— a route-aware assistant surface. This package has no runtime dependency on
|
|
42
|
+
`@happyvertical/smrt-chat` (only a devDependency, for tests); the composition
|
|
43
|
+
below is intentionally application code, not a `workspace/` export:
|
|
44
|
+
|
|
45
|
+
```svelte
|
|
46
|
+
<script lang="ts">
|
|
47
|
+
import { ShellDockTool } from '@happyvertical/smrt-svelte/workspace';
|
|
48
|
+
import { AssistantDock } from '@happyvertical/smrt-chat/svelte';
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<ShellDockTool id="assistant" label="Assistant" icon="bot">
|
|
52
|
+
{#snippet render()}
|
|
53
|
+
<AssistantDock {transport} {registry} />
|
|
54
|
+
{/snippet}
|
|
55
|
+
</ShellDockTool>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`registry` is the same `DataSurfaceRegistry` instance mounted routes register
|
|
59
|
+
their descriptors on. See
|
|
60
|
+
[`docs/assistant-dock.md`](../../../../../docs/assistant-dock.md) for the full
|
|
61
|
+
design.
|
|
62
|
+
|
|
38
63
|
## Migration
|
|
39
64
|
|
|
40
65
|
See [MIGRATION.md](./MIGRATION.md) for the first-generation workspace migration
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
/**
|
|
3
|
+
* AssistantDock conformance-style integration test (#2904).
|
|
4
|
+
*
|
|
5
|
+
* Mirrors `data-surface-conformance.integration.svelte.test.ts` in this
|
|
6
|
+
* directory: a real `DataSurfaceRegistry` and a real
|
|
7
|
+
* `createAssistantDockController`/`AssistantDock` are composed together;
|
|
8
|
+
* only the chat transport and the action client are in-process test doubles
|
|
9
|
+
* (`createInMemoryAssistantTransport`, and a thin `actionClient` adapter over
|
|
10
|
+
* the registry's own `execute()`), matching that file's stated scope of
|
|
11
|
+
* faking only the transport boundary.
|
|
12
|
+
*
|
|
13
|
+
* Scope note (disclosed deviation from the original test plan): the action
|
|
14
|
+
* preview/apply path here is driven directly through
|
|
15
|
+
* `createAssistantDockController` rather than by having a fake model reply
|
|
16
|
+
* trigger it through the rendered `AssistantDock` DOM — `AssistantDock` has
|
|
17
|
+
* no built-in "parse this assistant message as an action proposal" step (no
|
|
18
|
+
* such parsing is specified anywhere in #2904's binding decisions), so the
|
|
19
|
+
* host application is expected to call `controller.previewAction(...)` from
|
|
20
|
+
* its own message-rendering logic. This test still exercises the full
|
|
21
|
+
* dock (registry discovery, rendering, send/poll) through the DOM, and
|
|
22
|
+
* separately proves the action pathway (preview → confirm → apply → registry
|
|
23
|
+
* "command" event) end-to-end against the real registry.
|
|
24
|
+
*/
|
|
25
|
+
import { AssistantDock, createAssistantDockController, createInMemoryAssistantTransport, } from '@happyvertical/smrt-chat/svelte';
|
|
26
|
+
import { createDataSurfaceRegistry, } from '@happyvertical/smrt-ui/data-surface';
|
|
27
|
+
import { render, screen, waitFor } from '@testing-library/svelte';
|
|
28
|
+
import { describe, expect, it } from 'vitest';
|
|
29
|
+
const identity = {
|
|
30
|
+
surfaceId: 'assistant-dock-orders',
|
|
31
|
+
kind: 'table',
|
|
32
|
+
subject: { type: 'tenant', id: 'tenant-a' },
|
|
33
|
+
};
|
|
34
|
+
const descriptor = {
|
|
35
|
+
version: 1,
|
|
36
|
+
identity,
|
|
37
|
+
schemaVersion: 1,
|
|
38
|
+
label: 'Orders',
|
|
39
|
+
rowKey: 'id',
|
|
40
|
+
columns: [
|
|
41
|
+
{
|
|
42
|
+
id: 'id',
|
|
43
|
+
label: 'ID',
|
|
44
|
+
capabilities: ['read', 'project'],
|
|
45
|
+
role: 'row-key',
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
query: {
|
|
49
|
+
modes: ['rows'],
|
|
50
|
+
projectableColumnIds: ['id'],
|
|
51
|
+
searchableColumnIds: [],
|
|
52
|
+
filterableColumnIds: [],
|
|
53
|
+
sortableColumnIds: [],
|
|
54
|
+
},
|
|
55
|
+
actions: [
|
|
56
|
+
{
|
|
57
|
+
id: 'archive',
|
|
58
|
+
label: 'Archive',
|
|
59
|
+
selectionScopes: ['explicit-ids'],
|
|
60
|
+
requiresConfirmation: true,
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
controls: [{ id: 'data-surface.action.archive', label: 'Archive' }],
|
|
64
|
+
limits: { maxQueryRows: 100, maxQueryBytes: 100_000, maxSelectionSize: 100 },
|
|
65
|
+
};
|
|
66
|
+
function mountRegistryWithOneSurface() {
|
|
67
|
+
const registry = createDataSurfaceRegistry();
|
|
68
|
+
let revision = 1;
|
|
69
|
+
const state = { archived: false };
|
|
70
|
+
const unregister = registry.register({
|
|
71
|
+
descriptor,
|
|
72
|
+
getSnapshot: () => ({
|
|
73
|
+
version: 1,
|
|
74
|
+
descriptor,
|
|
75
|
+
revision,
|
|
76
|
+
state: { archived: state.archived },
|
|
77
|
+
}),
|
|
78
|
+
execute: async (command) => {
|
|
79
|
+
// A minimal real mutation: the "archive" action flips `archived` and
|
|
80
|
+
// bumps the snapshot revision, which is what makes the registry emit a
|
|
81
|
+
// 'command' event other mounted UI can react to.
|
|
82
|
+
state.archived = true;
|
|
83
|
+
revision += 1;
|
|
84
|
+
return {
|
|
85
|
+
version: 1,
|
|
86
|
+
commandId: command.commandId,
|
|
87
|
+
identity: command.identity,
|
|
88
|
+
ok: true,
|
|
89
|
+
revision,
|
|
90
|
+
};
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
return { registry, unregister, state: () => state.archived };
|
|
94
|
+
}
|
|
95
|
+
describe('AssistantDock integration (#2904)', () => {
|
|
96
|
+
it('fails closed and shows the no-surfaces notice with an empty registry', async () => {
|
|
97
|
+
const registry = createDataSurfaceRegistry();
|
|
98
|
+
const transport = createInMemoryAssistantTransport();
|
|
99
|
+
render(AssistantDock, { props: { transport, registry } });
|
|
100
|
+
await waitFor(() => {
|
|
101
|
+
expect(screen.getByText(/No data surfaces are mounted on this route/i)).toBeInTheDocument();
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
it('discovers a mounted surface via the registry and renders a sent conversation', async () => {
|
|
105
|
+
const { registry } = mountRegistryWithOneSurface();
|
|
106
|
+
const transport = createInMemoryAssistantTransport({
|
|
107
|
+
respond: (_threadId, userMessage) => ({
|
|
108
|
+
id: 'assistant-1',
|
|
109
|
+
threadId: userMessage.threadId,
|
|
110
|
+
content: 'Archived the order.',
|
|
111
|
+
role: 'assistant',
|
|
112
|
+
createdAt: new Date(),
|
|
113
|
+
}),
|
|
114
|
+
});
|
|
115
|
+
render(AssistantDock, { props: { transport, registry } });
|
|
116
|
+
await waitFor(() => {
|
|
117
|
+
expect(screen.queryByText(/No data surfaces are mounted on this route/i)).not.toBeInTheDocument();
|
|
118
|
+
});
|
|
119
|
+
const thread = await transport.createThread('Order question');
|
|
120
|
+
// Drive the same controller the component owns internally by exercising
|
|
121
|
+
// the transport directly (the composer's own send() path is covered at
|
|
122
|
+
// the unit level in packages/chat); here we assert the descriptor is
|
|
123
|
+
// visible to a controller built against this exact registry instance.
|
|
124
|
+
const controller = createAssistantDockController({ transport, registry });
|
|
125
|
+
await controller.openThread(thread.id);
|
|
126
|
+
// Cycle-3 second final finding 1: an attached file must round-trip onto
|
|
127
|
+
// the sent message, not just the message text — this is the surface
|
|
128
|
+
// that previously rendered nothing for message.attachments.
|
|
129
|
+
await controller.send('please archive this order', [
|
|
130
|
+
{ id: 'att-1', name: 'order-details.pdf' },
|
|
131
|
+
]);
|
|
132
|
+
await waitFor(() => {
|
|
133
|
+
expect(controller.messages.some((m) => m.content === 'Archived the order.')).toBe(true);
|
|
134
|
+
});
|
|
135
|
+
const sentMessage = controller.messages.find((m) => m.content === 'please archive this order');
|
|
136
|
+
expect(sentMessage?.attachments).toEqual([
|
|
137
|
+
{ id: 'att-1', name: 'order-details.pdf' },
|
|
138
|
+
]);
|
|
139
|
+
expect(controller.surfaces).toHaveLength(1);
|
|
140
|
+
expect(controller.surfaces[0].surfaceId).toBe('assistant-dock-orders');
|
|
141
|
+
controller.dispose();
|
|
142
|
+
});
|
|
143
|
+
it('drives preview → confirm → apply through the real registry and fires its "command" event', async () => {
|
|
144
|
+
const { registry, state } = mountRegistryWithOneSurface();
|
|
145
|
+
const events = [];
|
|
146
|
+
registry.subscribe((event) => events.push(event));
|
|
147
|
+
const actionClient = {
|
|
148
|
+
async preview(request) {
|
|
149
|
+
// A preview never mutates: assert the surface is untouched.
|
|
150
|
+
expect(state()).toBe(false);
|
|
151
|
+
return {
|
|
152
|
+
version: 1,
|
|
153
|
+
requestId: request.requestId,
|
|
154
|
+
identity: request.identity,
|
|
155
|
+
actionId: request.actionId,
|
|
156
|
+
phase: 'preview',
|
|
157
|
+
ok: true,
|
|
158
|
+
confirmationToken: 'token-1',
|
|
159
|
+
};
|
|
160
|
+
},
|
|
161
|
+
async apply(request) {
|
|
162
|
+
const result = await registry.execute({
|
|
163
|
+
version: 1,
|
|
164
|
+
commandId: request.requestId,
|
|
165
|
+
identity: request.identity,
|
|
166
|
+
expectedRevision: 1,
|
|
167
|
+
controlId: `data-surface.action.${request.actionId}`,
|
|
168
|
+
});
|
|
169
|
+
return {
|
|
170
|
+
version: 1,
|
|
171
|
+
requestId: request.requestId,
|
|
172
|
+
identity: request.identity,
|
|
173
|
+
actionId: request.actionId,
|
|
174
|
+
phase: 'apply',
|
|
175
|
+
ok: result.ok,
|
|
176
|
+
reason: result.ok ? undefined : result.reason,
|
|
177
|
+
};
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
const controller = createAssistantDockController({
|
|
181
|
+
transport: createInMemoryAssistantTransport(),
|
|
182
|
+
registry,
|
|
183
|
+
actionClient,
|
|
184
|
+
});
|
|
185
|
+
const requestId = 'archive-req-1';
|
|
186
|
+
await controller.previewAction({
|
|
187
|
+
version: 1,
|
|
188
|
+
requestId,
|
|
189
|
+
identity,
|
|
190
|
+
actionId: 'archive',
|
|
191
|
+
phase: 'preview',
|
|
192
|
+
selection: { scope: 'explicit-ids', rowIds: ['order-1'] },
|
|
193
|
+
});
|
|
194
|
+
expect(controller.actions.get(requestId)?.status).toBe('previewed');
|
|
195
|
+
expect(state()).toBe(false); // preview never mutated the surface
|
|
196
|
+
// applyAction now takes only requestId — it reuses the idempotencyKey
|
|
197
|
+
// minted once by previewAction (binding decision #2904, build phase 2).
|
|
198
|
+
await controller.applyAction(requestId);
|
|
199
|
+
expect(controller.actions.get(requestId)?.status).toBe('applied');
|
|
200
|
+
expect(state()).toBe(true);
|
|
201
|
+
expect(events.some((e) => e.type === 'command')).toBe(true);
|
|
202
|
+
controller.dispose();
|
|
203
|
+
});
|
|
204
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@happyvertical/smrt-svelte",
|
|
3
|
-
"version": "0.51.
|
|
3
|
+
"version": "0.51.7",
|
|
4
4
|
"smrtJsdoc": "strict",
|
|
5
5
|
"description": "Svelte 5 components for SMRT user management - auth, users, tenants, roles, permissions, groups",
|
|
6
6
|
"type": "module",
|
|
@@ -121,10 +121,10 @@
|
|
|
121
121
|
},
|
|
122
122
|
"dependencies": {
|
|
123
123
|
"@happyvertical/logger": "^0.89.11",
|
|
124
|
-
"@happyvertical/smrt-languages": "0.51.
|
|
125
|
-
"@happyvertical/smrt-types": "0.51.
|
|
126
|
-
"@happyvertical/smrt-ui": "0.51.
|
|
127
|
-
"@happyvertical/smrt-web": "0.51.
|
|
124
|
+
"@happyvertical/smrt-languages": "0.51.7",
|
|
125
|
+
"@happyvertical/smrt-types": "0.51.7",
|
|
126
|
+
"@happyvertical/smrt-ui": "0.51.7",
|
|
127
|
+
"@happyvertical/smrt-web": "0.51.7",
|
|
128
128
|
"@tanstack/db": "^0.6.14",
|
|
129
129
|
"@tanstack/svelte-db": "^0.1.91",
|
|
130
130
|
"esm-env": "^1.2.2"
|
|
@@ -155,14 +155,14 @@
|
|
|
155
155
|
}
|
|
156
156
|
},
|
|
157
157
|
"devDependencies": {
|
|
158
|
-
"@happyvertical/smrt-agents": "0.51.
|
|
159
|
-
"@happyvertical/smrt-chat": "0.51.
|
|
160
|
-
"@happyvertical/smrt-content": "0.51.
|
|
161
|
-
"@happyvertical/smrt-core": "0.51.
|
|
162
|
-
"@happyvertical/smrt-reports": "0.51.
|
|
163
|
-
"@happyvertical/smrt-scanner": "0.51.
|
|
164
|
-
"@happyvertical/smrt-tenancy": "0.51.
|
|
165
|
-
"@happyvertical/smrt-users": "0.51.
|
|
158
|
+
"@happyvertical/smrt-agents": "0.51.7",
|
|
159
|
+
"@happyvertical/smrt-chat": "0.51.7",
|
|
160
|
+
"@happyvertical/smrt-content": "0.51.7",
|
|
161
|
+
"@happyvertical/smrt-core": "0.51.7",
|
|
162
|
+
"@happyvertical/smrt-reports": "0.51.7",
|
|
163
|
+
"@happyvertical/smrt-scanner": "0.51.7",
|
|
164
|
+
"@happyvertical/smrt-tenancy": "0.51.7",
|
|
165
|
+
"@happyvertical/smrt-users": "0.51.7",
|
|
166
166
|
"@sveltejs/package": "^2.5.8",
|
|
167
167
|
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
|
168
168
|
"@testing-library/jest-dom": "^6.9.1",
|