diff --git a/common/log.cpp b/common/log.cpp index bd62616d8..2d1e74ad1 100644 --- a/common/log.cpp +++ b/common/log.cpp @@ -11,8 +11,13 @@ #include #include #include +#include #if defined(_WIN32) +# define WIN32_LEAN_AND_MEAN +# ifndef NOMINMAX +# define NOMINMAX +# endif # include # include # define isatty _isatty @@ -62,16 +67,15 @@ static const char* g_col[] = { }; struct common_log_entry { - enum ggml_log_level level; - - bool prefix; - - int64_t timestamp; + enum ggml_log_level level {GGML_LOG_LEVEL_INFO}; std::vector msg; - // signals the worker thread to stop - bool is_end; + int64_t timestamp { 0 }; + bool is_end { false }; // signals the worker thread to stop + bool prefix { false }; + + common_log_entry(size_t size = 256) : msg(size) { } void print(FILE * file = nullptr) const { FILE * fcur = file; @@ -122,22 +126,15 @@ struct common_log_entry { }; struct common_log { - // default capacity - will be expanded if needed - common_log() : common_log(256) {} - - common_log(size_t capacity) { - file = nullptr; - prefix = false; + // default capacity + common_log(size_t capacity = 512) { + file = nullptr; + prefix = false; timestamps = false; - running = false; - t_start = t_us(); - - // initial message size - will be expanded if longer messages arrive - entries.resize(capacity); - for (auto & entry : entries) { - entry.msg.resize(256); - } + running = false; + t_start = t_us(); + queue.resize(capacity, common_log_entry(256)); head = 0; tail = 0; @@ -152,9 +149,10 @@ struct common_log { } private: - std::mutex mtx; - std::thread thrd; - std::condition_variable cv; + std::mutex mtx; + std::thread thrd; + std::condition_variable cv_new; // new entry + std::condition_variable cv_full; // wait on full FILE * file; @@ -164,24 +162,53 @@ private: int64_t t_start; - // ring buffer of entries - std::vector entries; + // queue of entries + std::vector queue; size_t head; size_t tail; - // worker thread copies into this - common_log_entry cur; + bool print_entry(const common_log_entry & e) const { + if (e.is_end) return true; + + e.print(); + if (file) { + e.print(file); + } + return false; + } + + bool flush_queue(size_t start_head, size_t end_tail, size_t & out_head) const { + bool stop = false; + size_t h = start_head; + while (h != end_tail && !stop) { + stop = print_entry(queue[h]); + h = (h + 1) % queue.size(); + } + out_head = h; + return stop; + } public: + bool is_full() const { + return ((tail + 1) % queue.size()) == head; + } + + bool is_empty() const { + return head == tail; + } + void add(enum ggml_log_level level, const char * fmt, va_list args) { - std::lock_guard lock(mtx); + std::unique_lock lock(mtx); + + // block if the queue is full + cv_full.wait(lock, [this]() { return !running || !is_full(); }); if (!running) { // discard messages while the worker thread is paused return; } - auto & entry = entries[tail]; + auto & entry = queue[tail]; { // cannot use args twice, so make a copy in case we need to expand the buffer @@ -216,38 +243,16 @@ public: va_end(args_copy); } - entry.level = level; - entry.prefix = prefix; + entry.is_end = false; + entry.level = level; + entry.prefix = prefix; entry.timestamp = 0; if (timestamps) { entry.timestamp = t_us() - t_start; } - entry.is_end = false; - tail = (tail + 1) % entries.size(); - if (tail == head) { - // expand the buffer - std::vector new_entries(2*entries.size()); - - size_t new_tail = 0; - - do { - new_entries[new_tail] = std::move(entries[head]); - - head = (head + 1) % entries.size(); - new_tail = (new_tail + 1); - } while (head != tail); - - head = 0; - tail = new_tail; - - for (size_t i = tail; i < new_entries.size(); i++) { - new_entries[i].msg.resize(256); - } - - entries = std::move(new_entries); - } - cv.notify_one(); + tail = (tail + 1) % queue.size(); + cv_new.notify_one(); } void resume() { @@ -261,23 +266,24 @@ public: thrd = std::thread([this]() { while (true) { - { - std::unique_lock lock(mtx); - cv.wait(lock, [this]() { return head != tail; }); - cur = entries[head]; + std::unique_lock lock(mtx); + cv_new.wait(lock, [this]() { return !is_empty(); }); - head = (head + 1) % entries.size(); - } + size_t cached_head = head; + size_t cached_tail = tail; - if (cur.is_end) { + lock.unlock(); // drop the lock during flush + + size_t next_head; + bool stop = flush_queue(cached_head, cached_tail, next_head); + + lock.lock(); + head = next_head; + cv_full.notify_all(); + + if (stop) { break; } - - cur.print(); // stdout and stderr - - if (file) { - cur.print(file); - } } }); } @@ -293,13 +299,13 @@ public: running = false; // push an entry to signal the worker thread to stop - { - auto & entry = entries[tail]; - entry.is_end = true; + auto & entry = queue[tail]; + entry.is_end = true; + tail = (tail + 1) % queue.size(); - tail = (tail + 1) % entries.size(); - } - cv.notify_one(); + // wakeup everyone + cv_new.notify_one(); + cv_full.notify_all(); } thrd.join(); diff --git a/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp b/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp index 63503494d..b3a8ffcc1 100644 --- a/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp +++ b/ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp @@ -422,7 +422,7 @@ std::map merge_maps(const std::map> compiles; +static std::deque> compiles; void string_to_spv(std::string name, const std::string& source, const std::map& defines, bool fp16 = true, bool coopmat = false, bool coopmat2 = false, bool f16acc = false, const std::string& suffix = "") { name = name + (f16acc ? "_f16acc" : "") + (coopmat ? "_cm1" : "") + (coopmat2 ? "_cm2" : (fp16 ? "" : "_fp32")) + suffix; std::string out_path = join_paths(output_dir, name + ".spv"); @@ -452,6 +452,11 @@ void string_to_spv(std::string name, const std::string& source, const std::mapget_weight(w); if (lw == nullptr) { @@ -1106,18 +1110,24 @@ ggml_tensor * llm_graph_context::build_lora_mm( res = ggml_add(ctx0, res, ab_cur); } - if (w_s) { - res = ggml_mul(ctx0, res, w_s); - } - return res; } ggml_tensor * llm_graph_context::build_lora_mm_id( ggml_tensor * w, // ggml_tensor * as ggml_tensor * cur, // ggml_tensor * b - ggml_tensor * ids) const { + ggml_tensor * ids, + ggml_tensor * w_s) const { ggml_tensor * res = ggml_mul_mat_id(ctx0, w, cur, ids); + + if (w_s) { + const int64_t n_expert = w_s->ne[0]; + const int64_t n_tokens = cur->ne[2]; + ggml_tensor * s = ggml_reshape_3d(ctx0, w_s, 1, n_expert, 1); + s = ggml_repeat_4d(ctx0, s, 1, n_expert, n_tokens, 1); + s = ggml_get_rows(ctx0, s, ids); + res = ggml_mul(ctx0, res, s); + } for (const auto & lora : *loras) { llama_adapter_lora_weight * lw = lora.first->get_weight(w); if (lw == nullptr) { @@ -1269,6 +1279,29 @@ ggml_tensor * llm_graph_context::build_ffn( llm_ffn_op_type type_op, llm_ffn_gate_type type_gate, int il) const { + // NVFP4 support is currently restricted to + // 1) LORA absence (*_s would be applied after LORA residual, which is incorrect) + // 2) bias absense (*_s would be applied after bias addition, which is incorrect) + // TODO: disambiguate LLM-architectural scales (which use *_s) from NVFP4 scale_2 (which also uses *_s currently) + auto has_lora = [this](ggml_tensor * w) { + if (!w) { + return false; + } + for (const auto & lora : *loras) { + if (lora.first->get_weight(w) != nullptr) { + return true; + } + } + return false; + }; + + GGML_ASSERT(!up_s || !up_b || !up || up->type != GGML_TYPE_NVFP4); + GGML_ASSERT(!gate_s || !gate_b || !gate || gate->type != GGML_TYPE_NVFP4); + GGML_ASSERT(!down_s || !down_b || !down || down->type != GGML_TYPE_NVFP4); + GGML_ASSERT(!up_s || !up || up->type != GGML_TYPE_NVFP4 || !has_lora(up)); + GGML_ASSERT(!gate_s || !gate || gate->type != GGML_TYPE_NVFP4 || !has_lora(gate)); + GGML_ASSERT(!down_s || !down || down->type != GGML_TYPE_NVFP4 || !has_lora(down)); + ggml_tensor * tmp = up ? build_lora_mm(up, cur) : cur; cb(tmp, "ffn_up", il); @@ -1627,23 +1660,18 @@ ggml_tensor * llm_graph_context::build_moe_ffn( if (gate_up_exps) { // merged gate_up path: one mul_mat_id, then split into gate and up views - ggml_tensor * gate_up = build_lora_mm_id(gate_up_exps, cur, selected_experts); // [n_ff*2, n_expert_used, n_tokens] + ggml_tensor * gate_up = build_lora_mm_id(gate_up_exps, cur, selected_experts, up_exps_s); // [n_ff*2, n_expert_used, n_tokens] cb(gate_up, "ffn_moe_gate_up", il); + if (up_exps_s) { + cb(gate_up, "ffn_moe_gate_up_scaled", il); + } + if (gate_up_exps_b) { gate_up = ggml_add_id(ctx0, gate_up, gate_up_exps_b, selected_experts); cb(gate_up, "ffn_moe_gate_up_biased", il); } - // apply per-expert scale2 to merged gate_up (use up_exps_s since gate and up are fused) - if (up_exps_s) { - ggml_tensor * s = ggml_reshape_3d(ctx0, up_exps_s, 1, n_expert, 1); - s = ggml_repeat_4d(ctx0, s, 1, n_expert, n_tokens, 1); - s = ggml_get_rows(ctx0, s, selected_experts); // [1, n_expert_used, n_tokens] - gate_up = ggml_mul(ctx0, gate_up, s); - cb(gate_up, "ffn_moe_gate_up_scaled", il); - } - const int64_t n_ff = gate_up->ne[0] / 2; cur = ggml_view_3d(ctx0, gate_up, n_ff, gate_up->ne[1], gate_up->ne[2], gate_up->nb[1], gate_up->nb[2], 0); cb(cur, "ffn_moe_gate", il); @@ -1651,43 +1679,33 @@ ggml_tensor * llm_graph_context::build_moe_ffn( cb(up, "ffn_moe_up", il); } else { // separate gate and up path - up = build_lora_mm_id(up_exps, cur, selected_experts); // [n_ff, n_expert_used, n_tokens] + up = build_lora_mm_id(up_exps, cur, selected_experts, up_exps_s); // [n_ff, n_expert_used, n_tokens] cb(up, "ffn_moe_up", il); + if (up_exps_s) { + cb(up, "ffn_moe_up_scaled", il); + } + if (up_exps_b) { up = ggml_add_id(ctx0, up, up_exps_b, selected_experts); cb(up, "ffn_moe_up_biased", il); } - // apply per-expert scale2 to up - if (up_exps_s) { - ggml_tensor * s = ggml_reshape_3d(ctx0, up_exps_s, 1, n_expert, 1); - s = ggml_repeat_4d(ctx0, s, 1, n_expert, n_tokens, 1); - s = ggml_get_rows(ctx0, s, selected_experts); // [1, n_expert_used, n_tokens] - up = ggml_mul(ctx0, up, s); - cb(up, "ffn_moe_up_scaled", il); - } - if (gate_exps) { - cur = build_lora_mm_id(gate_exps, cur, selected_experts); // [n_ff, n_expert_used, n_tokens] + cur = build_lora_mm_id(gate_exps, cur, selected_experts, gate_exps_s); // [n_ff, n_expert_used, n_tokens] cb(cur, "ffn_moe_gate", il); } else { cur = up; } + if (gate_exps_s) { + cb(cur, "ffn_moe_gate_scaled", il); + } + if (gate_exps_b) { cur = ggml_add_id(ctx0, cur, gate_exps_b, selected_experts); cb(cur, "ffn_moe_gate_biased", il); } - - // apply per-expert scale2 to gate - if (gate_exps_s) { - ggml_tensor * s = ggml_reshape_3d(ctx0, gate_exps_s, 1, n_expert, 1); - s = ggml_repeat_4d(ctx0, s, 1, n_expert, n_tokens, 1); - s = ggml_get_rows(ctx0, s, selected_experts); // [1, n_expert_used, n_tokens] - cur = ggml_mul(ctx0, cur, s); - cb(cur, "ffn_moe_gate_scaled", il); - } } const bool has_gate = gate_exps || gate_up_exps; @@ -1759,23 +1777,18 @@ ggml_tensor * llm_graph_context::build_moe_ffn( GGML_ABORT("fatal error"); } - experts = build_lora_mm_id(down_exps, cur, selected_experts); // [n_embd, n_expert_used, n_tokens] + experts = build_lora_mm_id(down_exps, cur, selected_experts, down_exps_s); // [n_embd, n_expert_used, n_tokens] cb(experts, "ffn_moe_down", il); + if (down_exps_s) { + cb(experts, "ffn_moe_down_scaled", il); + } + if (down_exps_b) { experts = ggml_add_id(ctx0, experts, down_exps_b, selected_experts); cb(experts, "ffn_moe_down_biased", il); } - // apply per-expert scale2 to down - if (down_exps_s) { - ggml_tensor * s = ggml_reshape_3d(ctx0, down_exps_s, 1, n_expert, 1); - s = ggml_repeat_4d(ctx0, s, 1, n_expert, n_tokens, 1); - s = ggml_get_rows(ctx0, s, selected_experts); // [1, n_expert_used, n_tokens] - experts = ggml_mul(ctx0, experts, s); - cb(experts, "ffn_moe_down_scaled", il); - } - if (!weight_before_ffn) { experts = ggml_mul(ctx0, experts, weights); cb(experts, "ffn_moe_weighted", il); diff --git a/src/llama-graph.h b/src/llama-graph.h index cc5cfe51d..5e8a65835 100644 --- a/src/llama-graph.h +++ b/src/llama-graph.h @@ -853,11 +853,12 @@ struct llm_graph_context { ggml_tensor * cur, ggml_tensor * w_s = nullptr) const; - // do mat_mul_id, while optionally apply lora + // do mat_mul_id, while optionally apply lora and per-expert scale ggml_tensor * build_lora_mm_id( ggml_tensor * w, // ggml_tensor * as ggml_tensor * cur, // ggml_tensor * b - ggml_tensor * ids) const; + ggml_tensor * ids, + ggml_tensor * w_s = nullptr) const; ggml_tensor * build_norm( ggml_tensor * cur, diff --git a/tools/ui/src/lib/components/app/content/MarkdownContent/MarkdownContent.svelte b/tools/ui/src/lib/components/app/content/MarkdownContent/MarkdownContent.svelte index 7139d2e63..8ac7f9448 100644 --- a/tools/ui/src/lib/components/app/content/MarkdownContent/MarkdownContent.svelte +++ b/tools/ui/src/lib/components/app/content/MarkdownContent/MarkdownContent.svelte @@ -41,6 +41,7 @@ DATA_ERROR_HANDLED_ATTR, BOOL_TRUE_STRING, SETTINGS_KEYS, + CODE_BLOCK_HEADER_CLASS, MERMAID_WRAPPER_CLASS, MERMAID_BLOCK_CLASS, MERMAID_LANGUAGE, @@ -53,7 +54,11 @@ SVG_TAG_PREFIX, SVG_SOURCE_ATTR, SVG_RENDERED_ATTR, - SVG_INLINE_SHADOW_STYLE + SVG_INLINE_SHADOW_STYLE, + TOGGLE_SOURCE_BTN_CLASS, + DIAGRAM_VIEW_MODE_ATTR, + DIAGRAM_VIEW_RENDERED, + DIAGRAM_VIEW_SOURCE } from '$lib/constants'; import { ColorMode, UrlProtocol } from '$lib/enums'; import { FileTypeText } from '$lib/enums/files.enums'; @@ -501,6 +506,23 @@ async function handleMermaidClick(event: MouseEvent) { const target = event.target as HTMLElement; + // Toggle a diagram block between its rendered view and its source view. + // Shared by mermaid and svg, css drives the visibility from the wrapper mode. + const toggleBtn = target.closest(`.${TOGGLE_SOURCE_BTN_CLASS}`); + if (toggleBtn) { + event.preventDefault(); + event.stopPropagation(); + + const wrapper = toggleBtn.closest(`.${MERMAID_WRAPPER_CLASS}, .${SVG_WRAPPER_CLASS}`); + if (!wrapper) return; + + const isSource = wrapper.getAttribute(DIAGRAM_VIEW_MODE_ATTR) === DIAGRAM_VIEW_SOURCE; + const next = isSource ? DIAGRAM_VIEW_RENDERED : DIAGRAM_VIEW_SOURCE; + wrapper.setAttribute(DIAGRAM_VIEW_MODE_ATTR, next); + toggleBtn.setAttribute('aria-pressed', String(!isSource)); + return; + } + // Check if clicking on copy or preview button in mermaid block const copyBtn = target.closest(`.${MERMAID_WRAPPER_CLASS} .copy-code-btn`); const previewBtn = target.closest(`.${MERMAID_WRAPPER_CLASS} .preview-code-btn`); @@ -573,6 +595,11 @@ } } + // A click on the header chrome targets the action buttons, never the + // diagram. Guard so a header click can not fall through to the click to + // zoom branches below, whatever the scroll position or stacking. + if (target.closest(`.${CODE_BLOCK_HEADER_CLASS}`)) return; + // Open preview when clicking the svg block itself. A final block carries its // source, a streaming block does not and is mirrored live into the dialog. const svgEl = target.closest(`.${SVG_BLOCK_CLASS}`); diff --git a/tools/ui/src/lib/components/app/content/MarkdownContent/markdown-content.css b/tools/ui/src/lib/components/app/content/MarkdownContent/markdown-content.css index 072db383d..b0e04ca62 100644 --- a/tools/ui/src/lib/components/app/content/MarkdownContent/markdown-content.css +++ b/tools/ui/src/lib/components/app/content/MarkdownContent/markdown-content.css @@ -300,7 +300,8 @@ div.markdown-user-content :global(.table-wrapper) { } .markdown-content :global(.copy-code-btn), -.markdown-content :global(.preview-code-btn) { +.markdown-content :global(.preview-code-btn), +.markdown-content :global(.toggle-source-btn) { display: flex; align-items: center; justify-content: center; @@ -312,15 +313,22 @@ div.markdown-user-content :global(.table-wrapper) { } .markdown-content :global(.copy-code-btn:hover), -.markdown-content :global(.preview-code-btn:hover) { +.markdown-content :global(.preview-code-btn:hover), +.markdown-content :global(.toggle-source-btn:hover) { transform: scale(1.05); } .markdown-content :global(.copy-code-btn:active), -.markdown-content :global(.preview-code-btn:active) { +.markdown-content :global(.preview-code-btn:active), +.markdown-content :global(.toggle-source-btn:active) { transform: scale(0.95); } +/* Pressed state marks the source view as active */ +.markdown-content :global(.toggle-source-btn[aria-pressed='true']) { + color: var(--primary); +} + .markdown-content :global(.code-block-wrapper pre) { background: transparent; margin: 0; @@ -629,8 +637,8 @@ div.markdown-user-content :global(.table-wrapper) { overflow-y: auto; overflow-x: auto; display: flex; - align-items: center; - justify-content: center; + align-items: safe center; + justify-content: safe center; padding: 3rem 1rem 1rem; } @@ -645,7 +653,9 @@ div.markdown-user-content :global(.table-wrapper) { overflow-y: visible; } -/* Diagram block uses same header styling as code blocks */ +/* Diagram block uses same header styling as code blocks. The header floats over + scrollable diagram content and stays transparent, so the overflow shows up to + the box edge. It keeps a z-index so it stays the click target above content. */ .markdown-content :global(.mermaid-block-wrapper .code-block-header), .markdown-content :global(.svg-block-wrapper .code-block-header) { display: flex; @@ -657,6 +667,7 @@ div.markdown-user-content :global(.table-wrapper) { top: 0; left: 0; right: 0; + z-index: 2; } .markdown-content :global(.mermaid-block-wrapper .code-block-actions), @@ -683,6 +694,31 @@ div.markdown-user-content :global(.table-wrapper) { padding: 3rem 1rem; } +/* Source view stays hidden while the block renders, css swaps the two views + from the wrapper mode so the click handler only flips one attribute. The view + reuses the code block scroll container, so it matches the app code blocks. */ +.markdown-content :global(.diagram-source) { + display: none; + text-align: left; +} + +.markdown-content :global(.diagram-source pre) { + background: transparent; + margin: 0; + border-radius: 0; + border: none; + font-size: 0.875rem; +} + +.markdown-content :global([data-view-mode='source'] .mermaid-scroll-container), +.markdown-content :global([data-view-mode='source'] .svg-scroll-container) { + display: none; +} + +.markdown-content :global([data-view-mode='source'] .diagram-source) { + display: block; +} + /* Streaming mermaid block - empty preview box */ .mermaid-streaming-block { min-height: 300px; diff --git a/tools/ui/src/lib/components/app/content/MarkdownContent/plugins/rehype/code-block-utils.ts b/tools/ui/src/lib/components/app/content/MarkdownContent/plugins/rehype/code-block-utils.ts index 732315464..f1dd867e8 100644 --- a/tools/ui/src/lib/components/app/content/MarkdownContent/plugins/rehype/code-block-utils.ts +++ b/tools/ui/src/lib/components/app/content/MarkdownContent/plugins/rehype/code-block-utils.ts @@ -7,12 +7,16 @@ import type { Element, ElementContent } from 'hast'; import { CODE_BLOCK_HEADER_CLASS, CODE_BLOCK_ACTIONS_CLASS, + CODE_BLOCK_SCROLL_CONTAINER_CLASS, CODE_LANGUAGE_CLASS, COPY_CODE_BTN_CLASS, PREVIEW_CODE_BTN_CLASS, + TOGGLE_SOURCE_BTN_CLASS, + DIAGRAM_SOURCE_CLASS, RELATIVE_CLASS, COPY_ICON_SVG, - PREVIEW_ICON_SVG + PREVIEW_ICON_SVG, + CODE_ICON_SVG } from '$lib/constants'; export interface BlockIdGenerator { @@ -32,14 +36,16 @@ export function createIconElement(svg: string): Element { } /** - * Creates a button element with icon. + * Creates a button element with icon. Extra properties merge onto the button, + * which lets a stateful button carry attributes like aria-pressed. */ export function createButton( className: string, title: string, iconSvg: string, id: string, - idAttribute: string + idAttribute: string, + extraProperties: Record = {} ): Element { return { type: 'element', @@ -48,7 +54,8 @@ export function createButton( className: [className], [idAttribute]: id, title, - type: 'button' + type: 'button', + ...extraProperties }, children: [createIconElement(iconSvg)] }; @@ -72,6 +79,52 @@ export function createPreviewButton( return createButton(PREVIEW_CODE_BTN_CLASS, title, PREVIEW_ICON_SVG, id, idAttribute); } +/** + * Creates a button that toggles a diagram block between its rendered view and + * its source view. aria-pressed starts false, the rendered view is the default. + */ +export function createToggleSourceButton( + id: string, + idAttribute: string, + title: string = 'Toggle source' +): Element { + return createButton(TOGGLE_SOURCE_BTN_CLASS, title, CODE_ICON_SVG, id, idAttribute, { + 'aria-pressed': 'false' + }); +} + +/** + * Creates a source view for a diagram block. It reuses the code block scroll + * container so it matches the app code blocks, and wraps the highlighted code + * element captured at transform time. A missing code element falls back to a + * plain code node built from the raw source. + */ +export function createSourceView( + codeElement: Element | undefined, + source: string, + language: string +): Element { + const code: Element = codeElement ?? { + type: 'element', + tagName: 'code', + properties: { className: ['hljs', `language-${language}`] }, + children: [{ type: 'text', value: source }] + }; + return { + type: 'element', + tagName: 'div', + properties: { className: [DIAGRAM_SOURCE_CLASS, CODE_BLOCK_SCROLL_CONTAINER_CLASS] }, + children: [ + { + type: 'element', + tagName: 'pre', + properties: {}, + children: [code] + } + ] + }; +} + /** * Creates a block header with language label and action buttons. */ @@ -116,14 +169,17 @@ export function createScrollContainer(preElement: Element, scrollContainerClass: } /** - * Creates a wrapper element with header and scroll container. + * Creates a wrapper element with header and scroll container. Extra children + * append after the scroll container, which lets a block carry a source view + * alongside its rendered output. */ export function createWrapper( header: Element, preElement: Element, wrapperClass: string, scrollContainerClass: string, - additionalAttributes?: Record + additionalAttributes?: Record, + extraChildren: Element[] = [] ): Element { return { type: 'element', @@ -132,7 +188,7 @@ export function createWrapper( className: [wrapperClass, RELATIVE_CLASS], ...additionalAttributes } as Element['properties'], - children: [header, createScrollContainer(preElement, scrollContainerClass)] + children: [header, createScrollContainer(preElement, scrollContainerClass), ...extraChildren] }; } diff --git a/tools/ui/src/lib/components/app/content/MarkdownContent/plugins/rehype/enhance-mermaid-blocks.ts b/tools/ui/src/lib/components/app/content/MarkdownContent/plugins/rehype/enhance-mermaid-blocks.ts index f9decf206..4007c20a1 100644 --- a/tools/ui/src/lib/components/app/content/MarkdownContent/plugins/rehype/enhance-mermaid-blocks.ts +++ b/tools/ui/src/lib/components/app/content/MarkdownContent/plugins/rehype/enhance-mermaid-blocks.ts @@ -19,12 +19,17 @@ import { MERMAID_BLOCK_CLASS, MERMAID_LANGUAGE, MERMAID_SYNTAX_ATTR, - MERMAID_ID_ATTR + MERMAID_ID_ATTR, + DIAGRAM_VIEW_MODE_ATTR, + DIAGRAM_VIEW_RENDERED } from '$lib/constants'; +import type { DiagramPreData } from './pre-transform'; import { createBlockHeader, createCopyButton, createPreviewButton, + createToggleSourceButton, + createSourceView, createWrapper, generateBlockId } from './code-block-utils'; @@ -75,16 +80,23 @@ export const rehypeEnhanceMermaidBlocks: Plugin<[], Root> = () => { const actions = [ createCopyButton(mermaidId, MERMAID_ID_ATTR, 'Copy mermaid syntax'), + createToggleSourceButton(mermaidId, MERMAID_ID_ATTR, 'Toggle mermaid source'), createPreviewButton(mermaidId, MERMAID_ID_ATTR, 'Preview diagram') ]; const header = createBlockHeader(MERMAID_LANGUAGE, mermaidId, MERMAID_ID_ATTR, actions); + const preservedCode = (node.data as DiagramPreData | undefined)?.sourceCode; + const sourceView = createSourceView(preservedCode, diagramText, MERMAID_LANGUAGE); const wrapper = createWrapper( header, node, MERMAID_WRAPPER_CLASS, MERMAID_SCROLL_CONTAINER_CLASS, - { [MERMAID_ID_ATTR]: mermaidId } + { + [MERMAID_ID_ATTR]: mermaidId, + [DIAGRAM_VIEW_MODE_ATTR]: DIAGRAM_VIEW_RENDERED + }, + [sourceView] ); // Replace pre with wrapper in parent diff --git a/tools/ui/src/lib/components/app/content/MarkdownContent/plugins/rehype/enhance-svg-blocks.ts b/tools/ui/src/lib/components/app/content/MarkdownContent/plugins/rehype/enhance-svg-blocks.ts index e5e7514ed..55bcb6065 100644 --- a/tools/ui/src/lib/components/app/content/MarkdownContent/plugins/rehype/enhance-svg-blocks.ts +++ b/tools/ui/src/lib/components/app/content/MarkdownContent/plugins/rehype/enhance-svg-blocks.ts @@ -18,12 +18,17 @@ import { SVG_BLOCK_CLASS, SVG_LANGUAGE, SVG_SOURCE_ATTR, - SVG_ID_ATTR + SVG_ID_ATTR, + DIAGRAM_VIEW_MODE_ATTR, + DIAGRAM_VIEW_RENDERED } from '$lib/constants'; +import type { DiagramPreData } from './pre-transform'; import { createBlockHeader, createCopyButton, createPreviewButton, + createToggleSourceButton, + createSourceView, createWrapper, generateBlockId } from './code-block-utils'; @@ -65,13 +70,24 @@ export const rehypeEnhanceSvgBlocks: Plugin<[], Root> = () => { const actions = [ createCopyButton(svgId, SVG_ID_ATTR, 'Copy svg source'), + createToggleSourceButton(svgId, SVG_ID_ATTR, 'Toggle svg source'), createPreviewButton(svgId, SVG_ID_ATTR, 'Preview svg') ]; const header = createBlockHeader(SVG_LANGUAGE, svgId, SVG_ID_ATTR, actions); - const wrapper = createWrapper(header, node, SVG_WRAPPER_CLASS, SVG_SCROLL_CONTAINER_CLASS, { - [SVG_ID_ATTR]: svgId - }); + const preservedCode = (node.data as DiagramPreData | undefined)?.sourceCode; + const sourceView = createSourceView(preservedCode, svgSource, SVG_LANGUAGE); + const wrapper = createWrapper( + header, + node, + SVG_WRAPPER_CLASS, + SVG_SCROLL_CONTAINER_CLASS, + { + [SVG_ID_ATTR]: svgId, + [DIAGRAM_VIEW_MODE_ATTR]: DIAGRAM_VIEW_RENDERED + }, + [sourceView] + ); // Replace pre with wrapper in parent (parent.children as ElementContent[])[index] = wrapper; diff --git a/tools/ui/src/lib/components/app/content/MarkdownContent/plugins/rehype/pre-transform.ts b/tools/ui/src/lib/components/app/content/MarkdownContent/plugins/rehype/pre-transform.ts index 06848eb26..7aa967bb8 100644 --- a/tools/ui/src/lib/components/app/content/MarkdownContent/plugins/rehype/pre-transform.ts +++ b/tools/ui/src/lib/components/app/content/MarkdownContent/plugins/rehype/pre-transform.ts @@ -2,6 +2,15 @@ import type { Plugin } from 'unified'; import type { Root, Element, ElementContent, Text } from 'hast'; import { visit } from 'unist-util-visit'; +/** + * Metadata a diagram pre carries on its unist data field. The source code holds + * the highlighted code element captured before the pre became a render target, + * which the enhancer reuses to build a matching source view. + */ +export interface DiagramPreData { + sourceCode: Element; +} + /** * Recursively extracts all text content from a HAST node. * Handles nested elements (e.g., span wrappers from syntax highlighting). @@ -69,7 +78,10 @@ export function createPreTransform( properties: { className: [targetClass] }, - children: [{ type: 'text', value: text } as Text] + children: [{ type: 'text', value: text } as Text], + // Keep the highlighted code element so the block can offer a source + // view that matches the app code blocks without re highlighting. + data: { sourceCode: codeElement } satisfies DiagramPreData }; (parent.children as ElementContent[])[index] = pre; diff --git a/tools/ui/src/lib/constants/diagram-blocks.ts b/tools/ui/src/lib/constants/diagram-blocks.ts new file mode 100644 index 000000000..caeb6b5b3 --- /dev/null +++ b/tools/ui/src/lib/constants/diagram-blocks.ts @@ -0,0 +1,9 @@ +// Shared constants for diagram blocks (mermaid and svg) that toggle between a +// rendered view and a source view. The wrapper carries the active mode, css +// drives the visibility, the click handler only flips the attribute. + +export const DIAGRAM_VIEW_MODE_ATTR = 'data-view-mode'; +export const DIAGRAM_VIEW_RENDERED = 'rendered'; +export const DIAGRAM_VIEW_SOURCE = 'source'; +export const DIAGRAM_SOURCE_CLASS = 'diagram-source'; +export const TOGGLE_SOURCE_BTN_CLASS = 'toggle-source-btn'; diff --git a/tools/ui/src/lib/constants/icons.ts b/tools/ui/src/lib/constants/icons.ts index a9448c2a6..6ef02c4cb 100644 --- a/tools/ui/src/lib/constants/icons.ts +++ b/tools/ui/src/lib/constants/icons.ts @@ -39,3 +39,5 @@ export const MODALITY_LABELS = { export const COPY_ICON_SVG = ``; export const PREVIEW_ICON_SVG = ``; + +export const CODE_ICON_SVG = ``; diff --git a/tools/ui/src/lib/constants/index.ts b/tools/ui/src/lib/constants/index.ts index 07e441112..c51d84cdc 100644 --- a/tools/ui/src/lib/constants/index.ts +++ b/tools/ui/src/lib/constants/index.ts @@ -30,6 +30,7 @@ export * from './literal-html'; export * from './markdown'; export * from './mermaid-blocks'; export * from './svg-blocks'; +export * from './diagram-blocks'; export * from './max-bundle-size'; export * from './mcp'; export * from './mcp-form'; diff --git a/tools/ui/src/lib/constants/mcp.ts b/tools/ui/src/lib/constants/mcp.ts index 918eb9f94..5b11f989e 100644 --- a/tools/ui/src/lib/constants/mcp.ts +++ b/tools/ui/src/lib/constants/mcp.ts @@ -84,3 +84,8 @@ export const MCP_TRANSPORT_ICONS: Record = { [MCPTransportType.STREAMABLE_HTTP]: Globe, [MCPTransportType.SSE]: Radio }; + +/** Standard SSE endpoint path indicators */ +export const MCP_SSE_ENDPOINT = '/sse'; +export const MCP_SSE_ENDPOINT_SLASH = '/sse/'; +export const MCP_SSE_ENDPOINT_QUERY = '/sse?'; diff --git a/tools/ui/src/lib/services/mcp.service.ts b/tools/ui/src/lib/services/mcp.service.ts index d596381aa..0aa58dc5d 100644 --- a/tools/ui/src/lib/services/mcp.service.ts +++ b/tools/ui/src/lib/services/mcp.service.ts @@ -16,7 +16,8 @@ import { DEFAULT_MCP_CONFIG, DEFAULT_CLIENT_VERSION, DEFAULT_IMAGE_MIME_TYPE, - MCP_PARTIAL_REDACT_HEADERS + MCP_PARTIAL_REDACT_HEADERS, + CORS_PROXY_ENDPOINT } from '$lib/constants'; import { MCPConnectionPhase, @@ -236,6 +237,36 @@ export class MCPService { return { fetch: async (input, init) => { + if (useProxy && typeof window !== 'undefined') { + let requestUrlStr = ''; + if (typeof input === 'string') { + requestUrlStr = input; + } else if (input instanceof URL) { + requestUrlStr = input.href; + } + + if (requestUrlStr) { + const parsedRequestUrl = new URL(requestUrlStr, window.location.origin); + if ( + parsedRequestUrl.origin === window.location.origin && + !parsedRequestUrl.pathname.includes(CORS_PROXY_ENDPOINT) + ) { + const originalConfigUrl = new URL(config.url); + const realTargetUrl = new URL( + parsedRequestUrl.pathname + parsedRequestUrl.search, + originalConfigUrl.origin + ); + const proxiedUrl = buildProxiedUrl(realTargetUrl.href); + + if (typeof input === 'string') { + input = proxiedUrl.href; + } else if (input instanceof URL) { + input = proxiedUrl; + } + } + } + } + const startedAt = performance.now(); const requestHeaders = new Headers(baseInit.headers); @@ -403,6 +434,32 @@ export class MCPService { }; } + if (config.transport === MCPTransportType.SSE) { + const url = useProxy ? buildProxiedUrl(config.url) : new URL(config.url); + const { fetch: diagnosticFetch, disable: stopPhaseLogging } = this.createDiagnosticFetch( + serverName, + config, + requestInit, + url, + useProxy, + onLog + ); + + if (import.meta.env.DEV && import.meta.env.VITE_DEBUG) { + console.log(`[MCPService] Creating SSE transport for ${url.href}`); + } + + return { + transport: new SSEClientTransport(url, { + requestInit, + fetch: diagnosticFetch, + eventSourceInit: { fetch: diagnosticFetch } + }), + type: MCPTransportType.SSE, + stopPhaseLogging + }; + } + const url = useProxy ? buildProxiedUrl(config.url) : new URL(config.url); const { fetch: diagnosticFetch, disable: stopPhaseLogging } = this.createDiagnosticFetch( serverName, diff --git a/tools/ui/src/lib/utils/mcp.ts b/tools/ui/src/lib/utils/mcp.ts index 05fe90048..7ce7f19fe 100644 --- a/tools/ui/src/lib/utils/mcp.ts +++ b/tools/ui/src/lib/utils/mcp.ts @@ -19,7 +19,10 @@ import { DISPLAY_NAME_SEPARATOR_REGEX, PATH_SEPARATOR, RESOURCE_TEXT_CONTENT_SEPARATOR, - DEFAULT_RESOURCE_FILENAME + DEFAULT_RESOURCE_FILENAME, + MCP_SSE_ENDPOINT, + MCP_SSE_ENDPOINT_SLASH, + MCP_SSE_ENDPOINT_QUERY } from '$lib/constants'; import { Database, @@ -41,10 +44,22 @@ import type { MimeTypeUnion } from '$lib/types/common'; export function detectMcpTransportFromUrl(url: string): MCPTransportType { const normalized = url.trim().toLowerCase(); - return normalized.startsWith(UrlProtocol.WEBSOCKET) || + if ( + normalized.startsWith(UrlProtocol.WEBSOCKET) || normalized.startsWith(UrlProtocol.WEBSOCKET_SECURE) - ? MCPTransportType.WEBSOCKET - : MCPTransportType.STREAMABLE_HTTP; + ) { + return MCPTransportType.WEBSOCKET; + } + + if ( + normalized.endsWith(MCP_SSE_ENDPOINT) || + normalized.endsWith(MCP_SSE_ENDPOINT_SLASH) || + normalized.includes(MCP_SSE_ENDPOINT_QUERY) + ) { + return MCPTransportType.SSE; + } + + return MCPTransportType.STREAMABLE_HTTP; } /** diff --git a/vendor/cpp-httplib/CMakeLists.txt b/vendor/cpp-httplib/CMakeLists.txt index 5fb3cf8d5..408e06151 100644 --- a/vendor/cpp-httplib/CMakeLists.txt +++ b/vendor/cpp-httplib/CMakeLists.txt @@ -41,7 +41,7 @@ if (LLAMA_BUILD_BORINGSSL) set(FIPS OFF CACHE BOOL "Enable FIPS (BoringSSL)") set(BORINGSSL_GIT "https://boringssl.googlesource.com/boringssl" CACHE STRING "BoringSSL git repository") - set(BORINGSSL_VERSION "0.20260526.0" CACHE STRING "BoringSSL version") + set(BORINGSSL_VERSION "0.20260616.0" CACHE STRING "BoringSSL version") message(STATUS "Fetching BoringSSL version ${BORINGSSL_VERSION}")