@classic-homes/agent-chat 0.11.0 → 0.13.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.
- package/README.md +67 -2
- package/dist/components/AgentChat.svelte +60 -25
- package/dist/components/AgentChat.svelte.d.ts +11 -1
- package/dist/components/AgentChat.svelte.d.ts.map +1 -1
- package/dist/components/ArtifactPanel.svelte +4 -2
- package/dist/components/AssistantMessage.svelte +58 -16
- package/dist/components/AssistantMessage.svelte.d.ts +2 -0
- package/dist/components/AssistantMessage.svelte.d.ts.map +1 -1
- package/dist/components/ChatHeader.svelte +4 -1
- package/dist/components/ChatHeader.svelte.d.ts +2 -0
- package/dist/components/ChatHeader.svelte.d.ts.map +1 -1
- package/dist/components/ChatLauncher.svelte +175 -0
- package/dist/components/ChatLauncher.svelte.d.ts +26 -0
- package/dist/components/ChatLauncher.svelte.d.ts.map +1 -0
- package/dist/components/Composer.svelte +7 -3
- package/dist/components/Composer.svelte.d.ts.map +1 -1
- package/dist/components/FloatingWindow.svelte +280 -0
- package/dist/components/FloatingWindow.svelte.d.ts +35 -0
- package/dist/components/FloatingWindow.svelte.d.ts.map +1 -0
- package/dist/components/Markdown.svelte +4 -0
- package/dist/components/Markdown.svelte.d.ts.map +1 -1
- package/dist/components/MessageFeedback.svelte +11 -4
- package/dist/components/MessageFeedback.svelte.d.ts.map +1 -1
- package/dist/components/MessageFiles.svelte +133 -40
- package/dist/components/MessageFiles.svelte.d.ts +6 -0
- package/dist/components/MessageFiles.svelte.d.ts.map +1 -1
- package/dist/components/MessageItem.svelte +2 -0
- package/dist/components/MessageItem.svelte.d.ts +2 -0
- package/dist/components/MessageItem.svelte.d.ts.map +1 -1
- package/dist/components/MessageList.svelte +40 -7
- package/dist/components/MessageList.svelte.d.ts +3 -1
- package/dist/components/MessageList.svelte.d.ts.map +1 -1
- package/dist/components/ShortcutsDialog.svelte +10 -3
- package/dist/components/ShortcutsDialog.svelte.d.ts +2 -0
- package/dist/components/ShortcutsDialog.svelte.d.ts.map +1 -1
- package/dist/components/UiFrame.svelte +12 -4
- package/dist/components/UiFrame.svelte.d.ts.map +1 -1
- package/dist/components/UserMessage.svelte +10 -1
- package/dist/components/UserMessage.svelte.d.ts.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/markdown.d.ts.map +1 -1
- package/dist/markdown.js +94 -61
- package/dist/reducer.d.ts +11 -0
- package/dist/reducer.d.ts.map +1 -1
- package/dist/reducer.js +18 -2
- package/dist/session.svelte.d.ts +40 -0
- package/dist/session.svelte.d.ts.map +1 -1
- package/dist/session.svelte.js +178 -23
- package/dist/shortcuts.d.ts +7 -0
- package/dist/shortcuts.d.ts.map +1 -1
- package/dist/shortcuts.js +9 -0
- package/dist/surface-geometry.d.ts +60 -0
- package/dist/surface-geometry.d.ts.map +1 -0
- package/dist/surface-geometry.js +112 -0
- package/dist/surface.svelte.d.ts +79 -0
- package/dist/surface.svelte.d.ts.map +1 -0
- package/dist/surface.svelte.js +178 -0
- package/dist/thinking.d.ts +10 -0
- package/dist/thinking.d.ts.map +1 -0
- package/dist/thinking.js +61 -0
- package/dist/ui-frame-protocol.d.ts +6 -0
- package/dist/ui-frame-protocol.d.ts.map +1 -1
- package/dist/ui-frame-protocol.js +11 -0
- package/package.json +8 -7
- package/dist/artifact-pages.test.d.ts +0 -2
- package/dist/artifact-pages.test.d.ts.map +0 -1
- package/dist/artifact-pages.test.js +0 -102
- package/dist/components/a11y.svelte.test.d.ts +0 -2
- package/dist/components/a11y.svelte.test.d.ts.map +0 -1
- package/dist/components/a11y.svelte.test.js +0 -183
- package/dist/components/agent-picker.svelte.test.d.ts +0 -2
- package/dist/components/agent-picker.svelte.test.d.ts.map +0 -1
- package/dist/components/agent-picker.svelte.test.js +0 -84
- package/dist/components/composer.svelte.test.d.ts +0 -2
- package/dist/components/composer.svelte.test.d.ts.map +0 -1
- package/dist/components/composer.svelte.test.js +0 -162
- package/dist/components/continue.svelte.test.d.ts +0 -2
- package/dist/components/continue.svelte.test.d.ts.map +0 -1
- package/dist/components/continue.svelte.test.js +0 -109
- package/dist/components/conversation-chrome.svelte.test.d.ts +0 -2
- package/dist/components/conversation-chrome.svelte.test.d.ts.map +0 -1
- package/dist/components/conversation-chrome.svelte.test.js +0 -147
- package/dist/components/feedback.svelte.test.d.ts +0 -2
- package/dist/components/feedback.svelte.test.d.ts.map +0 -1
- package/dist/components/feedback.svelte.test.js +0 -99
- package/dist/components/live-fixes.svelte.test.d.ts +0 -2
- package/dist/components/live-fixes.svelte.test.d.ts.map +0 -1
- package/dist/components/live-fixes.svelte.test.js +0 -127
- package/dist/components/markdown-prose.svelte.test.d.ts +0 -2
- package/dist/components/markdown-prose.svelte.test.d.ts.map +0 -1
- package/dist/components/markdown-prose.svelte.test.js +0 -26
- package/dist/components/messages.svelte.test.d.ts +0 -2
- package/dist/components/messages.svelte.test.d.ts.map +0 -1
- package/dist/components/messages.svelte.test.js +0 -215
- package/dist/components/regenerate.svelte.test.d.ts +0 -2
- package/dist/components/regenerate.svelte.test.d.ts.map +0 -1
- package/dist/components/regenerate.svelte.test.js +0 -108
- package/dist/components/welcome.svelte.test.d.ts +0 -2
- package/dist/components/welcome.svelte.test.d.ts.map +0 -1
- package/dist/components/welcome.svelte.test.js +0 -119
- package/dist/composer-keys.test.d.ts +0 -2
- package/dist/composer-keys.test.d.ts.map +0 -1
- package/dist/composer-keys.test.js +0 -16
- package/dist/markdown.test.d.ts +0 -2
- package/dist/markdown.test.d.ts.map +0 -1
- package/dist/markdown.test.js +0 -35
- package/dist/reducer.test.d.ts +0 -2
- package/dist/reducer.test.d.ts.map +0 -1
- package/dist/reducer.test.js +0 -153
- package/dist/ui-frame-protocol.test.d.ts +0 -2
- package/dist/ui-frame-protocol.test.d.ts.map +0 -1
- package/dist/ui-frame-protocol.test.js +0 -48
- package/dist/ui-frame-relay.test.d.ts +0 -2
- package/dist/ui-frame-relay.test.d.ts.map +0 -1
- package/dist/ui-frame-relay.test.js +0 -36
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { cleanup, render, screen, waitFor } from '@testing-library/svelte';
|
|
3
|
-
import userEvent from '@testing-library/user-event';
|
|
4
|
-
import TwoSheets from '../../test-fixtures/TwoSheets.svelte';
|
|
5
|
-
import { releaseStuckBodyLock } from '../body-lock.js';
|
|
6
|
-
import { parseMarkdown } from '../markdown.js';
|
|
7
|
-
import { ChatSession } from '../session.svelte.js';
|
|
8
|
-
import AgentChat from './AgentChat.svelte';
|
|
9
|
-
import ConversationSidebar from './ConversationSidebar.svelte';
|
|
10
|
-
import Markdown from './Markdown.svelte';
|
|
11
|
-
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
12
|
-
afterEach(() => {
|
|
13
|
-
cleanup();
|
|
14
|
-
document.body.removeAttribute('style');
|
|
15
|
-
});
|
|
16
|
-
/** A client whose streamChat replays `events` (all in one tick) and reports done. */
|
|
17
|
-
function fakeClient(events) {
|
|
18
|
-
return {
|
|
19
|
-
fileUrl: (id) => `#${id}`,
|
|
20
|
-
uploadFile: vi.fn(),
|
|
21
|
-
cancel: vi.fn(),
|
|
22
|
-
getConversation: vi.fn(),
|
|
23
|
-
resumeTurn: vi.fn(),
|
|
24
|
-
streamChat: vi.fn(async (req, opts) => {
|
|
25
|
-
events(req.requestId).forEach((e, seq) => opts.onEvent({ requestId: req.requestId, seq, ...e }));
|
|
26
|
-
return { kind: 'done' };
|
|
27
|
-
}),
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
const scope = { agent: 'classic-agent', runId: 'run-1' };
|
|
31
|
-
const turn = (requestId) => [
|
|
32
|
-
{ type: 'start', conversationId: 'c-1', mode: 'live' },
|
|
33
|
-
{ type: 'delta', text: 'Here it is.' },
|
|
34
|
-
{
|
|
35
|
-
type: 'artifact',
|
|
36
|
-
...scope,
|
|
37
|
-
artifactId: 'a1',
|
|
38
|
-
op: 'open',
|
|
39
|
-
mediaType: 'text/markdown',
|
|
40
|
-
title: 'Memo',
|
|
41
|
-
},
|
|
42
|
-
{ type: 'artifact', ...scope, artifactId: 'a1', op: 'delta', text: '# Memo' },
|
|
43
|
-
{ type: 'artifact', ...scope, artifactId: 'a1', op: 'close' },
|
|
44
|
-
{ type: 'done', finishReason: 'stop' },
|
|
45
|
-
].map((e) => ({ ...e, requestId }));
|
|
46
|
-
describe('body lock guard', () => {
|
|
47
|
-
it('clears the lock the bits-ui race leaves behind once no modal is open', async () => {
|
|
48
|
-
// Reproduces the race: A closes and B opens in the same tick, then B closes.
|
|
49
|
-
const { rerender } = render(TwoSheets, { props: { a: true, b: false } });
|
|
50
|
-
await wait(100);
|
|
51
|
-
await rerender({ a: false, b: false });
|
|
52
|
-
await wait(0);
|
|
53
|
-
await rerender({ a: false, b: true });
|
|
54
|
-
await wait(300);
|
|
55
|
-
await rerender({ a: false, b: false });
|
|
56
|
-
await wait(600);
|
|
57
|
-
expect(document.body.style.pointerEvents).toBe('none');
|
|
58
|
-
releaseStuckBodyLock(0);
|
|
59
|
-
await wait(10);
|
|
60
|
-
expect(document.body.style.pointerEvents).toBe('');
|
|
61
|
-
expect(document.body.style.overflow).toBe('');
|
|
62
|
-
});
|
|
63
|
-
it('leaves the lock alone while a modal is open', async () => {
|
|
64
|
-
render(TwoSheets, { props: { a: true } });
|
|
65
|
-
await wait(300);
|
|
66
|
-
releaseStuckBodyLock(0);
|
|
67
|
-
await wait(10);
|
|
68
|
-
expect(document.body.style.pointerEvents).toBe('none');
|
|
69
|
-
});
|
|
70
|
-
});
|
|
71
|
-
describe('ChatSession onTurnEnd', () => {
|
|
72
|
-
it('reports the finished turn with its conversation and status', async () => {
|
|
73
|
-
const onTurnEnd = vi.fn();
|
|
74
|
-
const client = fakeClient(turn);
|
|
75
|
-
const session = new ChatSession({ client: client, onTurnEnd });
|
|
76
|
-
await session.send('hi');
|
|
77
|
-
expect(onTurnEnd).toHaveBeenCalledOnce();
|
|
78
|
-
expect(onTurnEnd.mock.calls[0][0]).toMatchObject({ conversationId: 'c-1', status: 'done' });
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
describe('AgentChat artifacts', () => {
|
|
82
|
-
it('opens an artifact that starts and finishes within one tick', async () => {
|
|
83
|
-
const user = userEvent.setup();
|
|
84
|
-
const onturnend = vi.fn();
|
|
85
|
-
render(AgentChat, {
|
|
86
|
-
props: { client: fakeClient(turn), sandboxOrigin: 'https://frame.test', onturnend },
|
|
87
|
-
});
|
|
88
|
-
await user.type(screen.getByRole('textbox', { name: 'Message' }), 'memo{Enter}');
|
|
89
|
-
// jsdom has no wide viewport, so the artifact opens in the sheet.
|
|
90
|
-
expect(await screen.findByRole('dialog', { name: 'Memo' })).toBeTruthy();
|
|
91
|
-
await waitFor(() => expect(onturnend).toHaveBeenCalledOnce());
|
|
92
|
-
});
|
|
93
|
-
});
|
|
94
|
-
describe('rename dialog', () => {
|
|
95
|
-
it('starts in the title field with the title selected', async () => {
|
|
96
|
-
const user = userEvent.setup();
|
|
97
|
-
const now = new Date().toISOString();
|
|
98
|
-
render(ConversationSidebar, {
|
|
99
|
-
props: {
|
|
100
|
-
conversations: [
|
|
101
|
-
{
|
|
102
|
-
conversationId: 'c1',
|
|
103
|
-
agent: 'classic-agent',
|
|
104
|
-
title: 'Lot 12',
|
|
105
|
-
createdAt: now,
|
|
106
|
-
updatedAt: now,
|
|
107
|
-
},
|
|
108
|
-
],
|
|
109
|
-
onrename: vi.fn(),
|
|
110
|
-
},
|
|
111
|
-
});
|
|
112
|
-
await user.click(screen.getByRole('button', { name: 'Options for Lot 12' }));
|
|
113
|
-
await user.click(await screen.findByRole('menuitem', { name: /Rename/ }));
|
|
114
|
-
const field = await screen.findByRole('textbox', { name: 'Conversation title' });
|
|
115
|
-
await waitFor(() => expect(document.activeElement).toBe(field));
|
|
116
|
-
const input = field;
|
|
117
|
-
expect([input.selectionStart, input.selectionEnd]).toEqual([0, 'Lot 12'.length]);
|
|
118
|
-
});
|
|
119
|
-
});
|
|
120
|
-
describe('Markdown tables', () => {
|
|
121
|
-
it('keep words whole so wide tables scroll instead of breaking numbers', () => {
|
|
122
|
-
const { container } = render(Markdown, {
|
|
123
|
-
props: { html: parseMarkdown('| a | b |\n| - | - |\n| $1,075,000 | x |') },
|
|
124
|
-
});
|
|
125
|
-
expect(container.firstElementChild.className).toContain('[&_table]:[overflow-wrap:normal]');
|
|
126
|
-
});
|
|
127
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"markdown-prose.svelte.test.d.ts","sourceRoot":"","sources":["../../src/lib/components/markdown-prose.svelte.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it } from 'vitest';
|
|
2
|
-
import { cleanup, render } from '@testing-library/svelte';
|
|
3
|
-
import { parseMarkdown } from '../markdown.js';
|
|
4
|
-
import { errorTitle } from '../reducer.js';
|
|
5
|
-
import Markdown from './Markdown.svelte';
|
|
6
|
-
afterEach(cleanup);
|
|
7
|
-
describe('Markdown', () => {
|
|
8
|
-
// The Classic App's cascade-layers polyfill outranks plain component CSS, so the prose rules
|
|
9
|
-
// must be utilities on the container (classic-agent visual check, 2026-11-03).
|
|
10
|
-
it('styles prose with utility classes, not a stylesheet', () => {
|
|
11
|
-
const { container } = render(Markdown, {
|
|
12
|
-
props: { html: parseMarkdown('- one\n- two'), class: 'text-sm' },
|
|
13
|
-
});
|
|
14
|
-
const root = container.firstElementChild;
|
|
15
|
-
expect(root.className).toContain('[&_ul]:list-disc');
|
|
16
|
-
expect(root.className).toContain('[&_td]:border');
|
|
17
|
-
expect(root.className).toContain('text-sm');
|
|
18
|
-
expect(root.querySelectorAll('li')).toHaveLength(2);
|
|
19
|
-
expect(document.querySelectorAll('style')).toHaveLength(0);
|
|
20
|
-
});
|
|
21
|
-
});
|
|
22
|
-
describe('errorTitle', () => {
|
|
23
|
-
it('falls back for a code this client does not know', () => {
|
|
24
|
-
expect(errorTitle('upstream')).toBe('Something went wrong');
|
|
25
|
-
});
|
|
26
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"messages.svelte.test.d.ts","sourceRoot":"","sources":["../../src/lib/components/messages.svelte.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,215 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/svelte';
|
|
3
|
-
import userEvent from '@testing-library/user-event';
|
|
4
|
-
import { parseMarkdown } from '../markdown.js';
|
|
5
|
-
import { daySeparators, describeProposal, formatBytes, formatDay } from '../message-format.js';
|
|
6
|
-
import { newAssistantMessage, newUserMessage } from '../reducer.js';
|
|
7
|
-
import AssistantMessageView from './AssistantMessage.svelte';
|
|
8
|
-
import Markdown from './Markdown.svelte';
|
|
9
|
-
import MessageList from './MessageList.svelte';
|
|
10
|
-
import UserMessageView from './UserMessage.svelte';
|
|
11
|
-
afterEach(() => cleanup());
|
|
12
|
-
const base = { sandboxOrigin: 'https://frame.test', fileUrl: (id) => `/files/${id}` };
|
|
13
|
-
const assistant = (patch = {}) => ({
|
|
14
|
-
...newAssistantMessage('r1', Date.UTC(2026, 10, 3, 15, 0)),
|
|
15
|
-
status: 'done',
|
|
16
|
-
...patch,
|
|
17
|
-
});
|
|
18
|
-
const DAY = 86_400_000;
|
|
19
|
-
describe('message formatting', () => {
|
|
20
|
-
it('formats sizes and relative days', () => {
|
|
21
|
-
expect(formatBytes(900)).toBe('900 B');
|
|
22
|
-
expect(formatBytes(2048)).toBe('2 KB');
|
|
23
|
-
expect(formatBytes(3 * 1048576)).toBe('3.0 MB');
|
|
24
|
-
const now = new Date(2026, 10, 3, 12).getTime();
|
|
25
|
-
expect(formatDay(now - 60_000, now)).toBe('Today');
|
|
26
|
-
expect(formatDay(now - DAY, now)).toBe('Yesterday');
|
|
27
|
-
expect(formatDay(now - 5 * DAY, now)).not.toMatch(/Today|Yesterday/);
|
|
28
|
-
});
|
|
29
|
-
it('marks the first message of each day', () => {
|
|
30
|
-
const t = new Date(2026, 10, 1, 10).getTime();
|
|
31
|
-
expect([...daySeparators([{ createdAt: t }, { createdAt: t + 60_000 }])]).toEqual([0]);
|
|
32
|
-
expect([...daySeparators([{ createdAt: t }, { createdAt: t + DAY }])]).toEqual([0, 1]);
|
|
33
|
-
});
|
|
34
|
-
it('describes proposals without applying them', () => {
|
|
35
|
-
const op = {
|
|
36
|
-
kind: 'excel.writeRange',
|
|
37
|
-
operationId: '00000000-0000-4000-8000-000000000001',
|
|
38
|
-
documentId: 'doc-1',
|
|
39
|
-
summary: 'Fill totals',
|
|
40
|
-
target: { sheet: 'Budget', address: 'B2:B4' },
|
|
41
|
-
write: { type: 'values', values: [[1], [2], [3]] },
|
|
42
|
-
precondition: { formulas: [[''], [''], ['']] },
|
|
43
|
-
};
|
|
44
|
-
expect(describeProposal(op)).toEqual({
|
|
45
|
-
action: 'Write values',
|
|
46
|
-
host: 'Excel',
|
|
47
|
-
target: 'Budget!B2:B4',
|
|
48
|
-
});
|
|
49
|
-
});
|
|
50
|
-
});
|
|
51
|
-
describe('markdown output', () => {
|
|
52
|
-
it('wraps tables in a scrolling container and never trusts a literal div', () => {
|
|
53
|
-
const html = parseMarkdown('| A | B |\n|---|---|\n| 1 | 2 |\n\n<div class="agent-x">hi</div>');
|
|
54
|
-
expect(html).toMatch(/<div class="agent-table-wrap"><table class="agent-table">/);
|
|
55
|
-
expect(html).toContain('<div');
|
|
56
|
-
expect(html.match(/<div/g)).toHaveLength(1);
|
|
57
|
-
});
|
|
58
|
-
it('marks external links', () => {
|
|
59
|
-
expect(parseMarkdown('[docs](https://example.com/)')).toContain('class="agent-external-link"');
|
|
60
|
-
});
|
|
61
|
-
});
|
|
62
|
-
describe('Markdown code blocks', () => {
|
|
63
|
-
it('adds a language header whose button copies the code', async () => {
|
|
64
|
-
const writeText = vi.fn().mockResolvedValue(undefined);
|
|
65
|
-
Object.defineProperty(navigator, 'clipboard', { value: { writeText }, configurable: true });
|
|
66
|
-
render(Markdown, { props: { html: parseMarkdown('```ts\nconst a = 1;\n```') } });
|
|
67
|
-
const button = await screen.findByRole('button', { name: 'Copy ts code' });
|
|
68
|
-
expect(button.closest('.agent-codeblock')?.textContent).toContain('ts');
|
|
69
|
-
await fireEvent.click(button);
|
|
70
|
-
expect(writeText).toHaveBeenCalledWith('const a = 1;');
|
|
71
|
-
await waitFor(() => expect(button.textContent).toContain('Copied'));
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
describe('AssistantMessage', () => {
|
|
75
|
-
it('shows a retryable error with Try again', async () => {
|
|
76
|
-
const onretry = vi.fn();
|
|
77
|
-
render(AssistantMessageView, {
|
|
78
|
-
props: {
|
|
79
|
-
...base,
|
|
80
|
-
last: true,
|
|
81
|
-
onretry,
|
|
82
|
-
message: assistant({
|
|
83
|
-
status: 'error',
|
|
84
|
-
error: { code: 'upstream_error', message: 'Model failed.', retryable: true },
|
|
85
|
-
}),
|
|
86
|
-
},
|
|
87
|
-
});
|
|
88
|
-
expect(screen.getByText(/The agent ran into a problem/)).toBeTruthy();
|
|
89
|
-
await userEvent.click(screen.getByRole('button', { name: /Try again/ }));
|
|
90
|
-
expect(onretry).toHaveBeenCalledOnce();
|
|
91
|
-
});
|
|
92
|
-
it('offers Continue on a cut-short or stopped last answer only', async () => {
|
|
93
|
-
const oncontinue = vi.fn();
|
|
94
|
-
const { rerender } = render(AssistantMessageView, {
|
|
95
|
-
props: {
|
|
96
|
-
...base,
|
|
97
|
-
last: true,
|
|
98
|
-
oncontinue,
|
|
99
|
-
message: assistant({ status: 'incomplete', text: 'Partial' }),
|
|
100
|
-
},
|
|
101
|
-
});
|
|
102
|
-
expect(screen.getByText('The answer was cut short.')).toBeTruthy();
|
|
103
|
-
await userEvent.click(screen.getByRole('button', { name: 'Continue' }));
|
|
104
|
-
expect(oncontinue).toHaveBeenCalledOnce();
|
|
105
|
-
await rerender({
|
|
106
|
-
...base,
|
|
107
|
-
last: true,
|
|
108
|
-
oncontinue,
|
|
109
|
-
message: assistant({ status: 'cancelled', text: 'Partial' }),
|
|
110
|
-
});
|
|
111
|
-
expect(screen.getByText('Stopped')).toBeTruthy();
|
|
112
|
-
expect(screen.getByRole('button', { name: 'Continue' })).toBeTruthy();
|
|
113
|
-
await rerender({
|
|
114
|
-
...base,
|
|
115
|
-
last: false,
|
|
116
|
-
oncontinue,
|
|
117
|
-
message: assistant({ status: 'cancelled', text: 'Partial' }),
|
|
118
|
-
});
|
|
119
|
-
expect(screen.queryByRole('button', { name: 'Continue' })).toBeNull();
|
|
120
|
-
// Stopped before any text: nothing to continue (Regenerate asks again).
|
|
121
|
-
await rerender({
|
|
122
|
-
...base,
|
|
123
|
-
last: true,
|
|
124
|
-
oncontinue,
|
|
125
|
-
message: assistant({ status: 'cancelled' }),
|
|
126
|
-
});
|
|
127
|
-
expect(screen.queryByRole('button', { name: 'Continue' })).toBeNull();
|
|
128
|
-
});
|
|
129
|
-
it('renders artifacts, proposals and a thinking state', async () => {
|
|
130
|
-
const onopenartifact = vi.fn();
|
|
131
|
-
const { rerender } = render(AssistantMessageView, {
|
|
132
|
-
props: { ...base, streaming: true, message: assistant({ status: 'streaming' }) },
|
|
133
|
-
});
|
|
134
|
-
expect(screen.getByRole('status').textContent).toContain('Thinking');
|
|
135
|
-
await rerender({
|
|
136
|
-
...base,
|
|
137
|
-
onopenartifact,
|
|
138
|
-
message: assistant({
|
|
139
|
-
artifacts: [
|
|
140
|
-
{ artifactId: 'a1', mediaType: 'text/html', title: 'Plans', text: '', open: false },
|
|
141
|
-
{ artifactId: 'a2', mediaType: 'text/markdown', title: 'Memo', text: '', open: true },
|
|
142
|
-
],
|
|
143
|
-
proposals: [
|
|
144
|
-
{
|
|
145
|
-
kind: 'word.replaceSelection',
|
|
146
|
-
operationId: '00000000-0000-4000-8000-000000000002',
|
|
147
|
-
documentId: 'doc-1',
|
|
148
|
-
summary: 'Tighten the intro',
|
|
149
|
-
target: { targetId: 't1', expectedText: 'Old' },
|
|
150
|
-
text: 'New',
|
|
151
|
-
},
|
|
152
|
-
],
|
|
153
|
-
}),
|
|
154
|
-
});
|
|
155
|
-
expect(screen.queryByText('Thinking')).toBeNull();
|
|
156
|
-
await userEvent.click(screen.getByRole('button', { name: 'Open Plans' }));
|
|
157
|
-
expect(onopenartifact).toHaveBeenCalledWith('a1');
|
|
158
|
-
expect(screen.getByRole('button', { name: 'Memo (writing)' })).toBeTruthy();
|
|
159
|
-
expect(screen.getByText('Tighten the intro')).toBeTruthy();
|
|
160
|
-
expect(screen.getByText('Apply from the Word add-in.')).toBeTruthy();
|
|
161
|
-
});
|
|
162
|
-
it('has a copy action once the answer is done', () => {
|
|
163
|
-
render(AssistantMessageView, {
|
|
164
|
-
props: { ...base, message: assistant({ text: 'Hello there' }) },
|
|
165
|
-
});
|
|
166
|
-
expect(screen.getByRole('button', { name: 'Copy message' })).toBeTruthy();
|
|
167
|
-
});
|
|
168
|
-
});
|
|
169
|
-
describe('UserMessage', () => {
|
|
170
|
-
it('shows the text in a bubble with its attachments', () => {
|
|
171
|
-
const message = newUserMessage('r1', 'See attached', {
|
|
172
|
-
attachments: [
|
|
173
|
-
{
|
|
174
|
-
fileId: 'f1',
|
|
175
|
-
name: 'plan.pdf',
|
|
176
|
-
mimeType: 'application/pdf',
|
|
177
|
-
bytes: 2048,
|
|
178
|
-
status: 'uploaded',
|
|
179
|
-
},
|
|
180
|
-
],
|
|
181
|
-
});
|
|
182
|
-
render(UserMessageView, { props: { message } });
|
|
183
|
-
expect(screen.getByText('See attached')).toBeTruthy();
|
|
184
|
-
expect(screen.getByText('plan.pdf')).toBeTruthy();
|
|
185
|
-
expect(screen.getByText('2 KB')).toBeTruthy();
|
|
186
|
-
});
|
|
187
|
-
});
|
|
188
|
-
describe('MessageList', () => {
|
|
189
|
-
it('labels days only when the transcript spans more than one', () => {
|
|
190
|
-
const t = Date.now();
|
|
191
|
-
const one = [newUserMessage('r1', 'hi', { now: t }), assistant({ createdAt: t })];
|
|
192
|
-
const { rerender } = render(MessageList, {
|
|
193
|
-
props: { ...base, streaming: false, messages: one },
|
|
194
|
-
});
|
|
195
|
-
expect(screen.queryByRole('separator')).toBeNull();
|
|
196
|
-
const two = [newUserMessage('r0', 'old', { now: t - 3 * DAY }), ...one];
|
|
197
|
-
void rerender({ ...base, streaming: false, messages: two });
|
|
198
|
-
return waitFor(() => expect(screen.getAllByRole('separator').map((s) => s.textContent?.trim())).toContain('Today'));
|
|
199
|
-
});
|
|
200
|
-
it('offers a jump to the latest once the reader scrolls up', async () => {
|
|
201
|
-
render(MessageList, {
|
|
202
|
-
props: { ...base, streaming: false, messages: [newUserMessage('r1', 'hi'), assistant()] },
|
|
203
|
-
});
|
|
204
|
-
const log = screen.getByRole('log');
|
|
205
|
-
Object.defineProperty(log, 'scrollHeight', { value: 2000, configurable: true });
|
|
206
|
-
Object.defineProperty(log, 'clientHeight', { value: 500, configurable: true });
|
|
207
|
-
log.scrollTop = 100;
|
|
208
|
-
await fireEvent.scroll(log);
|
|
209
|
-
const jump = await screen.findByRole('button', { name: 'Jump to the latest message' });
|
|
210
|
-
log.scrollTo = vi.fn();
|
|
211
|
-
await userEvent.click(jump);
|
|
212
|
-
expect(log.scrollTo).toHaveBeenCalled();
|
|
213
|
-
expect(screen.queryByRole('button', { name: /latest/ })).toBeNull();
|
|
214
|
-
});
|
|
215
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"regenerate.svelte.test.d.ts","sourceRoot":"","sources":["../../src/lib/components/regenerate.svelte.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { cleanup, render, screen, waitFor } from '@testing-library/svelte';
|
|
3
|
-
import userEvent from '@testing-library/user-event';
|
|
4
|
-
import { latestTurn, newAssistantMessage, newUserMessage } from '../reducer.js';
|
|
5
|
-
import { ChatSession } from '../session.svelte.js';
|
|
6
|
-
import AgentChat from './AgentChat.svelte';
|
|
7
|
-
afterEach(() => cleanup());
|
|
8
|
-
/** Answers each request with "Answer N" (N counts requests). */
|
|
9
|
-
function fakeClient() {
|
|
10
|
-
let n = 0;
|
|
11
|
-
return {
|
|
12
|
-
fileUrl: (id) => `#${id}`,
|
|
13
|
-
uploadFile: vi.fn(),
|
|
14
|
-
cancel: vi.fn(),
|
|
15
|
-
getConversation: vi.fn(),
|
|
16
|
-
resumeTurn: vi.fn(),
|
|
17
|
-
streamChat: vi.fn(async (req, opts) => {
|
|
18
|
-
n += 1;
|
|
19
|
-
[
|
|
20
|
-
{ type: 'start', conversationId: 'c-1', mode: 'live' },
|
|
21
|
-
{ type: 'delta', text: `Answer ${n}` },
|
|
22
|
-
{ type: 'done', finishReason: 'stop' },
|
|
23
|
-
].forEach((e, seq) => opts.onEvent({ requestId: req.requestId, seq, ...e }));
|
|
24
|
-
return { kind: 'done' };
|
|
25
|
-
}),
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
describe('latestTurn', () => {
|
|
29
|
-
it('finds the finished latest turn only', () => {
|
|
30
|
-
const done = { ...newAssistantMessage('r2'), status: 'done' };
|
|
31
|
-
const messages = [
|
|
32
|
-
newUserMessage('r1', 'one'),
|
|
33
|
-
{ ...newAssistantMessage('r1'), status: 'done' },
|
|
34
|
-
newUserMessage('r2', 'two'),
|
|
35
|
-
done,
|
|
36
|
-
];
|
|
37
|
-
expect(latestTurn(messages, 'r2')?.user.text).toBe('two');
|
|
38
|
-
expect(latestTurn(messages, 'r2')?.rest).toHaveLength(2);
|
|
39
|
-
expect(latestTurn(messages, 'r1')).toBeUndefined();
|
|
40
|
-
expect(latestTurn([...messages.slice(0, 3), newAssistantMessage('r2')], 'r2')).toBeUndefined();
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
|
-
describe('ChatSession regenerate and edit', () => {
|
|
44
|
-
it('replaces the latest turn instead of adding one', async () => {
|
|
45
|
-
const client = fakeClient();
|
|
46
|
-
const session = new ChatSession({ client: client });
|
|
47
|
-
await session.send('first');
|
|
48
|
-
const firstId = client.streamChat.mock.calls[0][0].requestId;
|
|
49
|
-
await session.regenerate(firstId);
|
|
50
|
-
const second = client.streamChat.mock.calls[1][0];
|
|
51
|
-
expect(second).toMatchObject({ message: 'first', replaces: firstId, conversationId: 'c-1' });
|
|
52
|
-
expect(session.messages.map((m) => m.text)).toEqual(['first', 'Answer 2']);
|
|
53
|
-
await session.edit(second.requestId, 'first, but better');
|
|
54
|
-
expect(client.streamChat.mock.calls[2][0]).toMatchObject({
|
|
55
|
-
message: 'first, but better',
|
|
56
|
-
replaces: second.requestId,
|
|
57
|
-
});
|
|
58
|
-
expect(session.messages.map((m) => m.text)).toEqual(['first, but better', 'Answer 3']);
|
|
59
|
-
});
|
|
60
|
-
it('sends a normal message without replaces', async () => {
|
|
61
|
-
const client = fakeClient();
|
|
62
|
-
const session = new ChatSession({ client: client });
|
|
63
|
-
await session.send('hi');
|
|
64
|
-
expect(client.streamChat.mock.calls[0][0]).not.toHaveProperty('replaces');
|
|
65
|
-
});
|
|
66
|
-
});
|
|
67
|
-
describe('AgentChat regenerate and edit', () => {
|
|
68
|
-
it('offers Regenerate on the latest answer and Edit on the latest question', async () => {
|
|
69
|
-
const user = userEvent.setup();
|
|
70
|
-
const client = fakeClient();
|
|
71
|
-
render(AgentChat, { props: { client: client, sandboxOrigin: 'https://frame.test' } });
|
|
72
|
-
await user.type(screen.getByRole('textbox', { name: 'Message' }), 'one{Enter}');
|
|
73
|
-
await screen.findAllByText('Answer 1');
|
|
74
|
-
await user.type(screen.getByRole('textbox', { name: 'Message' }), 'two{Enter}');
|
|
75
|
-
await screen.findAllByText('Answer 2');
|
|
76
|
-
// Only the latest turn.
|
|
77
|
-
expect(screen.getAllByRole('button', { name: 'Regenerate response' })).toHaveLength(1);
|
|
78
|
-
expect(screen.getAllByRole('button', { name: 'Edit message' })).toHaveLength(1);
|
|
79
|
-
await user.click(screen.getByRole('button', { name: 'Regenerate response' }));
|
|
80
|
-
await waitFor(() => expect(client.streamChat).toHaveBeenCalledTimes(3));
|
|
81
|
-
expect(client.streamChat.mock.calls[2][0]).toMatchObject({ message: 'two' });
|
|
82
|
-
expect((await screen.findAllByText('Answer 3')).length).toBeGreaterThan(0);
|
|
83
|
-
expect(screen.queryByText('Answer 2')).toBeNull();
|
|
84
|
-
await user.click(screen.getByRole('button', { name: 'Edit message' }));
|
|
85
|
-
const field = screen.getByRole('textbox', { name: 'Edited message' });
|
|
86
|
-
await user.clear(field);
|
|
87
|
-
await user.type(field, 'two, edited{Enter}');
|
|
88
|
-
await waitFor(() => expect(client.streamChat).toHaveBeenCalledTimes(4));
|
|
89
|
-
expect(client.streamChat.mock.calls[3][0]).toMatchObject({
|
|
90
|
-
message: 'two, edited',
|
|
91
|
-
replaces: client.streamChat.mock.calls[2][0].requestId,
|
|
92
|
-
});
|
|
93
|
-
expect(await screen.findByText('two, edited')).toBeTruthy();
|
|
94
|
-
expect(screen.queryByText(/^two$/)).toBeNull();
|
|
95
|
-
});
|
|
96
|
-
it('cancels an edit with Escape', async () => {
|
|
97
|
-
const user = userEvent.setup();
|
|
98
|
-
const client = fakeClient();
|
|
99
|
-
render(AgentChat, { props: { client: client, sandboxOrigin: 'https://frame.test' } });
|
|
100
|
-
await user.type(screen.getByRole('textbox', { name: 'Message' }), 'one{Enter}');
|
|
101
|
-
await screen.findAllByText('Answer 1');
|
|
102
|
-
await user.click(screen.getByRole('button', { name: 'Edit message' }));
|
|
103
|
-
await user.type(screen.getByRole('textbox', { name: 'Edited message' }), ' more{Escape}');
|
|
104
|
-
expect(screen.queryByRole('textbox', { name: 'Edited message' })).toBeNull();
|
|
105
|
-
expect(screen.getByText('one')).toBeTruthy();
|
|
106
|
-
expect(client.streamChat).toHaveBeenCalledOnce();
|
|
107
|
-
});
|
|
108
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"welcome.svelte.test.d.ts","sourceRoot":"","sources":["../../src/lib/components/welcome.svelte.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { cleanup, render, screen, waitFor } from '@testing-library/svelte';
|
|
3
|
-
import userEvent from '@testing-library/user-event';
|
|
4
|
-
import { greeting, suggestionText } from '../welcome.js';
|
|
5
|
-
import AgentChat from './AgentChat.svelte';
|
|
6
|
-
import Welcome from './Welcome.svelte';
|
|
7
|
-
afterEach(() => cleanup());
|
|
8
|
-
function fakeClient(getConversation = vi.fn()) {
|
|
9
|
-
return {
|
|
10
|
-
fileUrl: (id) => `#${id}`,
|
|
11
|
-
uploadFile: vi.fn(),
|
|
12
|
-
cancel: vi.fn(),
|
|
13
|
-
getConversation,
|
|
14
|
-
resumeTurn: vi.fn(),
|
|
15
|
-
streamChat: vi.fn(async (req, opts) => {
|
|
16
|
-
[
|
|
17
|
-
{ type: 'start', conversationId: 'c-1', mode: 'live' },
|
|
18
|
-
{ type: 'delta', text: 'Sure.' },
|
|
19
|
-
{ type: 'done', finishReason: 'stop' },
|
|
20
|
-
].forEach((e, seq) => opts.onEvent({ requestId: req.requestId, seq, ...e }));
|
|
21
|
-
return { kind: 'done' };
|
|
22
|
-
}),
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
const base = { sandboxOrigin: 'https://frame.test' };
|
|
26
|
-
describe('welcome helpers', () => {
|
|
27
|
-
it('greets by the time of day and the first name only', () => {
|
|
28
|
-
expect(greeting('Pat Lee', new Date(2026, 10, 4, 9))).toBe('Good morning, Pat');
|
|
29
|
-
expect(greeting(undefined, new Date(2026, 10, 4, 14))).toBe('Good afternoon');
|
|
30
|
-
expect(greeting(' ', new Date(2026, 10, 4, 20))).toBe('Good evening');
|
|
31
|
-
expect(greeting('Sam', new Date(2026, 10, 4, 2))).toBe('Good evening, Sam');
|
|
32
|
-
});
|
|
33
|
-
it('sends the text, or the label without one', () => {
|
|
34
|
-
expect(suggestionText({ label: 'Open lots' })).toBe('Open lots');
|
|
35
|
-
expect(suggestionText({ label: 'Lots', text: ' List open lots ' })).toBe('List open lots');
|
|
36
|
-
});
|
|
37
|
-
});
|
|
38
|
-
describe('Welcome', () => {
|
|
39
|
-
it('shows at most four suggestions and reports the chosen one', async () => {
|
|
40
|
-
const onselect = vi.fn();
|
|
41
|
-
render(Welcome, {
|
|
42
|
-
props: {
|
|
43
|
-
name: 'Pat',
|
|
44
|
-
title: 'Hi Pat',
|
|
45
|
-
onselect,
|
|
46
|
-
suggestions: ['A', 'B', 'C', 'D', 'E'].map((label) => ({ label, text: `Do ${label}` })),
|
|
47
|
-
},
|
|
48
|
-
});
|
|
49
|
-
expect(screen.getByRole('heading', { name: 'Hi Pat' })).toBeTruthy();
|
|
50
|
-
const list = screen.getByRole('list', { name: 'Suggestions' });
|
|
51
|
-
expect(list.querySelectorAll('button')).toHaveLength(4);
|
|
52
|
-
await userEvent.click(screen.getByRole('button', { name: 'B' }));
|
|
53
|
-
expect(onselect).toHaveBeenCalledWith('Do B', expect.objectContaining({ label: 'B' }));
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
describe('AgentChat empty states', () => {
|
|
57
|
-
it('welcomes a new chat and sends a chosen suggestion', async () => {
|
|
58
|
-
const client = fakeClient();
|
|
59
|
-
render(AgentChat, {
|
|
60
|
-
props: {
|
|
61
|
-
...base,
|
|
62
|
-
client: client,
|
|
63
|
-
userName: 'Pat Lee',
|
|
64
|
-
suggestions: [{ label: 'Open lots', text: 'List the open lots' }],
|
|
65
|
-
},
|
|
66
|
-
});
|
|
67
|
-
expect(screen.getByRole('heading', { name: /, Pat$/ })).toBeTruthy();
|
|
68
|
-
await userEvent.click(screen.getByRole('button', { name: 'Open lots' }));
|
|
69
|
-
await waitFor(() => expect(client.streamChat).toHaveBeenCalledOnce());
|
|
70
|
-
expect(client.streamChat.mock.calls[0][0]).toMatchObject({ message: 'List the open lots' });
|
|
71
|
-
expect((await screen.findAllByText('Sure.')).length).toBeGreaterThan(0);
|
|
72
|
-
expect(document.querySelector('[data-agent-welcome]')).toBeNull();
|
|
73
|
-
});
|
|
74
|
-
it('shows a skeleton, not the welcome, while a conversation loads', async () => {
|
|
75
|
-
let resolve;
|
|
76
|
-
const client = fakeClient(vi.fn(() => new Promise((r) => (resolve = r))));
|
|
77
|
-
render(AgentChat, { props: { ...base, client: client, conversationId: 'c-9' } });
|
|
78
|
-
expect(document.querySelector('[data-agent-loading]')).toBeTruthy();
|
|
79
|
-
expect(document.querySelector('[data-agent-welcome]')).toBeNull();
|
|
80
|
-
resolve({
|
|
81
|
-
conversation: { conversationId: 'c-9', activeRequestId: null },
|
|
82
|
-
messages: [
|
|
83
|
-
{
|
|
84
|
-
id: 'u-1',
|
|
85
|
-
role: 'user',
|
|
86
|
-
content: [{ type: 'text', text: 'Earlier question' }],
|
|
87
|
-
createdAt: Date.now(),
|
|
88
|
-
},
|
|
89
|
-
],
|
|
90
|
-
});
|
|
91
|
-
expect(await screen.findByText('Earlier question')).toBeTruthy();
|
|
92
|
-
expect(document.querySelector('[data-agent-loading]')).toBeNull();
|
|
93
|
-
});
|
|
94
|
-
it('explains a failed load and retries it', async () => {
|
|
95
|
-
const getConversation = vi
|
|
96
|
-
.fn()
|
|
97
|
-
.mockRejectedValueOnce(new Error('Network down'))
|
|
98
|
-
.mockResolvedValueOnce({
|
|
99
|
-
conversation: { conversationId: 'c-9', activeRequestId: null },
|
|
100
|
-
messages: [],
|
|
101
|
-
});
|
|
102
|
-
render(AgentChat, {
|
|
103
|
-
props: { ...base, client: fakeClient(getConversation), conversationId: 'c-9' },
|
|
104
|
-
});
|
|
105
|
-
expect(await screen.findByText('Could not load this conversation.')).toBeTruthy();
|
|
106
|
-
expect(document.querySelector('[data-agent-welcome]')).toBeNull();
|
|
107
|
-
// No automatic retry loop.
|
|
108
|
-
await new Promise((r) => setTimeout(r, 50));
|
|
109
|
-
expect(getConversation).toHaveBeenCalledOnce();
|
|
110
|
-
await userEvent.click(screen.getByRole('button', { name: /Try again/ }));
|
|
111
|
-
await waitFor(() => expect(getConversation).toHaveBeenCalledTimes(2));
|
|
112
|
-
await waitFor(() => expect(screen.queryByText('Could not load this conversation.')).toBeNull());
|
|
113
|
-
expect(document.querySelector('[data-agent-welcome]')).toBeTruthy();
|
|
114
|
-
});
|
|
115
|
-
it('renders the welcome when the host gives no empty snippet', () => {
|
|
116
|
-
render(AgentChat, { props: { ...base, client: fakeClient() } });
|
|
117
|
-
expect(document.querySelector('[data-agent-welcome]')).toBeTruthy();
|
|
118
|
-
});
|
|
119
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"composer-keys.test.d.ts","sourceRoot":"","sources":["../src/lib/composer-keys.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { composerKeyAction } from './composer-keys.js';
|
|
3
|
-
describe('composerKeyAction', () => {
|
|
4
|
-
it('sends on Enter and keeps Shift+Enter for a newline', () => {
|
|
5
|
-
expect(composerKeyAction({ key: 'Enter' }, false)).toBe('send');
|
|
6
|
-
expect(composerKeyAction({ key: 'Enter', shiftKey: true }, false)).toBeNull();
|
|
7
|
-
});
|
|
8
|
-
it('ignores Enter that confirms an IME candidate', () => {
|
|
9
|
-
expect(composerKeyAction({ key: 'Enter', isComposing: true }, false)).toBeNull();
|
|
10
|
-
expect(composerKeyAction({ key: 'Enter', keyCode: 229 }, false)).toBeNull();
|
|
11
|
-
});
|
|
12
|
-
it('stops a running reply on Escape only while streaming', () => {
|
|
13
|
-
expect(composerKeyAction({ key: 'Escape' }, true)).toBe('cancel');
|
|
14
|
-
expect(composerKeyAction({ key: 'Escape' }, false)).toBeNull();
|
|
15
|
-
});
|
|
16
|
-
});
|
package/dist/markdown.test.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"markdown.test.d.ts","sourceRoot":"","sources":["../src/lib/markdown.test.ts"],"names":[],"mappings":""}
|