diff --git a/common/utils/execution_progress.py b/common/utils/execution_progress.py
index 646f6ca..fdd1616 100644
--- a/common/utils/execution_progress.py
+++ b/common/utils/execution_progress.py
@@ -124,7 +124,7 @@ class ExecutionProgressTracker:
yield f"data: {message['data'].decode('utf-8')}\n\n"
# Check processing_type for completion
- if update_data['processing_type'] in ['Task Complete', 'Task Error']:
+ if update_data['processing_type'] in ['Task Complete', 'Task Error', 'EveAI Specialist Complete']:
break
finally:
try:
diff --git a/config/static-manifest/manifest.json b/config/static-manifest/manifest.json
index e401916..3b987e8 100644
--- a/config/static-manifest/manifest.json
+++ b/config/static-manifest/manifest.json
@@ -1,6 +1,6 @@
{
- "dist/chat-client.js": "dist/chat-client.6bfbd765.js",
- "dist/chat-client.css": "dist/chat-client.33f904ba.css",
+ "dist/chat-client.js": "dist/chat-client.24c00fcd.js",
+ "dist/chat-client.css": "dist/chat-client.7d8832b6.css",
"dist/main.js": "dist/main.f3dde0f6.js",
"dist/main.css": "dist/main.c40e57ad.css"
}
\ No newline at end of file
diff --git a/eveai_chat_client/static/assets/js/utils/markdownRenderer.js b/eveai_chat_client/static/assets/js/utils/markdownRenderer.js
new file mode 100644
index 0000000..a927910
--- /dev/null
+++ b/eveai_chat_client/static/assets/js/utils/markdownRenderer.js
@@ -0,0 +1,165 @@
+// Centralized Markdown rendering utility
+// Safe defaults per requirements: no inline HTML, no images, no code blocks, allow tables, links open in new tab with rel attributes
+
+export function renderMarkdown(text, options = {}) {
+ const opts = {
+ allowInlineHTML: false,
+ enableTables: true,
+ enableBreaks: true,
+ enableImages: false,
+ enableCodeBlocks: false,
+ allowInlineCode: false, // inline code currently not allowed
+ linkTargetBlank: true,
+ sidebarAccent: false,
+ consoleWarnOnFallback: true,
+ ...options
+ };
+
+ const input = typeof text === 'string' ? text : String(text ?? '');
+ if (!input) return '';
+
+ // Optional sidebar accent [[...]] -> ... BEFORE parsing
+ const preprocessed = opts.sidebarAccent
+ ? input.replace(/\[\[(.*?)\]\]/g, '$1')
+ : input;
+
+ try {
+ const hasMarked = typeof window !== 'undefined' && window.marked;
+
+ if (!hasMarked) {
+ if (opts.consoleWarnOnFallback) {
+ console.warn('renderMarkdown: Marked not available; falling back to plain text');
+ }
+ return escapeToPlainHtml(preprocessed);
+ }
+
+ // Configure marked options defensively
+ if (typeof window.marked?.use === 'function') {
+ // Create a lightweight renderer with restricted features
+ const renderer = new window.marked.Renderer();
+
+ // Links: target _blank + rel attributes; block unsafe protocols
+ renderer.link = (href, title, text) => {
+ const safeHref = sanitizeUrl(href);
+ if (!safeHref) {
+ return text; // drop link, keep text
+ }
+ const t = opts.linkTargetBlank ? ' target="_blank"' : '';
+ const rel = ' rel="noopener noreferrer nofollow"';
+ const titleAttr = title ? ` title="${escapeHtmlAttr(title)}"` : '';
+ return `${text}`;
+ };
+
+ // Images disabled: either drop or render alt text
+ renderer.image = (href, title, text) => {
+ if (!opts.enableImages) {
+ return text ? escapeToPlainHtml(text) : '';
+ }
+ // If enabled in future, still sanitize
+ const safeHref = sanitizeUrl(href);
+ if (!safeHref) return text ? escapeToPlainHtml(text) : '';
+ const titleAttr = title ? ` title="${escapeHtmlAttr(title)}"` : '';
+ return ``;
+ };
+
+ // Code blocks disabled
+ renderer.code = (code, infostring) => {
+ if (!opts.enableCodeBlocks) {
+ return `
${escapeToPlainHtml(code)}`; // still show as plain
+ }
+ return `${escapeToPlainHtml(code)}`;
+ };
+
+ // Inline code disabled
+ renderer.codespan = (code) => {
+ if (!opts.allowInlineCode) {
+ return escapeToPlainHtml(code);
+ }
+ return `${escapeToPlainHtml(code)}`;
+ };
+
+ // Disallow raw HTML if not allowed
+ const mOptions = {
+ gfm: true,
+ breaks: !!opts.enableBreaks,
+ headerIds: false,
+ mangle: false,
+ renderer
+ };
+
+ // Table support via GFM is on by default; if disabled, override table renderers to simple paragraphs
+ if (!opts.enableTables) {
+ renderer.table = (header, body) => `${header}${body}`;
+ }
+
+ const html = window.marked.parse(preprocessed, mOptions);
+
+ // Sanitize output
+ const sanitized = sanitizeHtml(html);
+ return sanitized;
+ }
+
+ // Fallback older API: window.marked is a function
+ const html = typeof window.marked === 'function' ? window.marked(preprocessed) : String(preprocessed);
+ const sanitized = sanitizeHtml(html);
+ return sanitized;
+ } catch (err) {
+ console.warn('renderMarkdown: error while rendering; falling back to plain text', err);
+ return escapeToPlainHtml(preprocessed);
+ }
+}
+
+// Very small sanitizer if DOMPurify is not provided globally. Prefer window.DOMPurify if available.
+function sanitizeHtml(html) {
+ try {
+ if (typeof window !== 'undefined' && window.DOMPurify && typeof window.DOMPurify.sanitize === 'function') {
+ return window.DOMPurify.sanitize(html, {
+ USE_PROFILES: { html: true },
+ ADD_ATTR: ['target', 'rel', 'title'],
+ ALLOWED_TAGS: undefined // use default safe set
+ });
+ }
+ } catch (e) {
+ console.warn('sanitizeHtml: DOMPurify error, using basic sanitizer', e);
+ }
+ // Basic sanitizer: strip script tags and on* attributes
+ return String(html)
+ .replace(/