@classic-homes/agent-chat 0.1.3 → 0.2.0

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 (64) hide show
  1. package/README.md +40 -0
  2. package/dist/components/AgentChat.svelte +65 -39
  3. package/dist/components/AgentChat.svelte.d.ts +2 -0
  4. package/dist/components/AgentChat.svelte.d.ts.map +1 -1
  5. package/dist/components/ArtifactPanel.svelte +79 -96
  6. package/dist/components/ArtifactPanel.svelte.d.ts +2 -0
  7. package/dist/components/ArtifactPanel.svelte.d.ts.map +1 -1
  8. package/dist/components/ChatHeader.svelte +122 -0
  9. package/dist/components/ChatHeader.svelte.d.ts +27 -0
  10. package/dist/components/ChatHeader.svelte.d.ts.map +1 -0
  11. package/dist/components/Composer.svelte +78 -121
  12. package/dist/components/Composer.svelte.d.ts.map +1 -1
  13. package/dist/components/ConversationDialogs.svelte +134 -0
  14. package/dist/components/ConversationDialogs.svelte.d.ts +25 -0
  15. package/dist/components/ConversationDialogs.svelte.d.ts.map +1 -0
  16. package/dist/components/ConversationSidebar.svelte +135 -114
  17. package/dist/components/ConversationSidebar.svelte.d.ts +8 -2
  18. package/dist/components/ConversationSidebar.svelte.d.ts.map +1 -1
  19. package/dist/components/Markdown.svelte +48 -0
  20. package/dist/components/Markdown.svelte.d.ts +16 -0
  21. package/dist/components/Markdown.svelte.d.ts.map +1 -0
  22. package/dist/components/MessageItem.svelte +27 -116
  23. package/dist/components/MessageItem.svelte.d.ts.map +1 -1
  24. package/dist/components/MessageList.svelte +23 -34
  25. package/dist/components/ModeBanner.svelte +7 -13
  26. package/dist/components/ModeBanner.svelte.d.ts.map +1 -1
  27. package/dist/components/SandboxFrame.svelte +43 -0
  28. package/dist/components/SandboxFrame.svelte.d.ts +13 -0
  29. package/dist/components/SandboxFrame.svelte.d.ts.map +1 -0
  30. package/dist/components/ToolTimeline.svelte +17 -68
  31. package/dist/components/ToolTimeline.svelte.d.ts.map +1 -1
  32. package/dist/components/UiFrame.svelte +9 -55
  33. package/dist/components/UiFrame.svelte.d.ts.map +1 -1
  34. package/dist/components/conversation-chrome.svelte.test.d.ts +2 -0
  35. package/dist/components/conversation-chrome.svelte.test.d.ts.map +1 -0
  36. package/dist/components/conversation-chrome.svelte.test.js +127 -0
  37. package/dist/components/markdown-prose.svelte.test.d.ts +2 -0
  38. package/dist/components/markdown-prose.svelte.test.d.ts.map +1 -0
  39. package/dist/components/markdown-prose.svelte.test.js +26 -0
  40. package/dist/composer-keys.d.ts +11 -0
  41. package/dist/composer-keys.d.ts.map +1 -0
  42. package/dist/composer-keys.js +10 -0
  43. package/dist/composer-keys.test.d.ts +2 -0
  44. package/dist/composer-keys.test.d.ts.map +1 -0
  45. package/dist/composer-keys.test.js +16 -0
  46. package/dist/index.d.ts +2 -0
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +2 -0
  49. package/dist/reducer.d.ts +10 -0
  50. package/dist/reducer.d.ts.map +1 -1
  51. package/dist/reducer.js +19 -1
  52. package/dist/reducer.test.js +29 -1
  53. package/dist/session.svelte.d.ts +6 -0
  54. package/dist/session.svelte.d.ts.map +1 -1
  55. package/dist/session.svelte.js +15 -1
  56. package/dist/ui-frame-protocol.d.ts +2 -2
  57. package/dist/ui-frame-protocol.js +2 -2
  58. package/dist/ui-frame-relay.d.ts +16 -0
  59. package/dist/ui-frame-relay.d.ts.map +1 -0
  60. package/dist/ui-frame-relay.js +22 -0
  61. package/dist/ui-frame-relay.test.d.ts +2 -0
  62. package/dist/ui-frame-relay.test.d.ts.map +1 -0
  63. package/dist/ui-frame-relay.test.js +36 -0
  64. package/package.json +8 -5
