@happyvertical/smrt-chat 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.
Files changed (47) hide show
  1. package/AGENTS.md +19 -0
  2. package/dist/index.js +1 -1
  3. package/dist/manifest.json +1 -1
  4. package/dist/smrt-knowledge.json +5 -5
  5. package/dist/svelte/components/agent/ToolCallDisplay.svelte +103 -7
  6. package/dist/svelte/components/agent/ToolCallDisplay.svelte.d.ts +18 -0
  7. package/dist/svelte/components/agent/ToolCallDisplay.svelte.d.ts.map +1 -1
  8. package/dist/svelte/components/agent/__tests__/ToolCallDisplay.test.js +100 -9
  9. package/dist/svelte/components/assistant/AssistantComposer.svelte +354 -0
  10. package/dist/svelte/components/assistant/AssistantComposer.svelte.d.ts +20 -0
  11. package/dist/svelte/components/assistant/AssistantComposer.svelte.d.ts.map +1 -0
  12. package/dist/svelte/components/assistant/AssistantDock.svelte +534 -0
  13. package/dist/svelte/components/assistant/AssistantDock.svelte.d.ts +30 -0
  14. package/dist/svelte/components/assistant/AssistantDock.svelte.d.ts.map +1 -0
  15. package/dist/svelte/components/assistant/AssistantThreadList.svelte +136 -0
  16. package/dist/svelte/components/assistant/AssistantThreadList.svelte.d.ts +15 -0
  17. package/dist/svelte/components/assistant/AssistantThreadList.svelte.d.ts.map +1 -0
  18. package/dist/svelte/components/assistant/__tests__/AssistantComposer.test.js +75 -0
  19. package/dist/svelte/components/assistant/__tests__/AssistantDock.test.js +395 -0
  20. package/dist/svelte/components/assistant/__tests__/AssistantThreadList.test.js +45 -0
  21. package/dist/svelte/components/assistant/__tests__/action-status.test.js +25 -0
  22. package/dist/svelte/components/assistant/__tests__/assistant-transport.test.js +253 -0
  23. package/dist/svelte/components/assistant/__tests__/attachment-href.test.js +31 -0
  24. package/dist/svelte/components/assistant/__tests__/create-assistant-dock-controller.test.js +2492 -0
  25. package/dist/svelte/components/assistant/action-status.d.ts +14 -0
  26. package/dist/svelte/components/assistant/action-status.d.ts.map +1 -0
  27. package/dist/svelte/components/assistant/action-status.js +7 -0
  28. package/dist/svelte/components/assistant/assistant-transport.d.ts +239 -0
  29. package/dist/svelte/components/assistant/assistant-transport.d.ts.map +1 -0
  30. package/dist/svelte/components/assistant/assistant-transport.js +306 -0
  31. package/dist/svelte/components/assistant/attachment-href.d.ts +23 -0
  32. package/dist/svelte/components/assistant/attachment-href.d.ts.map +1 -0
  33. package/dist/svelte/components/assistant/attachment-href.js +36 -0
  34. package/dist/svelte/components/assistant/create-assistant-dock-controller.svelte.d.ts +172 -0
  35. package/dist/svelte/components/assistant/create-assistant-dock-controller.svelte.d.ts.map +1 -0
  36. package/dist/svelte/components/assistant/create-assistant-dock-controller.svelte.js +0 -0
  37. package/dist/svelte/components/shared/ModelPicker.svelte +84 -0
  38. package/dist/svelte/components/shared/ModelPicker.svelte.d.ts +26 -0
  39. package/dist/svelte/components/shared/ModelPicker.svelte.d.ts.map +1 -0
  40. package/dist/svelte/components/shared/__tests__/ModelPicker.test.js +36 -0
  41. package/dist/svelte/i18n.d.ts +21 -0
  42. package/dist/svelte/i18n.d.ts.map +1 -1
  43. package/dist/svelte/i18n.js +25 -0
  44. package/dist/svelte/index.d.ts +7 -0
  45. package/dist/svelte/index.d.ts.map +1 -1
  46. package/dist/svelte/index.js +12 -0
  47. package/package.json +11 -11
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Maps an `AssistantActionState.status` to `ToolCallDisplayData['status']`
3
+ * (#2904 review, Copilot PR #2919 jAwua/jAwtb).
4
+ *
5
+ * Shared by `AssistantDock.svelte` and the demo route
6
+ * (`packages/chat/src/routes/previews/assistant-dock/+page.svelte`) so both
7
+ * status badges stay in sync — both previously fell through to `'success'`
8
+ * while `status === 'previewing'`, showing "Completed" for an action whose
9
+ * preview request (or, in the demo, apply request too) was still in flight
10
+ * or about to fail.
11
+ */
12
+ import type { AssistantActionState } from './create-assistant-dock-controller.svelte.js';
13
+ export declare function toolCallStatusForAction(status: AssistantActionState['status']): 'running' | 'success' | 'error';
14
+ //# sourceMappingURL=action-status.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"action-status.d.ts","sourceRoot":"","sources":["../../../../src/svelte/components/assistant/action-status.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,8CAA8C,CAAC;AAEzF,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,oBAAoB,CAAC,QAAQ,CAAC,GACrC,SAAS,GAAG,SAAS,GAAG,OAAO,CAIjC"}
@@ -0,0 +1,7 @@
1
+ export function toolCallStatusForAction(status) {
2
+ if (status === 'failed')
3
+ return 'error';
4
+ if (status === 'previewing' || status === 'applying')
5
+ return 'running';
6
+ return 'success';
7
+ }
@@ -0,0 +1,239 @@
1
+ /**
2
+ * AssistantDock transport contract (#2904).
3
+ *
4
+ * `AssistantTransport` is intentionally narrower than a full `ChatClientBackend`
5
+ * (`../../../client.js`): it covers only what a route-mounted assistant surface
6
+ * needs — list/create threads, load a thread's messages, send a message, and
7
+ * stage an attachment. `SmrtChatBackend.send` (`../../../client.js`) still owns
8
+ * the actual model-turn streaming/consumption contract; a `sendMessage` here is
9
+ * expected to internally drive that turn (consuming only `onDone`, per the
10
+ * binding decision that streaming UI is out of scope, tracked as #2908) and
11
+ * return once a reply exists or the request is still processing.
12
+ *
13
+ * Two implementations ship in this file:
14
+ * - `createInMemoryAssistantTransport` — deterministic, no network, for tests
15
+ * and the package dev workbench/demos.
16
+ * - `createSmrtAssistantTransport` — reads go through a host-supplied,
17
+ * MEMBER-scoped `readEndpoint` (Copilot PR #2919 jAwqo/jAwrQ/jAwvV: NEVER
18
+ * the generated `ChatThread`/`ChatMessage` list REST routes directly —
19
+ * those enforce only authentication + tenant scope, not the per-room
20
+ * membership check that lives in `ChatService.listRoomThreads`, and
21
+ * `threadId` isn't even a filter the generated list handler understands).
22
+ * `createThread`/`sendMessage`/`uploadAttachment` have no generated REST
23
+ * counterpart to call either way (no `create`/`post` route is exposed on
24
+ * `ChatThread`/`ChatMessage`, `api: { include: ['list', 'get'] }` —
25
+ * `../../../models/ChatThread.ts:16`, `../../../models/ChatMessage.ts:26`)
26
+ * and must be wired to an application's own `ChatService`-backed
27
+ * `writeEndpoint`. Both gaps are documented in `docs/assistant-dock.md`
28
+ * rather than silently faked. See the "smrt-generated-REST-backed
29
+ * transport" section below for the full read wire contract.
30
+ */
31
+ /**
32
+ * AssistantDock uses its own light thread/message shapes rather than the
33
+ * room-based `ChatThreadData`/`ChatMessageData` (`../types.js`): those UI
34
+ * types require room-scoped fields (e.g. `ChatThreadData.rootMessage`,
35
+ * `ChatMessageData.senderName`/`reactions`) that don't apply to a
36
+ * tenant-scoped, route-agnostic assistant thread. Field names below
37
+ * intentionally line up with `ChatThreadData`/`ChatMessageData` so a future
38
+ * adapter between the two is a narrow mapping, not a rewrite.
39
+ */
40
+ export interface AssistantThreadSummary {
41
+ id: string;
42
+ title: string;
43
+ isResolved: boolean;
44
+ messageCount: number;
45
+ lastMessageAt?: string | Date | null;
46
+ }
47
+ export interface AssistantMessage {
48
+ id: string;
49
+ threadId: string;
50
+ content: string;
51
+ role: 'user' | 'assistant' | 'system' | 'tool';
52
+ createdAt: string | Date;
53
+ attachments?: AssistantAttachmentRef[];
54
+ toolCallData?: unknown;
55
+ /** The send transport's `clientRequestId` that produced this message, when
56
+ * the transport echoes it back on the persisted row (#2904 review finding
57
+ * A). Lets the controller resolve a pending send by id instead of by
58
+ * `(threadId, content)` equality, which is ambiguous when a thread repeats
59
+ * the same text (e.g. "yes") across turns. Optional: a transport that
60
+ * doesn't persist/return this falls back to content matching. */
61
+ clientRequestId?: string;
62
+ }
63
+ export interface AssistantAttachmentRef {
64
+ id: string;
65
+ name: string;
66
+ contentType?: string;
67
+ size?: number;
68
+ url?: string;
69
+ }
70
+ /** A selectable model, matching `shared/ModelPicker.svelte`'s `ModelOption`. */
71
+ export interface ModelOption {
72
+ id: string;
73
+ label: string;
74
+ }
75
+ export interface AssistantSendMessageInput {
76
+ threadId: string;
77
+ content: string;
78
+ attachments?: AssistantAttachmentRef[];
79
+ /** Client-generated idempotency key for the send transport, distinct from
80
+ * any data-surface action `idempotencyKey` (binding decision, #2904). */
81
+ clientRequestId: string;
82
+ /** The model selected via ModelPicker, when the transport supports one. */
83
+ model?: string;
84
+ }
85
+ export interface AssistantSendMessageResult {
86
+ /** True when the assistant turn has not resolved yet and the caller should
87
+ * poll `loadMessages` for the reply (mirrors anytown's `payload.inProgress`,
88
+ * `PortalChatTool.svelte:391`). */
89
+ inProgress: boolean;
90
+ userMessage?: AssistantMessage;
91
+ assistantMessage?: AssistantMessage;
92
+ }
93
+ export interface AssistantTransport {
94
+ listThreads(): Promise<AssistantThreadSummary[]>;
95
+ createThread(title: string): Promise<AssistantThreadSummary>;
96
+ loadMessages(threadId: string): Promise<AssistantMessage[]>;
97
+ sendMessage(input: AssistantSendMessageInput): Promise<AssistantSendMessageResult>;
98
+ uploadAttachment(file: File): Promise<AssistantAttachmentRef>;
99
+ /** When present, `AssistantDock` renders `ModelPicker` in the composer
100
+ * header and threads the selected id through `sendMessage`'s `model`. A
101
+ * transport that has no model choice (e.g. a single fixed backend model)
102
+ * simply omits this method. */
103
+ listModels?(): Promise<ModelOption[]>;
104
+ }
105
+ export interface InMemoryAssistantTransportOptions {
106
+ /** Simulates a still-processing turn: the Nth send for a given thread
107
+ * returns `inProgress: true` before resolving on a later `loadMessages`. */
108
+ simulateInProgressOnce?: boolean;
109
+ /** Copilot PR #2919 jAwu8: `simulateInProgressOnce` alone previously left a
110
+ * turn `inProgress: true` FOREVER — it was never appended or scheduled to
111
+ * resolve, so this shipped transport could not exercise successful
112
+ * stale-send recovery (retry after the pending send goes 'stale'). When
113
+ * set, the pending turn's assistant reply is appended on the Nth
114
+ * `loadMessages()` call for that thread AFTER the send went in-progress
115
+ * (1 = the very next load). Omitted (the default) preserves the prior
116
+ * "never resolves on its own" behavior, which the existing stale-marking
117
+ * test relies on (polling must NOT resolve it before the staleness
118
+ * timeout fires). */
119
+ resolveInProgressAfterLoads?: number;
120
+ now?: () => number;
121
+ /** Deterministic id generator for tests; defaults to an incrementing counter. */
122
+ createId?: () => string;
123
+ respond?: (threadId: string, userMessage: AssistantMessage, model?: string) => AssistantMessage | null;
124
+ /** When set, `listModels()` resolves to this list, and `AssistantDock`
125
+ * renders `ModelPicker`. Omit to simulate a transport with no model
126
+ * choice. */
127
+ models?: ModelOption[];
128
+ }
129
+ export declare function createInMemoryAssistantTransport(options?: InMemoryAssistantTransportOptions): AssistantTransport;
130
+ /**
131
+ * Wire contract for `createSmrtAssistantTransport`'s reads (Copilot PR #2919
132
+ * jAwqo/jAwrQ/jAwvV). `readEndpoint` must be a host-supplied, MEMBER-scoped
133
+ * endpoint — never the generated `ChatThread`/`ChatMessage` REST list routes
134
+ * directly, which enforce only authentication + tenant scope, not the
135
+ * per-room/per-thread membership check that lives in
136
+ * `ChatService.listRoomThreads` (`packages/chat/src/services/ChatService.ts:1042`).
137
+ * Calling the generated list route straight from the browser would let any
138
+ * authenticated tenant member read every thread (and, for messages, every
139
+ * OTHER thread's messages too — `threadId` isn't even a filter the generated
140
+ * handler understands, it only parses `limit`/`offset`). See
141
+ * `docs/assistant-dock.md`'s "Transport" section for the full contract this
142
+ * endpoint must implement.
143
+ *
144
+ * Two calls:
145
+ * - `GET {readEndpoint}/threads` -> `{ items: <ThreadSummary wire shape>[] }`
146
+ * - `GET {readEndpoint}/threads/{id}/messages` -> `{ items: <Message wire shape>[] }`
147
+ *
148
+ * Documented wire shape (camelCase JSON, matching `AssistantThreadSummary`/
149
+ * `AssistantMessage` field names directly) — this is what a CONFORMING
150
+ * endpoint should send:
151
+ * ThreadSummary: `{ id, title, isResolved, messageCount, lastMessageAt? }`
152
+ * Message: `{ id, threadId, content, role, createdAt,
153
+ * attachments?: { name, url?, size? }[] }`, in CHRONOLOGICAL
154
+ * (oldest-first) order.
155
+ *
156
+ * `normalizeAssistantThreadSummary`/`normalizeAssistantMessage` below are
157
+ * DEFENSIVE, not a second contract: a host wrapping the raw generated model
158
+ * JSON (rather than writing a shape-converting endpoint) commonly hands back
159
+ * snake_case (`created_at`, `thread_id`), a `ChatMessage.attachments` value
160
+ * that is still the STORED JSON STRING with `filename` fields rather than a
161
+ * parsed array with `name`, and newest-first pagination order. Both
162
+ * normalizers tolerate that shape too so `loadMessages` still resolves
163
+ * in-progress replies and renders attachments/messages correctly either way.
164
+ */
165
+ export interface AssistantThreadSummaryWire {
166
+ id: string;
167
+ title: string;
168
+ isResolved?: boolean;
169
+ messageCount?: number;
170
+ lastMessageAt?: string | Date | null;
171
+ is_resolved?: boolean;
172
+ message_count?: number;
173
+ last_message_at?: string | Date | null;
174
+ }
175
+ export interface AssistantMessageWire {
176
+ id: string;
177
+ threadId?: string;
178
+ content?: string;
179
+ role?: AssistantMessage['role'];
180
+ createdAt?: string | Date;
181
+ attachments?: unknown;
182
+ clientRequestId?: string;
183
+ thread_id?: string;
184
+ created_at?: string | Date;
185
+ client_request_id?: string;
186
+ }
187
+ /** Normalizes one thread summary row from either the documented camelCase
188
+ * wire shape or a raw generated-model snake_case row. Exported for tests
189
+ * (Copilot PR #2919 jAwvV). */
190
+ export declare function normalizeAssistantThreadSummary(raw: AssistantThreadSummaryWire): AssistantThreadSummary;
191
+ /** Normalizes one message row from either the documented camelCase wire
192
+ * shape or a raw generated-model snake_case row, including its
193
+ * (possibly JSON-string) `attachments` field. Exported for tests (Copilot
194
+ * PR #2919 jAwvV). */
195
+ export declare function normalizeAssistantMessage(raw: AssistantMessageWire): AssistantMessage;
196
+ /** Sorts messages chronologically (oldest first) — the documented wire order
197
+ * this transport requires, but a raw generated-model list response is
198
+ * newest-first (Copilot PR #2919 jAwvV): without this, an in-progress
199
+ * reply's resolution check (which searches for an assistant/tool message
200
+ * AFTER the triggering user message) never finds it. Exported for tests. */
201
+ export declare function sortAssistantMessagesChronologically(messages: AssistantMessage[]): AssistantMessage[];
202
+ export interface SmrtAssistantTransportOptions {
203
+ /** Host-supplied, MEMBER-scoped read endpoint (Copilot PR #2919
204
+ * jAwqo/jAwrQ) — required. `GET {readEndpoint}/threads` and
205
+ * `GET {readEndpoint}/threads/{id}/messages`; see this file's
206
+ * "smrt-generated-REST-backed transport" section header for the full wire
207
+ * contract and why the generated `ChatThread`/`ChatMessage` list routes
208
+ * must never be called directly from here. `ChatService.listRoomThreads`
209
+ * (`packages/chat/src/services/ChatService.ts:1042`) is the server-side
210
+ * building block a host's endpoint implementation should call. */
211
+ readEndpoint: string;
212
+ token: string;
213
+ /** Required for sendMessage/createThread, which have no generated REST route
214
+ * (ChatThread/ChatMessage only expose `list`/`get` — see file header). Host
215
+ * apps must supply a `ChatService`-backed endpoint here; omitting it leaves
216
+ * `createThread`/`sendMessage` throwing a descriptive error rather than
217
+ * silently no-opping. */
218
+ writeEndpoint?: {
219
+ createThread: (title: string) => Promise<AssistantThreadSummary>;
220
+ sendMessage: (input: AssistantSendMessageInput) => Promise<AssistantSendMessageResult>;
221
+ uploadAttachment: (file: File) => Promise<AssistantAttachmentRef>;
222
+ };
223
+ /** Static model catalog; when supplied, `listModels()` resolves to it and
224
+ * `AssistantDock` renders `ModelPicker`. The `model` field of a `send` is
225
+ * threaded straight through to `writeEndpoint.sendMessage`'s `input`. */
226
+ models?: ModelOption[];
227
+ fetchImpl?: typeof fetch;
228
+ }
229
+ /**
230
+ * Reads go through a host-supplied, MEMBER-scoped `readEndpoint` (never the
231
+ * generated `ChatThread`/`ChatMessage` list routes directly — see this
232
+ * section's header comment above `SmrtAssistantTransportOptions`). Writes
233
+ * (`createThread`, `sendMessage`, `uploadAttachment`) require an explicit
234
+ * `writeEndpoint` supplied by the host application, since neither model
235
+ * exposes a generated `create` route (`api: { include: ['list', 'get'] }`,
236
+ * `../../../models/ChatThread.ts:16`, `../../../models/ChatMessage.ts:26`).
237
+ */
238
+ export declare function createSmrtAssistantTransport(options: SmrtAssistantTransportOptions): AssistantTransport;
239
+ //# sourceMappingURL=assistant-transport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assistant-transport.d.ts","sourceRoot":"","sources":["../../../../src/svelte/components/assistant/assistant-transport.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,306 @@
1
+ /**
2
+ * AssistantDock transport contract (#2904).
3
+ *
4
+ * `AssistantTransport` is intentionally narrower than a full `ChatClientBackend`
5
+ * (`../../../client.js`): it covers only what a route-mounted assistant surface
6
+ * needs — list/create threads, load a thread's messages, send a message, and
7
+ * stage an attachment. `SmrtChatBackend.send` (`../../../client.js`) still owns
8
+ * the actual model-turn streaming/consumption contract; a `sendMessage` here is
9
+ * expected to internally drive that turn (consuming only `onDone`, per the
10
+ * binding decision that streaming UI is out of scope, tracked as #2908) and
11
+ * return once a reply exists or the request is still processing.
12
+ *
13
+ * Two implementations ship in this file:
14
+ * - `createInMemoryAssistantTransport` — deterministic, no network, for tests
15
+ * and the package dev workbench/demos.
16
+ * - `createSmrtAssistantTransport` — reads go through a host-supplied,
17
+ * MEMBER-scoped `readEndpoint` (Copilot PR #2919 jAwqo/jAwrQ/jAwvV: NEVER
18
+ * the generated `ChatThread`/`ChatMessage` list REST routes directly —
19
+ * those enforce only authentication + tenant scope, not the per-room
20
+ * membership check that lives in `ChatService.listRoomThreads`, and
21
+ * `threadId` isn't even a filter the generated list handler understands).
22
+ * `createThread`/`sendMessage`/`uploadAttachment` have no generated REST
23
+ * counterpart to call either way (no `create`/`post` route is exposed on
24
+ * `ChatThread`/`ChatMessage`, `api: { include: ['list', 'get'] }` —
25
+ * `../../../models/ChatThread.ts:16`, `../../../models/ChatMessage.ts:26`)
26
+ * and must be wired to an application's own `ChatService`-backed
27
+ * `writeEndpoint`. Both gaps are documented in `docs/assistant-dock.md`
28
+ * rather than silently faked. See the "smrt-generated-REST-backed
29
+ * transport" section below for the full read wire contract.
30
+ */
31
+ export function createInMemoryAssistantTransport(options = {}) {
32
+ const now = options.now ?? (() => Date.now());
33
+ let counter = 0;
34
+ const createId = options.createId ?? (() => `im-${++counter}`);
35
+ const threads = new Map();
36
+ const messages = new Map();
37
+ const seenClientRequestIds = new Map();
38
+ const pendingOnce = new Set();
39
+ // Copilot PR #2919 jAwu8: tracks an in-progress turn awaiting its
40
+ // simulated resolution, keyed by threadId. `loadsRemaining` counts down on
41
+ // each loadMessages() call for that thread; the assistant reply is
42
+ // appended when it reaches 0.
43
+ const pendingTurns = new Map();
44
+ function requireThread(threadId) {
45
+ const thread = threads.get(threadId);
46
+ if (!thread) {
47
+ throw new Error(`AssistantDock: unknown thread "${threadId}"`);
48
+ }
49
+ return thread;
50
+ }
51
+ return {
52
+ async listThreads() {
53
+ return Array.from(threads.values());
54
+ },
55
+ ...(options.models
56
+ ? { listModels: async () => options.models }
57
+ : {}),
58
+ async createThread(title) {
59
+ const id = createId();
60
+ const thread = {
61
+ id,
62
+ title,
63
+ isResolved: false,
64
+ messageCount: 0,
65
+ lastMessageAt: new Date(now()),
66
+ };
67
+ threads.set(id, thread);
68
+ messages.set(id, []);
69
+ return thread;
70
+ },
71
+ async loadMessages(threadId) {
72
+ requireThread(threadId);
73
+ // Copilot PR #2919 jAwu8: resolve a pending simulated in-progress turn
74
+ // (only when `resolveInProgressAfterLoads` opted in — see its doc).
75
+ const pending = pendingTurns.get(threadId);
76
+ if (pending) {
77
+ pending.loadsRemaining -= 1;
78
+ if (pending.loadsRemaining <= 0) {
79
+ pendingTurns.delete(threadId);
80
+ const list = messages.get(threadId) ?? [];
81
+ const assistantMessage = options.respond?.(threadId, pending.userMessage, pending.model) ?? {
82
+ id: createId(),
83
+ threadId,
84
+ content: `echo: ${pending.userMessage.content}`,
85
+ role: 'assistant',
86
+ createdAt: new Date(now()),
87
+ };
88
+ list.push(assistantMessage);
89
+ messages.set(threadId, list);
90
+ const thread = threads.get(threadId);
91
+ if (thread)
92
+ thread.messageCount = (thread.messageCount ?? 0) + 1;
93
+ }
94
+ }
95
+ return [...(messages.get(threadId) ?? [])];
96
+ },
97
+ async sendMessage(input) {
98
+ const cached = seenClientRequestIds.get(input.clientRequestId);
99
+ if (cached) {
100
+ // Same retry semantics as PortalChatTool.svelte:304-316: a resend with
101
+ // the same clientRequestId must not create a second message.
102
+ return cached;
103
+ }
104
+ const thread = requireThread(input.threadId);
105
+ const list = messages.get(input.threadId) ?? [];
106
+ const userMessage = {
107
+ id: createId(),
108
+ threadId: input.threadId,
109
+ content: input.content,
110
+ role: 'user',
111
+ createdAt: new Date(now()),
112
+ attachments: input.attachments,
113
+ // Echoed back so the controller can resolve this exact send by id
114
+ // rather than by (threadId, content) equality (#2904 review F-A).
115
+ clientRequestId: input.clientRequestId,
116
+ };
117
+ list.push(userMessage);
118
+ messages.set(input.threadId, list);
119
+ thread.messageCount = (thread.messageCount ?? 0) + 1;
120
+ if (options.simulateInProgressOnce && !pendingOnce.has(input.threadId)) {
121
+ pendingOnce.add(input.threadId);
122
+ if (options.resolveInProgressAfterLoads !== undefined) {
123
+ pendingTurns.set(input.threadId, {
124
+ userMessage,
125
+ model: input.model,
126
+ loadsRemaining: options.resolveInProgressAfterLoads,
127
+ });
128
+ }
129
+ const result = {
130
+ inProgress: true,
131
+ userMessage,
132
+ };
133
+ seenClientRequestIds.set(input.clientRequestId, result);
134
+ return result;
135
+ }
136
+ const assistantMessage = options.respond?.(input.threadId, userMessage, input.model) ?? {
137
+ id: createId(),
138
+ threadId: input.threadId,
139
+ content: `echo: ${input.content}`,
140
+ role: 'assistant',
141
+ createdAt: new Date(now()),
142
+ };
143
+ list.push(assistantMessage);
144
+ thread.messageCount = (thread.messageCount ?? 0) + 1;
145
+ const result = {
146
+ inProgress: false,
147
+ userMessage,
148
+ assistantMessage,
149
+ };
150
+ seenClientRequestIds.set(input.clientRequestId, result);
151
+ return result;
152
+ },
153
+ async uploadAttachment(file) {
154
+ return {
155
+ id: createId(),
156
+ name: file.name,
157
+ contentType: file.type || undefined,
158
+ size: file.size,
159
+ };
160
+ },
161
+ };
162
+ }
163
+ /**
164
+ * Normalizes one attachment entry to `AssistantAttachmentRef`'s shape.
165
+ * Accepts the documented `{ name, url?, size? }` shape directly, or a raw
166
+ * generated-model `{ filename, ... }` entry (`filename` -> `name`).
167
+ */
168
+ function normalizeAssistantAttachment(raw, index) {
169
+ if (raw == null || typeof raw !== 'object')
170
+ return undefined;
171
+ const obj = raw;
172
+ const name = typeof obj.name === 'string'
173
+ ? obj.name
174
+ : typeof obj.filename === 'string'
175
+ ? obj.filename
176
+ : undefined;
177
+ if (!name)
178
+ return undefined;
179
+ return {
180
+ id: typeof obj.id === 'string' ? obj.id : `att-${index}`,
181
+ name,
182
+ url: typeof obj.url === 'string' ? obj.url : undefined,
183
+ size: typeof obj.size === 'number' ? obj.size : undefined,
184
+ contentType: typeof obj.contentType === 'string'
185
+ ? obj.contentType
186
+ : typeof obj.content_type === 'string'
187
+ ? obj.content_type
188
+ : undefined,
189
+ };
190
+ }
191
+ /**
192
+ * Normalizes a message's `attachments` field, which the documented wire
193
+ * shape sends as an array but `ChatMessage.attachments` (the raw generated
194
+ * model column) stores as a JSON-encoded STRING (Copilot PR #2919 jAwvV).
195
+ */
196
+ function normalizeAssistantAttachments(raw) {
197
+ let value = raw;
198
+ if (typeof value === 'string') {
199
+ if (value.length === 0)
200
+ return undefined;
201
+ try {
202
+ value = JSON.parse(value);
203
+ }
204
+ catch {
205
+ return undefined;
206
+ }
207
+ }
208
+ if (!Array.isArray(value))
209
+ return undefined;
210
+ const normalized = value
211
+ .map((entry, index) => normalizeAssistantAttachment(entry, index))
212
+ .filter((ref) => ref !== undefined);
213
+ return normalized.length > 0 ? normalized : undefined;
214
+ }
215
+ /** Normalizes one thread summary row from either the documented camelCase
216
+ * wire shape or a raw generated-model snake_case row. Exported for tests
217
+ * (Copilot PR #2919 jAwvV). */
218
+ export function normalizeAssistantThreadSummary(raw) {
219
+ return {
220
+ id: raw.id,
221
+ title: raw.title,
222
+ isResolved: raw.isResolved ?? raw.is_resolved ?? false,
223
+ messageCount: raw.messageCount ?? raw.message_count ?? 0,
224
+ lastMessageAt: raw.lastMessageAt ?? raw.last_message_at ?? null,
225
+ };
226
+ }
227
+ /** Normalizes one message row from either the documented camelCase wire
228
+ * shape or a raw generated-model snake_case row, including its
229
+ * (possibly JSON-string) `attachments` field. Exported for tests (Copilot
230
+ * PR #2919 jAwvV). */
231
+ export function normalizeAssistantMessage(raw) {
232
+ return {
233
+ id: raw.id,
234
+ threadId: raw.threadId ?? raw.thread_id ?? '',
235
+ content: raw.content ?? '',
236
+ role: raw.role ?? 'assistant',
237
+ createdAt: raw.createdAt ?? raw.created_at ?? new Date(),
238
+ attachments: normalizeAssistantAttachments(raw.attachments),
239
+ clientRequestId: raw.clientRequestId ?? raw.client_request_id,
240
+ };
241
+ }
242
+ function messageTimeValue(message) {
243
+ const t = new Date(message.createdAt).getTime();
244
+ return Number.isNaN(t) ? 0 : t;
245
+ }
246
+ /** Sorts messages chronologically (oldest first) — the documented wire order
247
+ * this transport requires, but a raw generated-model list response is
248
+ * newest-first (Copilot PR #2919 jAwvV): without this, an in-progress
249
+ * reply's resolution check (which searches for an assistant/tool message
250
+ * AFTER the triggering user message) never finds it. Exported for tests. */
251
+ export function sortAssistantMessagesChronologically(messages) {
252
+ return [...messages].sort((a, b) => messageTimeValue(a) - messageTimeValue(b));
253
+ }
254
+ async function getJson(fetchImpl, url, token) {
255
+ const response = await fetchImpl(url, {
256
+ headers: { Authorization: `Bearer ${token}`, accept: 'application/json' },
257
+ });
258
+ if (!response.ok) {
259
+ throw new Error(`AssistantDock transport: GET ${url} -> ${response.status}`);
260
+ }
261
+ return response.json();
262
+ }
263
+ /**
264
+ * Reads go through a host-supplied, MEMBER-scoped `readEndpoint` (never the
265
+ * generated `ChatThread`/`ChatMessage` list routes directly — see this
266
+ * section's header comment above `SmrtAssistantTransportOptions`). Writes
267
+ * (`createThread`, `sendMessage`, `uploadAttachment`) require an explicit
268
+ * `writeEndpoint` supplied by the host application, since neither model
269
+ * exposes a generated `create` route (`api: { include: ['list', 'get'] }`,
270
+ * `../../../models/ChatThread.ts:16`, `../../../models/ChatMessage.ts:26`).
271
+ */
272
+ export function createSmrtAssistantTransport(options) {
273
+ const fetchImpl = options.fetchImpl ?? fetch;
274
+ function requireWrite(key) {
275
+ const endpoint = options.writeEndpoint?.[key];
276
+ if (!endpoint) {
277
+ throw new Error(`AssistantDock: createSmrtAssistantTransport has no writeEndpoint.${String(key)} configured. ChatThread/ChatMessage do not expose a generated ` +
278
+ `"create" REST route (list/get only); supply a ChatService-backed ` +
279
+ `writeEndpoint to enable sending messages.`);
280
+ }
281
+ return endpoint;
282
+ }
283
+ return {
284
+ async listThreads() {
285
+ const page = await getJson(fetchImpl, `${options.readEndpoint}/threads`, options.token);
286
+ return (page.items ?? []).map(normalizeAssistantThreadSummary);
287
+ },
288
+ ...(options.models
289
+ ? { listModels: async () => options.models }
290
+ : {}),
291
+ async createThread(title) {
292
+ return requireWrite('createThread')(title);
293
+ },
294
+ async loadMessages(threadId) {
295
+ const page = await getJson(fetchImpl, `${options.readEndpoint}/threads/${encodeURIComponent(threadId)}/messages`, options.token);
296
+ const normalized = (page.items ?? []).map(normalizeAssistantMessage);
297
+ return sortAssistantMessagesChronologically(normalized);
298
+ },
299
+ async sendMessage(input) {
300
+ return requireWrite('sendMessage')(input);
301
+ },
302
+ async uploadAttachment(file) {
303
+ return requireWrite('uploadAttachment')(file);
304
+ },
305
+ };
306
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Safe attachment `href` resolution (#2904 review, cycle-3 second final F1
3
+ * addendum).
4
+ *
5
+ * `AssistantAttachmentRef.url` is transport-supplied data, not something
6
+ * this package controls or validates upstream — `AssistantDock.svelte` and
7
+ * the demo route both bound it directly to an `<a href>`, so a transport
8
+ * that echoes back (or a message history that contains) a `javascript:` or
9
+ * other non-http(s) URL would render a clickable attachment link capable of
10
+ * executing script in the viewer's context. Only `http:`/`https:` URLs are
11
+ * ever rendered as a link; anything else renders the attachment name as
12
+ * plain text instead.
13
+ */
14
+ /**
15
+ * Returns `url` unchanged when it resolves to an `http:`/`https:` URL
16
+ * (absolute or relative — a relative path resolves against the current
17
+ * origin, or `http://localhost` outside a browser, e.g. under SSR/test).
18
+ * Returns `undefined` for anything else (`javascript:`, `data:`, a
19
+ * malformed string, or no `url` at all), so callers can render plain text
20
+ * instead of an anchor.
21
+ */
22
+ export declare function safeAttachmentHref(url: string | undefined): string | undefined;
23
+ //# sourceMappingURL=attachment-href.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attachment-href.d.ts","sourceRoot":"","sources":["../../../../src/svelte/components/assistant/attachment-href.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,MAAM,GAAG,SAAS,GACtB,MAAM,GAAG,SAAS,CAYpB"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Safe attachment `href` resolution (#2904 review, cycle-3 second final F1
3
+ * addendum).
4
+ *
5
+ * `AssistantAttachmentRef.url` is transport-supplied data, not something
6
+ * this package controls or validates upstream — `AssistantDock.svelte` and
7
+ * the demo route both bound it directly to an `<a href>`, so a transport
8
+ * that echoes back (or a message history that contains) a `javascript:` or
9
+ * other non-http(s) URL would render a clickable attachment link capable of
10
+ * executing script in the viewer's context. Only `http:`/`https:` URLs are
11
+ * ever rendered as a link; anything else renders the attachment name as
12
+ * plain text instead.
13
+ */
14
+ /**
15
+ * Returns `url` unchanged when it resolves to an `http:`/`https:` URL
16
+ * (absolute or relative — a relative path resolves against the current
17
+ * origin, or `http://localhost` outside a browser, e.g. under SSR/test).
18
+ * Returns `undefined` for anything else (`javascript:`, `data:`, a
19
+ * malformed string, or no `url` at all), so callers can render plain text
20
+ * instead of an anchor.
21
+ */
22
+ export function safeAttachmentHref(url) {
23
+ if (!url)
24
+ return undefined;
25
+ let parsed;
26
+ try {
27
+ parsed = new URL(url, globalThis.location?.origin ?? 'http://localhost');
28
+ }
29
+ catch {
30
+ return undefined;
31
+ }
32
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
33
+ return undefined;
34
+ }
35
+ return url;
36
+ }