## CHANGES

- Upgraded group chat exports to true GitHub-flavored Markdown rendering! 🚀
- Added `marked` dependency and configured GFM plus line-break handling! 
- Exported messages now support tables, blockquotes, rules, and strikethrough! 🔥
- Code formatting improved: inline code and fenced blocks render correctly! 🧠
- Image embedding revamped: replaces references with base64 before parsing! 🖼️
- New branded export header with Maestro icon, tagline, and links! 🎉
- Export title now reads “Maestro Group Chat Export” for clarity! 🏷️
- Footer attribution updated to <a href="https://runmaestro.ai" target="_blank">runmaestro.ai</a> for consistency! 🌐
- Markdown styling massively enhanced: headings, lists, tables, and code look great! 🎨
- AutoRunLightbox tests updated for portal backdrop and higher z-index! 
This commit is contained in:
Pedram Amini
2025-12-21 20:32:10 -06:00
parent 41680af24a
commit 427840c7f3
5 changed files with 410 additions and 144 deletions

23
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "maestro",
"version": "0.10.0",
"version": "0.10.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "maestro",
"version": "0.10.0",
"version": "0.10.2",
"hasInstallScript": true,
"license": "AGPL 3.0",
"dependencies": {
@@ -31,6 +31,7 @@
"electron-updater": "^6.6.2",
"fastify": "^4.25.2",
"js-tiktoken": "^1.0.21",
"marked": "^17.0.1",
"mermaid": "^11.12.1",
"node-pty": "^1.0.0",
"qrcode": "^1.5.4",
@@ -10151,9 +10152,9 @@
}
},
"node_modules/marked": {
"version": "16.4.2",
"resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz",
"integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==",
"version": "17.0.1",
"resolved": "https://registry.npmjs.org/marked/-/marked-17.0.1.tgz",
"integrity": "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg==",
"license": "MIT",
"bin": {
"marked": "bin/marked.js"
@@ -10543,6 +10544,18 @@
"uuid": "^11.1.0"
}
},
"node_modules/mermaid/node_modules/marked": {
"version": "16.4.2",
"resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz",
"integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==",
"license": "MIT",
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/micromark": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz",

View File

@@ -121,15 +121,24 @@
"target": [
{
"target": "AppImage",
"arch": ["x64", "arm64"]
"arch": [
"x64",
"arm64"
]
},
{
"target": "deb",
"arch": ["x64", "arm64"]
"arch": [
"x64",
"arm64"
]
},
{
"target": "rpm",
"arch": ["x64", "arm64"]
"arch": [
"x64",
"arm64"
]
}
],
"category": "Development",
@@ -198,6 +207,7 @@
"electron-updater": "^6.6.2",
"fastify": "^4.25.2",
"js-tiktoken": "^1.0.21",
"marked": "^17.0.1",
"mermaid": "^11.12.1",
"node-pty": "^1.0.0",
"qrcode": "^1.5.4",

View File

@@ -147,8 +147,8 @@ describe('AutoRunLightbox', () => {
const props = createDefaultProps();
renderWithProviders(<AutoRunLightbox {...props} />);
// Should render the backdrop container
const backdrop = document.querySelector('.fixed.inset-0.z-50');
// Should render the backdrop container (rendered via portal to document.body)
const backdrop = document.querySelector('.fixed.inset-0.z-\\[9999\\]');
expect(backdrop).toBeInTheDocument();
});
@@ -341,7 +341,7 @@ describe('AutoRunLightbox', () => {
const props = createDefaultProps({ onNavigate });
renderWithProviders(<AutoRunLightbox {...props} />);
const backdrop = document.querySelector('.fixed.inset-0.z-50');
const backdrop = document.querySelector('.fixed.inset-0.z-\\[9999\\]');
fireEvent.keyDown(backdrop!, { key: 'ArrowRight' });
expect(onNavigate).toHaveBeenCalledWith('image2.png');
@@ -355,7 +355,7 @@ describe('AutoRunLightbox', () => {
});
renderWithProviders(<AutoRunLightbox {...props} />);
const backdrop = document.querySelector('.fixed.inset-0.z-50');
const backdrop = document.querySelector('.fixed.inset-0.z-\\[9999\\]');
fireEvent.keyDown(backdrop!, { key: 'ArrowLeft' });
expect(onNavigate).toHaveBeenCalledWith('image1.png');
@@ -365,7 +365,7 @@ describe('AutoRunLightbox', () => {
const props = createDefaultProps();
renderWithProviders(<AutoRunLightbox {...props} />);
const backdrop = document.querySelector('.fixed.inset-0.z-50');
const backdrop = document.querySelector('.fixed.inset-0.z-\\[9999\\]');
const event = fireEvent.keyDown(backdrop!, { key: 'ArrowRight' });
// fireEvent.keyDown returns false when preventDefault is called
@@ -376,7 +376,7 @@ describe('AutoRunLightbox', () => {
const props = createDefaultProps();
renderWithProviders(<AutoRunLightbox {...props} />);
const backdrop = document.querySelector('.fixed.inset-0.z-50');
const backdrop = document.querySelector('.fixed.inset-0.z-\\[9999\\]');
const event = fireEvent.keyDown(backdrop!, { key: 'ArrowLeft' });
expect(event).toBe(false);
@@ -390,7 +390,7 @@ describe('AutoRunLightbox', () => {
});
renderWithProviders(<AutoRunLightbox {...props} />);
const backdrop = document.querySelector('.fixed.inset-0.z-50');
const backdrop = document.querySelector('.fixed.inset-0.z-\\[9999\\]');
fireEvent.keyDown(backdrop!, { key: 'ArrowRight' });
expect(onNavigate).not.toHaveBeenCalled();
@@ -406,7 +406,7 @@ describe('AutoRunLightbox', () => {
});
renderWithProviders(<AutoRunLightbox {...props} />);
const backdrop = document.querySelector('.fixed.inset-0.z-50');
const backdrop = document.querySelector('.fixed.inset-0.z-\\[9999\\]');
fireEvent.keyDown(backdrop!, { key: 'ArrowRight' });
expect(onNavigate).not.toHaveBeenCalled();
@@ -450,7 +450,7 @@ describe('AutoRunLightbox', () => {
const props = createDefaultProps({ onDelete });
renderWithProviders(<AutoRunLightbox {...props} />);
const backdrop = document.querySelector('.fixed.inset-0.z-50');
const backdrop = document.querySelector('.fixed.inset-0.z-\\[9999\\]');
fireEvent.keyDown(backdrop!, { key: 'Delete' });
// Confirm modal should appear
@@ -469,7 +469,7 @@ describe('AutoRunLightbox', () => {
const props = createDefaultProps({ onDelete });
renderWithProviders(<AutoRunLightbox {...props} />);
const backdrop = document.querySelector('.fixed.inset-0.z-50');
const backdrop = document.querySelector('.fixed.inset-0.z-\\[9999\\]');
fireEvent.keyDown(backdrop!, { key: 'Backspace' });
// Confirm modal should appear
@@ -486,7 +486,7 @@ describe('AutoRunLightbox', () => {
const props = createDefaultProps();
renderWithProviders(<AutoRunLightbox {...props} />);
const backdrop = document.querySelector('.fixed.inset-0.z-50');
const backdrop = document.querySelector('.fixed.inset-0.z-\\[9999\\]');
const event = fireEvent.keyDown(backdrop!, { key: 'Delete' });
expect(event).toBe(false);
@@ -500,7 +500,7 @@ describe('AutoRunLightbox', () => {
});
renderWithProviders(<AutoRunLightbox {...props} />);
const backdrop = document.querySelector('.fixed.inset-0.z-50');
const backdrop = document.querySelector('.fixed.inset-0.z-\\[9999\\]');
fireEvent.keyDown(backdrop!, { key: 'Delete' });
expect(onDelete).not.toHaveBeenCalled();
@@ -510,7 +510,7 @@ describe('AutoRunLightbox', () => {
const props = createDefaultProps({ onDelete: undefined });
renderWithProviders(<AutoRunLightbox {...props} />);
const backdrop = document.querySelector('.fixed.inset-0.z-50');
const backdrop = document.querySelector('.fixed.inset-0.z-\\[9999\\]');
// This should not throw or cause issues
fireEvent.keyDown(backdrop!, { key: 'Delete' });
// No assertion needed - just verifying it doesn't throw
@@ -750,7 +750,7 @@ describe('AutoRunLightbox', () => {
const props = createDefaultProps({ onClose });
renderWithProviders(<AutoRunLightbox {...props} />);
const backdrop = document.querySelector('.fixed.inset-0.z-50');
const backdrop = document.querySelector('.fixed.inset-0.z-\\[9999\\]');
fireEvent.click(backdrop!);
expect(onClose).toHaveBeenCalled();
@@ -871,7 +871,7 @@ describe('AutoRunLightbox', () => {
const props = createDefaultProps();
renderWithProviders(<AutoRunLightbox {...props} />);
const backdrop = document.querySelector('.fixed.inset-0.z-50');
const backdrop = document.querySelector('.fixed.inset-0.z-\\[9999\\]');
expect(backdrop).toHaveAttribute('tabIndex', '-1');
// The ref={(el) => el?.focus()} should focus the element
expect(document.activeElement).toBe(backdrop);
@@ -951,7 +951,7 @@ describe('AutoRunLightbox', () => {
const props = createDefaultProps();
renderWithProviders(<AutoRunLightbox {...props} />);
const backdrop = document.querySelector('.fixed.inset-0.z-50');
const backdrop = document.querySelector('.fixed.inset-0.z-\\[9999\\]');
expect(backdrop).toHaveClass('bg-black/90');
});
@@ -959,7 +959,7 @@ describe('AutoRunLightbox', () => {
const props = createDefaultProps();
renderWithProviders(<AutoRunLightbox {...props} />);
const backdrop = document.querySelector('.fixed.inset-0.z-50');
const backdrop = document.querySelector('.fixed.inset-0.z-\\[9999\\]');
expect(backdrop).toHaveClass('flex');
expect(backdrop).toHaveClass('items-center');
expect(backdrop).toHaveClass('justify-center');
@@ -1034,7 +1034,7 @@ describe('AutoRunLightbox', () => {
const props = createDefaultProps({ onNavigate });
renderWithProviders(<AutoRunLightbox {...props} />);
const backdrop = document.querySelector('.fixed.inset-0.z-50');
const backdrop = document.querySelector('.fixed.inset-0.z-\\[9999\\]');
// Rapid arrow key presses
for (let i = 0; i < 10; i++) {
@@ -1101,7 +1101,7 @@ describe('AutoRunLightbox', () => {
const props = createDefaultProps();
renderWithProviders(<AutoRunLightbox {...props} />);
const backdrop = document.querySelector('.fixed.inset-0.z-50');
const backdrop = document.querySelector('.fixed.inset-0.z-\\[9999\\]');
expect(backdrop).toHaveAttribute('tabIndex', '-1');
});
});

View File

@@ -124,7 +124,7 @@ describe('groupChatExport', () => {
const html = generateGroupChatExportHtml(groupChat, messages, [], {}, mockTheme);
expect(html).toContain('<title>My Custom Chat - Group Chat Export</title>');
expect(html).toContain('<title>My Custom Chat - Maestro Group Chat Export</title>');
});
it('includes group chat name in header', () => {
@@ -149,6 +149,49 @@ describe('groupChatExport', () => {
});
});
describe('branding', () => {
it('includes Maestro branding section', () => {
const groupChat = createMockGroupChat();
const messages = createMockMessages();
const html = generateGroupChatExportHtml(groupChat, messages, [], {}, mockTheme);
expect(html).toContain('class="branding"');
expect(html).toContain('Maestro');
expect(html).toContain('Multi-agent orchestration');
});
it('includes runmaestro.ai link', () => {
const groupChat = createMockGroupChat();
const messages = createMockMessages();
const html = generateGroupChatExportHtml(groupChat, messages, [], {}, mockTheme);
expect(html).toContain('href="https://runmaestro.ai"');
expect(html).toContain('runmaestro.ai');
});
it('includes GitHub link', () => {
const groupChat = createMockGroupChat();
const messages = createMockMessages();
const html = generateGroupChatExportHtml(groupChat, messages, [], {}, mockTheme);
expect(html).toContain('href="https://github.com/pedramamini/Maestro"');
expect(html).toContain('GitHub');
});
it('includes Maestro logo SVG', () => {
const groupChat = createMockGroupChat();
const messages = createMockMessages();
const html = generateGroupChatExportHtml(groupChat, messages, [], {}, mockTheme);
expect(html).toContain('class="branding-logo"');
expect(html).toContain('<svg');
});
});
describe('theme colors', () => {
it('uses theme colors in CSS variables', () => {
const groupChat = createMockGroupChat();
@@ -310,27 +353,29 @@ describe('groupChatExport', () => {
});
});
describe('content escaping and formatting', () => {
it('escapes HTML special characters in message content', () => {
describe('markdown rendering with marked library', () => {
it('renders tables correctly', () => {
const groupChat = createMockGroupChat();
const messages: GroupChatMessage[] = [
{ timestamp: '2023-12-21T10:00:00Z', from: 'user', content: '<div>test</div>' },
{ timestamp: '2023-12-21T10:00:00Z', from: 'Agent1', content: '| A | B |\n|---|---|\n| 1 | 2 |' },
];
const html = generateGroupChatExportHtml(groupChat, messages, [], {}, mockTheme);
// The message content should be escaped in the rendered HTML
expect(html).toContain('&lt;div&gt;test&lt;/div&gt;');
expect(html).toContain('<table>');
expect(html).toContain('<th>');
expect(html).toContain('<td>');
});
it('escapes HTML in group chat name in title', () => {
const groupChat = createMockGroupChat({ name: '<b>Bold Name</b>' });
const messages = createMockMessages();
it('renders horizontal rules correctly', () => {
const groupChat = createMockGroupChat();
const messages: GroupChatMessage[] = [
{ timestamp: '2023-12-21T10:00:00Z', from: 'Agent1', content: 'Before\n\n---\n\nAfter' },
];
const html = generateGroupChatExportHtml(groupChat, messages, [], {}, mockTheme);
// The title should have escaped HTML
expect(html).toContain('<title>&lt;b&gt;Bold Name&lt;/b&gt; - Group Chat Export</title>');
expect(html).toContain('<hr');
});
it('converts inline code to HTML code tags', () => {
@@ -341,8 +386,7 @@ describe('groupChatExport', () => {
const html = generateGroupChatExportHtml(groupChat, messages, [], {}, mockTheme);
expect(html).toContain('inline-code');
expect(html).toContain('npm install');
expect(html).toContain('<code>npm install</code>');
});
it('converts code blocks to pre tags', () => {
@@ -357,7 +401,7 @@ describe('groupChatExport', () => {
const html = generateGroupChatExportHtml(groupChat, messages, [], {}, mockTheme);
expect(html).toContain('code-block');
expect(html).toContain('<pre>');
expect(html).toContain('const x = 1;');
});
@@ -386,14 +430,17 @@ describe('groupChatExport', () => {
it('converts markdown headers', () => {
const groupChat = createMockGroupChat();
const messages: GroupChatMessage[] = [
{ timestamp: '2023-12-21T10:00:00Z', from: 'Agent1', content: '# Heading 1\n## Heading 2\n### Heading 3' },
{ timestamp: '2023-12-21T10:00:00Z', from: 'Agent1', content: '# Heading 1\n\n## Heading 2\n\n### Heading 3' },
];
const html = generateGroupChatExportHtml(groupChat, messages, [], {}, mockTheme);
expect(html).toContain('<h1>Heading 1</h1>');
expect(html).toContain('<h2>Heading 2</h2>');
expect(html).toContain('<h3>Heading 3</h3>');
expect(html).toContain('<h1');
expect(html).toContain('Heading 1');
expect(html).toContain('<h2');
expect(html).toContain('Heading 2');
expect(html).toContain('<h3');
expect(html).toContain('Heading 3');
});
it('converts markdown bullet lists', () => {
@@ -406,6 +453,7 @@ describe('groupChatExport', () => {
expect(html).toContain('<li>Item 1</li>');
expect(html).toContain('<li>Item 2</li>');
expect(html).toContain('<ul>');
});
it('converts markdown links', () => {
@@ -416,18 +464,30 @@ describe('groupChatExport', () => {
const html = generateGroupChatExportHtml(groupChat, messages, [], {}, mockTheme);
expect(html).toContain('<a href="https://example.com" target="_blank">this link</a>');
expect(html).toContain('href="https://example.com"');
expect(html).toContain('this link');
});
it('converts newlines to br tags', () => {
it('converts blockquotes', () => {
const groupChat = createMockGroupChat();
const messages: GroupChatMessage[] = [
{ timestamp: '2023-12-21T10:00:00Z', from: 'Agent1', content: 'Line 1\nLine 2' },
{ timestamp: '2023-12-21T10:00:00Z', from: 'Agent1', content: '> This is a quote' },
];
const html = generateGroupChatExportHtml(groupChat, messages, [], {}, mockTheme);
expect(html).toContain('<br>');
expect(html).toContain('<blockquote>');
});
it('converts strikethrough text', () => {
const groupChat = createMockGroupChat();
const messages: GroupChatMessage[] = [
{ timestamp: '2023-12-21T10:00:00Z', from: 'Agent1', content: '~~deleted~~' },
];
const html = generateGroupChatExportHtml(groupChat, messages, [], {}, mockTheme);
expect(html).toContain('<del>deleted</del>');
});
});
@@ -442,10 +502,9 @@ describe('groupChatExport', () => {
const html = generateGroupChatExportHtml(groupChat, messages, [], images, mockTheme);
expect(html).toContain('src="data:image/png;base64,abc123"');
expect(html).toContain('class="embedded-image"');
});
it('handles image references in different formats', () => {
it('handles [Image: filename] pattern', () => {
const groupChat = createMockGroupChat();
const messages: GroupChatMessage[] = [
{ timestamp: '2023-12-21T10:00:00Z', from: 'Agent1', content: '[Image: photo.jpg]' },
@@ -533,24 +592,14 @@ describe('groupChatExport', () => {
});
describe('footer', () => {
it('includes Maestro attribution', () => {
it('includes Maestro attribution with runmaestro.ai', () => {
const groupChat = createMockGroupChat();
const messages = createMockMessages();
const html = generateGroupChatExportHtml(groupChat, messages, [], {}, mockTheme);
expect(html).toContain('Maestro');
expect(html).toContain('https://maestro.sh');
});
it('does not include JSON extraction tip', () => {
const groupChat = createMockGroupChat();
const messages = createMockMessages();
const html = generateGroupChatExportHtml(groupChat, messages, [], {}, mockTheme);
// JSON was removed, so no extraction tip
expect(html).not.toContain('embedded JSON');
expect(html).toContain('Exported from');
expect(html).toContain('href="https://runmaestro.ai"');
});
});

File diff suppressed because one or more lines are too long