package/dist/reducer.d.ts CHANGED
@@ -90,6 +90,16 @@ export declare function newUserMessage(requestId: string, text: string, options?
90
90
  export declare function describeContext(context: DocumentContext): string;
91
91
  /** Fold one stream event into the assistant message it belongs to. Returns a new object. */
92
92
  export declare function applyEvent(message: AssistantMessage, event: ChatEvent): AssistantMessage;
93
+ /**
94
+ * Prepare a retry of the turn `requestId`: returns the user message to resend and the transcript
95
+ * without that turn (its user message and the failed assistant reply), so the retried turn
96
+ * replaces the failed one instead of appending a duplicate. Only the last turn can be retried,
97
+ * and only when it did not finish normally.
98
+ */
99
+ export declare function turnForRetry(messages: ChatMessage[], requestId: string): {
100
+ user: UserMessage;
101
+ rest: ChatMessage[];
102
+ } | undefined;
93
103
  /** Human titles for client error codes (mirrors the task pane's wording). */
94
104
  export declare function errorTitle(code: ClientErrorCode): string;
95
105
  export type { ClientKind };
@@ -1 +1 @@
1
- {"version":3,"file":"reducer.d.ts","sourceRoot":"","sources":["../src/lib/reducer.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"reducer.d.ts","sourceRoot":"","sources":["../src/lib/reducer.ts"],"names":[],"mappings":"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"}
package/dist/reducer.js CHANGED
@@ -165,6 +165,23 @@ export function applyEvent(message, event) {
165
165
  };
166
166
  }
167
167
  }
168
+ /**
169
+ * Prepare a retry of the turn `requestId`: returns the user message to resend and the transcript
170
+ * without that turn (its user message and the failed assistant reply), so the retried turn
171
+ * replaces the failed one instead of appending a duplicate. Only the last turn can be retried,
172
+ * and only when it did not finish normally.
173
+ */
174
+ export function turnForRetry(messages, requestId) {
175
+ const last = messages[messages.length - 1];
176
+ if (last?.role !== 'assistant' || last.requestId !== requestId)
177
+ return undefined;
178
+ if (last.status === 'streaming' || last.status === 'done')
179
+ return undefined;
180
+ const user = messages[messages.length - 2];
181
+ if (user?.role !== 'user')
182
+ return undefined;
183
+ return { user, rest: messages.slice(0, -2) };
184
+ }
168
185
  /** Human titles for client error codes (mirrors the task pane's wording). */
169
186
  export function errorTitle(code) {
170
187
  const titles = {
@@ -182,5 +199,6 @@ export function errorTitle(code) {
182
199
  network: 'Cannot reach Classic Agent',
183
200
  protocol: 'Unexpected response from Classic Agent',
184
201
  };
185
- return titles[code];
202
+ // A newer runtime may send a code this client doesn't know yet.
203
+ return titles[code] ?? 'Something went wrong';
186
204
  }
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { applyEvent, errorTitle, newAssistantMessage, newUserMessage } from './reducer.js';
2
+ import { applyEvent, errorTitle, newAssistantMessage, newUserMessage, turnForRetry, } from './reducer.js';
3
3
  const requestId = '11111111-1111-4111-8111-111111111111';
4
4
  const scope = { requestId, agent: 'classic-agent', runId: 'r1' };
5
5
  const ev = (e, seq) => ({ ...e, seq });
@@ -123,3 +123,31 @@ describe('applyEvent', () => {
123
123
  expect(user.contextLabel).toBe('Lot 12');
124
124
  });
125
125
  });
