@classic-homes/agent-chat 0.11.0 → 0.12.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 (114) hide show
  1. package/README.md +57 -2
  2. package/dist/components/AgentChat.svelte +58 -22
  3. package/dist/components/AgentChat.svelte.d.ts +11 -1
  4. package/dist/components/AgentChat.svelte.d.ts.map +1 -1
  5. package/dist/components/ArtifactPanel.svelte +4 -2
  6. package/dist/components/AssistantMessage.svelte +14 -5
  7. package/dist/components/AssistantMessage.svelte.d.ts +2 -0
  8. package/dist/components/AssistantMessage.svelte.d.ts.map +1 -1
  9. package/dist/components/ChatHeader.svelte +4 -1
  10. package/dist/components/ChatHeader.svelte.d.ts +2 -0
  11. package/dist/components/ChatHeader.svelte.d.ts.map +1 -1
  12. package/dist/components/ChatLauncher.svelte +157 -0
  13. package/dist/components/ChatLauncher.svelte.d.ts +23 -0
  14. package/dist/components/ChatLauncher.svelte.d.ts.map +1 -0
  15. package/dist/components/Composer.svelte +7 -3
  16. package/dist/components/Composer.svelte.d.ts.map +1 -1
  17. package/dist/components/FloatingWindow.svelte +239 -0
  18. package/dist/components/FloatingWindow.svelte.d.ts +26 -0
  19. package/dist/components/FloatingWindow.svelte.d.ts.map +1 -0
  20. package/dist/components/Markdown.svelte +4 -0
  21. package/dist/components/Markdown.svelte.d.ts.map +1 -1
  22. package/dist/components/MessageFeedback.svelte +11 -4
  23. package/dist/components/MessageFeedback.svelte.d.ts.map +1 -1
  24. package/dist/components/MessageFiles.svelte +133 -40
  25. package/dist/components/MessageFiles.svelte.d.ts +6 -0
  26. package/dist/components/MessageFiles.svelte.d.ts.map +1 -1
  27. package/dist/components/MessageItem.svelte +2 -0
  28. package/dist/components/MessageItem.svelte.d.ts +2 -0
  29. package/dist/components/MessageItem.svelte.d.ts.map +1 -1
  30. package/dist/components/MessageList.svelte +40 -7
  31. package/dist/components/MessageList.svelte.d.ts +3 -1
  32. package/dist/components/MessageList.svelte.d.ts.map +1 -1
  33. package/dist/components/ShortcutsDialog.svelte +10 -3
  34. package/dist/components/ShortcutsDialog.svelte.d.ts +2 -0
  35. package/dist/components/ShortcutsDialog.svelte.d.ts.map +1 -1
  36. package/dist/components/UiFrame.svelte +12 -4
  37. package/dist/components/UiFrame.svelte.d.ts.map +1 -1
  38. package/dist/components/UserMessage.svelte +10 -1
  39. package/dist/components/UserMessage.svelte.d.ts.map +1 -1
  40. package/dist/index.d.ts +4 -0
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +4 -0
  43. package/dist/markdown.d.ts.map +1 -1
  44. package/dist/markdown.js +94 -61
  45. package/dist/reducer.d.ts +11 -0
  46. package/dist/reducer.d.ts.map +1 -1
  47. package/dist/reducer.js +18 -2
  48. package/dist/session.svelte.d.ts +38 -0
  49. package/dist/session.svelte.d.ts.map +1 -1
  50. package/dist/session.svelte.js +176 -23
  51. package/dist/shortcuts.d.ts +7 -0
  52. package/dist/shortcuts.d.ts.map +1 -1
  53. package/dist/shortcuts.js +9 -0
  54. package/dist/surface-geometry.d.ts +56 -0
  55. package/dist/surface-geometry.d.ts.map +1 -0
  56. package/dist/surface-geometry.js +99 -0
  57. package/dist/surface.svelte.d.ts +69 -0
  58. package/dist/surface.svelte.d.ts.map +1 -0
  59. package/dist/surface.svelte.js +156 -0
  60. package/dist/ui-frame-protocol.d.ts +6 -0
  61. package/dist/ui-frame-protocol.d.ts.map +1 -1
  62. package/dist/ui-frame-protocol.js +11 -0
  63. package/package.json +8 -7
  64. package/dist/artifact-pages.test.d.ts +0 -2
  65. package/dist/artifact-pages.test.d.ts.map +0 -1
  66. package/dist/artifact-pages.test.js +0 -102
  67. package/dist/components/a11y.svelte.test.d.ts +0 -2
  68. package/dist/components/a11y.svelte.test.d.ts.map +0 -1
  69. package/dist/components/a11y.svelte.test.js +0 -183
  70. package/dist/components/agent-picker.svelte.test.d.ts +0 -2
  71. package/dist/components/agent-picker.svelte.test.d.ts.map +0 -1
  72. package/dist/components/agent-picker.svelte.test.js +0 -84
  73. package/dist/components/composer.svelte.test.d.ts +0 -2
  74. package/dist/components/composer.svelte.test.d.ts.map +0 -1
  75. package/dist/components/composer.svelte.test.js +0 -162
  76. package/dist/components/continue.svelte.test.d.ts +0 -2
  77. package/dist/components/continue.svelte.test.d.ts.map +0 -1
  78. package/dist/components/continue.svelte.test.js +0 -109
  79. package/dist/components/conversation-chrome.svelte.test.d.ts +0 -2
  80. package/dist/components/conversation-chrome.svelte.test.d.ts.map +0 -1
  81. package/dist/components/conversation-chrome.svelte.test.js +0 -147
  82. package/dist/components/feedback.svelte.test.d.ts +0 -2
  83. package/dist/components/feedback.svelte.test.d.ts.map +0 -1
  84. package/dist/components/feedback.svelte.test.js +0 -99
  85. package/dist/components/live-fixes.svelte.test.d.ts +0 -2
  86. package/dist/components/live-fixes.svelte.test.d.ts.map +0 -1
  87. package/dist/components/live-fixes.svelte.test.js +0 -127
  88. package/dist/components/markdown-prose.svelte.test.d.ts +0 -2
  89. package/dist/components/markdown-prose.svelte.test.d.ts.map +0 -1
  90. package/dist/components/markdown-prose.svelte.test.js +0 -26
  91. package/dist/components/messages.svelte.test.d.ts +0 -2
  92. package/dist/components/messages.svelte.test.d.ts.map +0 -1
  93. package/dist/components/messages.svelte.test.js +0 -215
  94. package/dist/components/regenerate.svelte.test.d.ts +0 -2
  95. package/dist/components/regenerate.svelte.test.d.ts.map +0 -1
  96. package/dist/components/regenerate.svelte.test.js +0 -108
  97. package/dist/components/welcome.svelte.test.d.ts +0 -2
  98. package/dist/components/welcome.svelte.test.d.ts.map +0 -1
  99. package/dist/components/welcome.svelte.test.js +0 -119
  100. package/dist/composer-keys.test.d.ts +0 -2
  101. package/dist/composer-keys.test.d.ts.map +0 -1
  102. package/dist/composer-keys.test.js +0 -16
  103. package/dist/markdown.test.d.ts +0 -2
  104. package/dist/markdown.test.d.ts.map +0 -1
  105. package/dist/markdown.test.js +0 -35
  106. package/dist/reducer.test.d.ts +0 -2
  107. package/dist/reducer.test.d.ts.map +0 -1
  108. package/dist/reducer.test.js +0 -153
  109. package/dist/ui-frame-protocol.test.d.ts +0 -2
  110. package/dist/ui-frame-protocol.test.d.ts.map +0 -1
  111. package/dist/ui-frame-protocol.test.js +0 -48
  112. package/dist/ui-frame-relay.test.d.ts +0 -2
  113. package/dist/ui-frame-relay.test.d.ts.map +0 -1
  114. package/dist/ui-frame-relay.test.js +0 -36
