@happyvertical/smrt-svelte 0.51.5 → 0.51.6

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.5",
3
+ "version": "0.51.6",
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.5",
125
- "@happyvertical/smrt-types": "0.51.5",
126
- "@happyvertical/smrt-ui": "0.51.5",
127
- "@happyvertical/smrt-web": "0.51.5",
124
+ "@happyvertical/smrt-languages": "0.51.6",
125
+ "@happyvertical/smrt-types": "0.51.6",
126
+ "@happyvertical/smrt-ui": "0.51.6",
127
+ "@happyvertical/smrt-web": "0.51.6",
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.5",
159
- "@happyvertical/smrt-chat": "0.51.5",
160
- "@happyvertical/smrt-content": "0.51.5",
161
- "@happyvertical/smrt-core": "0.51.5",
162
- "@happyvertical/smrt-reports": "0.51.5",
163
- "@happyvertical/smrt-scanner": "0.51.5",
164
- "@happyvertical/smrt-tenancy": "0.51.5",
165
- "@happyvertical/smrt-users": "0.51.5",
158
+ "@happyvertical/smrt-agents": "0.51.6",
159
+ "@happyvertical/smrt-chat": "0.51.6",
160
+ "@happyvertical/smrt-content": "0.51.6",
161
+ "@happyvertical/smrt-core": "0.51.6",
162
+ "@happyvertical/smrt-reports": "0.51.6",
163
+ "@happyvertical/smrt-scanner": "0.51.6",
164
+ "@happyvertical/smrt-tenancy": "0.51.6",
165
+ "@happyvertical/smrt-users": "0.51.6",
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",