126
+ describe('turnForRetry', () => {
127
+ const failed = (id, status) => ({
128
+ ...newAssistantMessage(id, 2),
129
+ status,
130
+ });
131
+ const idA = '22222222-2222-4222-8222-222222222222';
132
+ it('drops the failed turn so the retry replaces it', () => {
133
+ const earlier = [newUserMessage(idA, 'first', { now: 1 }), failed(idA, 'done')];
134
+ const messages = [
135
+ ...earlier,
136
+ newUserMessage(requestId, 'again', { now: 3 }),
137
+ failed(requestId, 'error'),
138
+ ];
139
+ const turn = turnForRetry(messages, requestId);
140
+ expect(turn?.user.text).toBe('again');
141
+ expect(turn?.rest).toEqual(earlier);
142
+ });
143
+ it('retries stopped turns but not finished, running or earlier ones', () => {
144
+ const turn = (status) => [
145
+ newUserMessage(requestId, 'q', { now: 1 }),
146
+ failed(requestId, status),
147
+ ];
148
+ expect(turnForRetry(turn('cancelled'), requestId)).toBeDefined();
149
+ expect(turnForRetry(turn('done'), requestId)).toBeUndefined();
150
+ expect(turnForRetry(turn('streaming'), requestId)).toBeUndefined();
151
+ expect(turnForRetry(turn('error'), idA)).toBeUndefined();
152
+ });
153
+ });
@@ -44,6 +44,12 @@ export declare class ChatSession {
44
44
  send(text: string, options?: {
45
45
  files?: FileRef[];
46
46
  }): Promise<void>;
47
+ /**
48
+ * Resend the last turn after it failed, was stopped or was cut short. The failed turn is
49
+ * replaced in the view rather than duplicated. (The runtime may still hold the first user
50
+ * message when it had accepted the turn; a server-side regenerate is a separate change.)
51
+ */
52
+ retry(requestId: string): Promise<void>;
47
53
  /** Re-attach to a turn after reload: replays events after the last seq we hold (none here). */
48
54
  resume(requestId: string): Promise<void>;
49
55
  cancel(): void;
@@ -1 +1 @@
1
- {"version":3,"file":"session.svelte.d.ts","sourceRoot":"","sources":["../src/lib/session.svelte.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAElE,OAAO,KAAK,EACV,QAAQ,EACR,WAAW,EACX,SAAS,EAET,UAAU,EACV,eAAe,EACf,OAAO,EACR,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAIL,KAAK,gBAAgB,EACrB,KAAK,WAAW,EACjB,MAAM,cAAc,CAAC;AAEtB,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,cAAc,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,mGAAmG;IACnG,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,KAAK,CAAC,EAAE,QAAQ,CAAC;IACjB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,kEAAkE;IAClE,UAAU,CAAC,EAAE,MAAM,eAAe,GAAG,IAAI,GAAG,SAAS,CAAC;IACtD,qBAAqB,CAAC,EAAE,CAAC,cAAc,EAAE,MAAM,KAAK,IAAI,CAAC;IACzD,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACtD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;CACtC;AAED,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,SAAS,GAAG,WAAW,CAAC;AAE7D;;;GAGG;AACH,qBAAa,WAAW;IACtB,QAAQ,gBAA6B;IACrC,MAAM,gBAAiC;IACvC,cAAc,gBAA+B;IAC7C,gGAAgG;IAChG,oBAAoB,gBAA+B;IACnD,IAAI,8BAA8C;IAClD,SAAS,qBAAyC;IAClD,YAAY;;;;;;QAAyB;IAErC,OAAO,CAAC,UAAU,CAA8B;IAChD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;gBAEjC,OAAO,EAAE,kBAAkB;IAMvC,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,IAAI,OAAO,IAAI,gBAAgB,GAAG,SAAS,CAG1C;IAED,yFAAyF;IACnF,IAAI,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA+B3C,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE;QAAE,KAAK,CAAC,EAAE,OAAO,EAAE,CAAA;KAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IA0B5E,+FAA+F;IACzF,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAa9C,MAAM,IAAI,IAAI;YAOA,GAAG;IAmDjB,OAAO,CAAC,QAAQ;IAOhB,OAAO,CAAC,MAAM;CAKf;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,kBAAkB,GAAG,WAAW,CAE1E"}
1
+ {"version":3,"file":"session.svelte.d.ts","sourceRoot":"","sources":["../src/lib/session.svelte.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAElE,OAAO,KAAK,EACV,QAAQ,EACR,WAAW,EACX,SAAS,EAET,UAAU,EACV,eAAe,EACf,OAAO,EACR,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAKL,KAAK,gBAAgB,EACrB,KAAK,WAAW,EACjB,MAAM,cAAc,CAAC;AAEtB,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,cAAc,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,mGAAmG;IACnG,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,KAAK,CAAC,EAAE,QAAQ,CAAC;IACjB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,kEAAkE;IAClE,UAAU,CAAC,EAAE,MAAM,eAAe,GAAG,IAAI,GAAG,SAAS,CAAC;IACtD,qBAAqB,CAAC,EAAE,CAAC,cAAc,EAAE,MAAM,KAAK,IAAI,CAAC;IACzD,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACtD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;CACtC;AAED,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,SAAS,GAAG,WAAW,CAAC;AAE7D;;;GAGG;AACH,qBAAa,WAAW;IACtB,QAAQ,gBAA6B;IACrC,MAAM,gBAAiC;IACvC,cAAc,gBAA+B;IAC7C,gGAAgG;IAChG,oBAAoB,gBAA+B;IACnD,IAAI,8BAA8C;IAClD,SAAS,qBAAyC;IAClD,YAAY;;;;;;QAAyB;IAErC,OAAO,CAAC,UAAU,CAA8B;IAChD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;gBAEjC,OAAO,EAAE,kBAAkB;IAMvC,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,IAAI,OAAO,IAAI,gBAAgB,GAAG,SAAS,CAG1C;IAED,yFAAyF;IACnF,IAAI,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA+B3C,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE;QAAE,KAAK,CAAC,EAAE,OAAO,EAAE,CAAA;KAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IA0B5E;;;;OAIG;IACG,KAAK,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQ7C,+FAA+F;IACzF,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAa9C,MAAM,IAAI,IAAI;YAOA,GAAG;IAmDjB,OAAO,CAAC,QAAQ;IAOhB,OAAO,CAAC,MAAM;CAKf;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,kBAAkB,GAAG,WAAW,CAE1E"}
@@ -1,5 +1,5 @@
1
1
  import { ApiRequestError } from '@classic-homes/agent-client';
2
- import { applyEvent, newAssistantMessage, newUserMessage, } from './reducer.js';
2
+ import { applyEvent, newAssistantMessage, newUserMessage, turnForRetry, } from './reducer.js';
3
3
  /**
4
4
  * Reactive conversation state (Svelte 5 runes class). Owns the stream lifecycle: one turn at a
5
5
  * time, cancel, resume after reload, history load. Rendering is left to the components.
@@ -82,6 +82,20 @@ export class ChatSession {
82
82
  ];
83
83
  await this.run(requestId, (signal, onEvent) => this.options.client.streamChat(request, { signal, onEvent }));
84
84
  }
85
+ /**
86
+ * Resend the last turn after it failed, was stopped or was cut short. The failed turn is
87
+ * replaced in the view rather than duplicated. (The runtime may still hold the first user
88
+ * message when it had accepted the turn; a server-side regenerate is a separate change.)
89
+ */
90
+ async retry(requestId) {
91
+ if (this.status !== 'idle')
92
+ return;
93
+ const turn = turnForRetry(this.messages, requestId);
94
+ if (!turn)
95
+ return;
96
+ this.messages = turn.rest;
97
+ await this.send(turn.user.text, { files: turn.user.attachments });
98
+ }
85
99
  /** Re-attach to a turn after reload: replays events after the last seq we hold (none here). */
86
100
  async resume(requestId) {
87
101
  if (!this.conversationId || this.status !== 'idle')
@@ -43,8 +43,8 @@ export declare function isFromFrame(event: MessageEvent, frame: HTMLIFrameElemen
43
43
  /**
44
44
  * Target origin for messages to the sandbox frame. An opaque origin matches nothing but "*",
45
45
  * so a message addressed to the sandbox's https origin would be dropped silently. The window
46
- * reference itself (`frame.contentWindow`) is what scopes the message; the HTML it carries is
47
- * model output that is untrusted anyway.
46
+ * reference itself (`frame.contentWindow`) is what scopes the message; the HTML it carries may contain private data. Keep the outer frame
47
+ * on the trusted sandbox page; never add navigation controlled by rendered content.
48
48
  */
49
49
  export declare const FRAME_TARGET_ORIGIN = "*";
50
50
  /** Text an intent or prompt action becomes in the composer. */
@@ -74,8 +74,8 @@ export function isFromFrame(event, frame, sandboxOrigin) {
74
74
  /**
75
75
  * Target origin for messages to the sandbox frame. An opaque origin matches nothing but "*",
76
76
  * so a message addressed to the sandbox's https origin would be dropped silently. The window
77
- * reference itself (`frame.contentWindow`) is what scopes the message; the HTML it carries is
78
- * model output that is untrusted anyway.
77
+ * reference itself (`frame.contentWindow`) is what scopes the message; the HTML it carries may contain private data. Keep the outer frame
78
+ * on the trusted sandbox page; never add navigation controlled by rendered content.
79
79
  */
80
80
  export const FRAME_TARGET_ORIGIN = '*';
81
81
  /** Text an intent or prompt action becomes in the composer. */
@@ -0,0 +1,16 @@
1
+ import { type FrameAction } from './ui-frame-protocol.js';
2
+ export interface FrameRelay {
3
+ send(): void;
4
+ destroy(): void;
5
+ }
6
+ /** Shared lifecycle for artifacts and MCP-UI; the outer frame URL is host-controlled. */
7
+ export declare function createFrameRelay(options: {
8
+ frame: () => HTMLIFrameElement | null;
9
+ origin: () => string;
10
+ payload: () => {
11
+ html: string;
12
+ uri: string;
13
+ };
14
+ onaction: (action: FrameAction) => void;
15
+ }): FrameRelay;
16
+ //# sourceMappingURL=ui-frame-relay.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ui-frame-relay.d.ts","sourceRoot":"","sources":["../src/lib/ui-frame-relay.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,WAAW,EACjB,MAAM,wBAAwB,CAAC;AAEhC,MAAM,WAAW,UAAU;IACzB,IAAI,IAAI,IAAI,CAAC;IACb,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,yFAAyF;AACzF,wBAAgB,gBAAgB,CAAC,OAAO,EAAE;IACxC,KAAK,EAAE,MAAM,iBAAiB,GAAG,IAAI,CAAC;IACtC,MAAM,EAAE,MAAM,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IAC7C,QAAQ,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,CAAC;CACzC,GAAG,UAAU,CAkBb"}
@@ -0,0 +1,22 @@
1
+ import { FRAME_TARGET_ORIGIN, UI_FRAME_RENDER, isFromFrame, parseFrameMessage, } from './ui-frame-protocol.js';
2
+ /** Shared lifecycle for artifacts and MCP-UI; the outer frame URL is host-controlled. */
3
+ export function createFrameRelay(options) {
4
+ const send = () => {
5
+ options
6
+ .frame()
7
+ ?.contentWindow?.postMessage({ type: UI_FRAME_RENDER, ...options.payload() }, FRAME_TARGET_ORIGIN);
8
+ };
9
+ const listener = (event) => {
10
+ if (!isFromFrame(event, options.frame(), options.origin()))
11
+ return;
12
+ const action = parseFrameMessage(event.data);
13
+ if (!action)
14
+ return;
15
+ if (action.type === 'ready')
16
+ send();
17
+ else
18
+ options.onaction(action);
19
+ };
20
+ window.addEventListener('message', listener);
21
+ return { send, destroy: () => window.removeEventListener('message', listener) };
22
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ui-frame-relay.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ui-frame-relay.test.d.ts","sourceRoot":"","sources":["../src/lib/ui-frame-relay.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,36 @@
1
+ import { expect, it, vi } from 'vitest';
2
+ import { createFrameRelay } from './ui-frame-relay.js';
3
+ it('scopes ready/actions to its own frame, sends current HTML, and removes the listener', () => {
4
+ const postMessage = vi.fn();
5
+ const source = { postMessage };
6
+ const frame = { contentWindow: source };
7
+ const onaction = vi.fn();
8
+ let html = '<p>first</p>';
9
+ const relay = createFrameRelay({
10
+ frame: () => frame,
11
+ origin: () => 'https://sandbox.example',
12
+ payload: () => ({ html, uri: 'ui://test' }),
13
+ onaction,
14
+ });
15
+ const dispatch = (sender, data, origin = 'null') => window.dispatchEvent(new MessageEvent('message', { source: sender, origin, data }));
16
+ try {
17
+ dispatch(window, { type: 'ui-lifecycle-iframe-ready' });
18
+ expect(postMessage).not.toHaveBeenCalled();
19
+ dispatch(source, { type: 'ui-lifecycle-iframe-ready' });
20
+ expect(postMessage).toHaveBeenLastCalledWith({ type: 'agent-ui:render', html, uri: 'ui://test' }, '*');
21
+ html = '<p>updated artifact</p>';
22
+ relay.send();
23
+ expect(postMessage).toHaveBeenLastCalledWith({ type: 'agent-ui:render', html, uri: 'ui://test' }, '*');
24
+ dispatch(source, { type: 'tool', payload: { toolName: 'test' } });
25
+ expect(onaction).toHaveBeenCalledTimes(1);
26
+ expect(onaction).toHaveBeenCalledWith({ type: 'tool', toolName: 'test', params: undefined });
27
+ dispatch(source, { type: 'link', payload: { url: 'javascript:alert(1)' } });
28
+ dispatch(source, { type: 'notify', payload: { message: 'spoof' } }, 'https://other.example');
29
+ expect(onaction).toHaveBeenCalledTimes(1);
30
+ }
31
+ finally {
32
+ relay.destroy();
33
+ }
34
+ dispatch(source, { type: 'ui-lifecycle-iframe-ready' });
35
+ expect(postMessage).toHaveBeenCalledTimes(2);
36
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classic-homes/agent-chat",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Svelte 5 chat UI for Classic Agent: session state, message list, composer, tool timeline, artifacts and MCP-UI sandbox frames. Consumed by the Classic App.",
@@ -17,18 +17,21 @@
17
17
  "dist"
18
18
  ],
19
19
  "dependencies": {
20
- "@classic-homes/agent-client": "0.1.3",
21
- "@classic-homes/agent-contracts": "0.1.3",
20
+ "@classic-homes/agent-client": "0.2.0",
21
+ "@classic-homes/agent-contracts": "0.2.0",
22
22
  "dompurify": "3.4.17"
23
23
  },
24
24
  "peerDependencies": {
25
- "@classic-homes/theme-svelte": "^1.34.0",
25
+ "@classic-homes/theme-svelte": "^1.39.0",
26
26
  "svelte": "^5.0.0"
27
27
  },
28
28
  "devDependencies": {
29
- "@classic-homes/theme-svelte": "1.34.0",
29
+ "@classic-homes/theme-svelte": "1.39.0",
30
30
  "@sveltejs/package": "3.0.2",
31
31
  "@sveltejs/vite-plugin-svelte": "7.3.1",
32
+ "@testing-library/svelte": "5.4.2",
33
+ "@testing-library/user-event": "14.6.1",
34
+ "jsdom": "30.1.3",
32
35
  "svelte": "5.57.3",
33
36
  "svelte-check": "4.7.7",
34
37
  "vite": "8.3.5"