@@ -1,35 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { isAllowedImageUrl, isSafeUrl, parseMarkdown } from './markdown.js';
3
- describe('parseMarkdown', () => {
4
- it('renders common blocks and sanitizes', () => {
5
- const html = parseMarkdown('# Title\n\nSome **bold** and `code`.\n\n- a\n- b\n\n| h | i |\n|---|---|\n| 1 | 2 |');
6
- expect(html).toContain('<h2>Title</h2>');
7
- expect(html).toContain('<strong>bold</strong>');
8
- expect(html).toContain('<code>code</code>');
9
- expect(html).toContain('<li>a</li>');
10
- expect(html).toContain('<table class="agent-table">');
11
- });
12
- it('strips scripts, inline styles and foreign classes', () => {
13
- const html = parseMarkdown('hi <script>alert(1)</script> <b style="position:fixed" class="evil">x</b>');
14
- expect(html).not.toContain('<script');
15
- expect(html).not.toMatch(/<[a-z]+[^>]*style=/);
16
- expect(html).not.toMatch(/<[a-z]+[^>]*class="evil"/);
17
- expect(html).not.toMatch(/<b[\s>]/);
18
- });
19
- it('allows images only from allowed hosts and blocks unsafe links', () => {
20
- expect(parseMarkdown('![pix](https://attacker.example/p.gif?leak=1)')).not.toContain('<img');
21
- expect(parseMarkdown('![pix](https://images.classichomes.com/a.png)')).toContain('<img');
22
- expect(parseMarkdown('[x](javascript:alert(1))')).not.toContain('<a');
23
- const ext = parseMarkdown('[docs](https://developers.cloudflare.com/)');
24
- expect(ext).toContain('target="_blank"');
25
- expect(ext).toContain('rel="noopener noreferrer"');
26
- });
27
- it('keeps code blocks verbatim and renders task lists', () => {
28
- const html = parseMarkdown('```ts\nconst a = "<b>";\n```\n\n- [x] done\n- [ ] todo');
29
- expect(html).toContain('agent-lang-ts');
30
- expect(html).toContain('&lt;b&gt;');
31
- expect(html).toMatch(/<input[^>]*type="checkbox"[^>]*checked/);
32
- expect(isSafeUrl('javascript%3Aalert(1)')).toBe(false);
33
- expect(isAllowedImageUrl('/api/files/f1')).toBe(true);
34
- });
35
- });
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=reducer.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"reducer.test.d.ts","sourceRoot":"","sources":["../src/lib/reducer.test.ts"],"names":[],"mappings":""}
@@ -1,153 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { applyEvent, errorTitle, newAssistantMessage, newUserMessage, turnForRetry, } from './reducer.js';
3
- const requestId = '11111111-1111-4111-8111-111111111111';
4
- const scope = { requestId, agent: 'classic-agent', runId: 'r1' };
5
- const ev = (e, seq) => ({ ...e, seq });
6
- describe('applyEvent', () => {
7
- it('folds a full turn into the assistant message', () => {
8
- const events = [
9
- ev({ type: 'start', requestId, conversationId: 'c1', mode: 'mock' }, 0),
10
- ev({ type: 'delegation', ...scope, child: 'company-data', childRunId: 'r2', status: 'start' }, 1),
11
- ev({
12
- type: 'tool_call',
13
- requestId,
14
- agent: 'company-data',
15
- runId: 'r2',
16
- parentRunId: 'r1',
17
- callId: 'c1',
18
- name: 'list_homes',
19
- server: 'chapi',
20
- argsPreview: '{}',
21
- }, 2),
22
- ev({
23
- type: 'tool_result',
24
- requestId,
25
- agent: 'company-data',
26
- runId: 'r2',
27
- parentRunId: 'r1',
28
- callId: 'c1',
29
- status: 'ok',
30
- durationMs: 5,
31
- summary: '3 rows',
32
- }, 3),
33
- ev({
34
- type: 'delegation',
35
- ...scope,
36
- child: 'company-data',
37
- childRunId: 'r2',
38
- status: 'end',
39
- summary: 'done',
40
- }, 4),
41
- ev({ type: 'delta', requestId, text: 'Hello ' }, 5),
42
- ev({
43
- type: 'artifact',
44
- ...scope,
45
- artifactId: 'a1',
46
- op: 'open',
47
- mediaType: 'text/markdown',
48
- title: 'Notes',
49
- }, 6),
50
- ev({ type: 'artifact', ...scope, artifactId: 'a1', op: 'delta', text: '# N' }, 7),
51
- ev({ type: 'artifact', ...scope, artifactId: 'a1', op: 'close' }, 8),
52
- ev({
53
- type: 'ui',
54
- ...scope,
55
- callId: 'c2',
56
- uri: 'ui://map/1',
57
- mimeType: 'text/html',
58
- html: '<p>m</p>',
59
- }, 9),
60
- ev({
61
- type: 'file',
62
- ...scope,
63
- fileId: 'f1',
64
- name: 'a.png',
65
- mimeType: 'image/png',
66
- bytes: 9,
67
- path: '/api/files/f1',
68
- }, 10),
69
- ev({ type: 'usage', ...scope, model: 'm', inputTokens: 10, outputTokens: 2 }, 11),
70
- ev({ type: 'delta', requestId, text: 'world' }, 12),
71
- ev({ type: 'done', requestId, finishReason: 'stop' }, 13),
72
- ];
73
- const message = events.reduce(applyEvent, newAssistantMessage(requestId, 0));
74
- expect(message).toMatchObject({
75
- text: 'Hello world',
76
- status: 'done',
77
- mode: 'mock',
78
- finishReason: 'stop',
79
- });
80
- expect(message.tools[0]).toMatchObject({
81
- name: 'list_homes',
82
- status: 'ok',
83
- durationMs: 5,
84
- summary: '3 rows',
85
- });
86
- expect(message.delegations).toEqual([
87
- { child: 'company-data', childRunId: 'r2', status: 'end', summary: 'done' },
88
- ]);
89
- expect(message.artifacts[0]).toMatchObject({
90
- artifactId: 'a1',
91
- text: '# N',
92
- open: false,
93
- title: 'Notes',
94
- });
95
- expect(message.ui[0]).toMatchObject({ uri: 'ui://map/1' });
96
- expect(message.files[0]).toMatchObject({ fileId: 'f1', path: '/api/files/f1' });
97
- expect(message.usage).toEqual({
98
- inputTokens: 10,
99
- outputTokens: 2,
100
- cacheReadTokens: 0,
101
- calls: 1,
102
- });
103
- });
104
- it('marks errors, cancellations and truncation', () => {
105
- const base = newAssistantMessage(requestId, 0);
106
- const errored = applyEvent(applyEvent(base, ev({ type: 'error', requestId, code: 'timeout', message: 'slow', retryable: true }, 1)), ev({ type: 'done', requestId, finishReason: 'error' }, 2));
107
- expect(errored).toMatchObject({ status: 'error', error: { code: 'timeout', retryable: true } });
108
- expect(applyEvent(base, ev({ type: 'done', requestId, finishReason: 'cancelled' }, 1)).status).toBe('cancelled');
109
- expect(applyEvent(base, ev({ type: 'done', requestId, finishReason: 'length' }, 1)).status).toBe('incomplete');
110
- expect(errorTitle('network')).toMatch(/Cannot reach/);
111
- });
112
- it('labels user context', () => {
113
- const user = newUserMessage(requestId, 'hi', {
114
- context: {
115
- host: 'app',
116
- capturedAt: '2026-10-30T00:00:00.000Z',
117
- route: '/sales/homes/1',
118
- title: 'Home 1',
119
- record: { kind: 'home', id: '1', label: 'Lot 12' },
120
- },
121
- now: 0,
122
- });
123
- expect(user.contextLabel).toBe('Lot 12');
124
- });
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
- });
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=ui-frame-protocol.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"ui-frame-protocol.test.d.ts","sourceRoot":"","sources":["../src/lib/ui-frame-protocol.test.ts"],"names":[],"mappings":""}
@@ -1,48 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { composerTextFor, isFromFrame, parseFrameMessage } from './ui-frame-protocol.js';
3
- describe('parseFrameMessage', () => {
4
- it('accepts the MCP-UI action set and clamps sizes', () => {
5
- expect(parseFrameMessage({ type: 'ui-lifecycle-iframe-ready' })).toEqual({ type: 'ready' });
6
- expect(parseFrameMessage({ type: 'ui-size-change', payload: { height: 5000 } })).toEqual({
7
- type: 'size',
8
- height: 1200,
9
- });
10
- expect(parseFrameMessage({ type: 'ui-size-change', payload: { height: 10 } })).toEqual({
11
- type: 'size',
12
- height: 120,
13
- });
14
- expect(parseFrameMessage({ type: 'prompt', payload: { prompt: 'Show lot 12' } })).toEqual({
15
- type: 'prompt',
16
- prompt: 'Show lot 12',
17
- });
18
- expect(parseFrameMessage({
19
- type: 'tool',
20
- payload: { toolName: 'search_homes', params: { q: 'x' } },
21
- })).toEqual({ type: 'tool', toolName: 'search_homes', params: { q: 'x' } });
22
- expect(composerTextFor({ type: 'intent', intent: 'open_home', params: { id: 1 } })).toBe('open_home {"id":1}');
23
- });
24
- it('rejects non-https links and unknown shapes', () => {
25
- expect(parseFrameMessage({ type: 'link', payload: { url: 'javascript:alert(1)' } })).toBeNull();
26
- expect(parseFrameMessage({ type: 'link', payload: { url: 'http://x.example' } })).toBeNull();
27
- expect(parseFrameMessage({
28
- type: 'link',
29
- payload: { url: 'https://hotsheet.classichomes.com/l/1' },
30
- })).toEqual({ type: 'link', url: 'https://hotsheet.classichomes.com/l/1' });
31
- expect(parseFrameMessage('nope')).toBeNull();
32
- expect(parseFrameMessage({ type: 'eval', payload: {} })).toBeNull();
33
- });
34
- });
35
- describe('isFromFrame', () => {
36
- const win = {};
37
- const frame = { contentWindow: win };
38
- const event = (source, origin) => ({ source, origin });
39
- it('accepts the sandbox window with an opaque ("null") or the sandbox origin', () => {
40
- expect(isFromFrame(event(win, 'null'), frame, 'https://sandbox.example')).toBe(true);
41
- expect(isFromFrame(event(win, 'https://sandbox.example'), frame, 'https://sandbox.example')).toBe(true);
42
- });
43
- it('rejects other windows, other origins and a missing frame', () => {
44
- expect(isFromFrame(event({}, 'null'), frame, 'https://sandbox.example')).toBe(false);
45
- expect(isFromFrame(event(win, 'https://evil.example'), frame, 'https://sandbox.example')).toBe(false);
46
- expect(isFromFrame(event(win, 'null'), null, 'https://sandbox.example')).toBe(false);
47
- });
48
- });
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=ui-frame-relay.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"ui-frame-relay.test.d.ts","sourceRoot":"","sources":["../src/lib/ui-frame-relay.test.ts"],"names":[],"mappings":""}
@@ -1,36 +0,0 @@
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
- });