From 4937ca83f4f3da63004943fe05d8aa4f0217d238 Mon Sep 17 00:00:00 2001 From: Yash Raj Pandey <55940078+devYRPauli@users.noreply.github.com> Date: Sat, 18 Jul 2026 07:43:18 -0400 Subject: [PATCH 1/9] llama-quant : exclude i32 ffn_gate_tid2eid routing table from quantization (#25787) DeepSeek-V4's ffn_gate_tid2eid tensor is an i32 token-id -> expert-id index table, not weights. It was never added to the name-based exclusion list alongside ffn_gate_inp.weight, so llama-quantize tries to quantize it and fails since i32 cannot convert to a float type. Fixes ggml-org/llama.cpp#25754 Signed-off-by: Yash Raj Pandey --- src/llama-quant.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/llama-quant.cpp b/src/llama-quant.cpp index b66759b27..b187f5de9 100644 --- a/src/llama-quant.cpp +++ b/src/llama-quant.cpp @@ -306,6 +306,9 @@ static bool tensor_allows_quantization(const llama_model_quantize_params * param // NOTE: can't use LLM_TN here because the layer number is not known quantize &= name.find("ffn_gate_inp.weight") == std::string::npos; + // do not quantize the i32 token-id -> expert-id routing table (DeepSeek-V4) + quantize &= name.find("ffn_gate_tid2eid.weight") == std::string::npos; + // these are very small (e.g. 4x4) quantize &= name.find("altup") == std::string::npos; quantize &= name.find("laurel") == std::string::npos; From 571d0d540df04f25298d0e159e520d9fc62ed121 Mon Sep 17 00:00:00 2001 From: Ruixiang Wang Date: Sat, 18 Jul 2026 15:02:18 +0200 Subject: [PATCH 2/9] model: rotate injected K/V cache for DFlash (#25823) * dflash: rotate injected K/V cache when using K/V quantization * Update src/models/dflash.cpp Co-authored-by: Georgi Gerganov * clearer format * remove trailing whitespace --------- Co-authored-by: Georgi Gerganov --- src/models/dflash.cpp | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/models/dflash.cpp b/src/models/dflash.cpp index a7b4f4435..427eed459 100644 --- a/src/models/dflash.cpp +++ b/src/models/dflash.cpp @@ -1,5 +1,6 @@ #include "models.h" +#include "llama-impl.h" #include "llama-kv-cache.h" #include "llama-kv-cache-iswa.h" @@ -164,9 +165,25 @@ llama_model_dflash::graph::graph(const llama_model & model, const llm_gra const auto * kv = is_swa ? inp_attn_iswa->mctx->get_swa() : inp_attn_iswa->mctx->get_base(); ggml_tensor * k_idxs = is_swa ? inp_attn_iswa->get_k_idxs_swa() : inp_attn_iswa->get_k_idxs(); ggml_tensor * v_idxs = is_swa ? inp_attn_iswa->get_v_idxs_swa() : inp_attn_iswa->get_v_idxs(); + // rotate K/V into the cache's rotated space + ggml_tensor * k_rot = is_swa ? inp_attn_iswa->self_k_rot_swa : inp_attn_iswa->self_k_rot; + ggml_tensor * v_rot = is_swa ? inp_attn_iswa->self_v_rot_swa : inp_attn_iswa->self_v_rot; + if (k_rot) { + Kcur = llama_mul_mat_hadamard(ctx0, Kcur, k_rot); + } + if (v_rot) { + Vcur = llama_mul_mat_hadamard(ctx0, Vcur, v_rot); + } ggml_build_forward_expand(gf, kv->cpy_k(ctx0, Kcur, k_idxs, il)); ggml_build_forward_expand(gf, kv->cpy_v(ctx0, Vcur, v_idxs, il)); } else { + // rotate K/V into the cache's rotated space + if (inp_attn->self_k_rot) { + Kcur = llama_mul_mat_hadamard(ctx0, Kcur, inp_attn->self_k_rot); + } + if (inp_attn->self_v_rot) { + Vcur = llama_mul_mat_hadamard(ctx0, Vcur, inp_attn->self_v_rot); + } ggml_build_forward_expand(gf, inp_attn->mctx->cpy_k(ctx0, Kcur, inp_attn->get_k_idxs(), il)); ggml_build_forward_expand(gf, inp_attn->mctx->cpy_v(ctx0, Vcur, inp_attn->get_v_idxs(), il)); } From 178a6c44937154dc4c4eff0d166f4a044c4fceba Mon Sep 17 00:00:00 2001 From: Hongqiang Wang Date: Sun, 19 Jul 2026 22:48:57 -0700 Subject: [PATCH 3/9] opencl: Support broadcast for Adreno MUL_MAT and honor `view_offs` for Adreno Q8_0 MUL_MAT for llama-server multi-stream (#25910) * opencl: handle broadcast for adreno gemm/gemv_noshuffle * opencl: honor view_offs for adreno noshuffle gemm/gemv * opencl: general GEMM/GEMV support broadcast * opencl: remove unnecessary tests * opencl: remove unnecessary comments --------- Co-authored-by: Li He --- ggml/src/ggml-opencl/ggml-opencl.cpp | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/ggml/src/ggml-opencl/ggml-opencl.cpp b/ggml/src/ggml-opencl/ggml-opencl.cpp index 3563f40c4..e30eb43a3 100644 --- a/ggml/src/ggml-opencl/ggml-opencl.cpp +++ b/ggml/src/ggml-opencl/ggml-opencl.cpp @@ -17085,9 +17085,6 @@ static void ggml_cl_mul_mat_q8_0_f32_adreno(ggml_backend_t backend, const ggml_t cl_ulong offset1 = extra1->offset + src1->view_offs; cl_ulong offsetd = extrad->offset + dst->view_offs; - GGML_ASSERT(src1->view_offs == 0); - GGML_ASSERT(dst->view_offs == 0); - const int ne00 = src0->ne[0]; const int ne01 = src0->ne[1]; const int ne02 = src0->ne[2]; @@ -17148,9 +17145,9 @@ static void ggml_cl_mul_mat_q8_0_f32_adreno(ggml_backend_t backend, const ggml_t CL_CHECK(clSetKernelArg(kernel, 0, sizeof(cl_mem), &q_img)); CL_CHECK(clSetKernelArg(kernel, 1, sizeof(cl_mem), &extra0_q8_0->d)); CL_CHECK(clSetKernelArg(kernel, 2, sizeof(cl_mem), &b_img)); - CL_CHECK(clSetKernelArg(kernel, 3, sizeof(cl_ulong), &extra1->offset)); + CL_CHECK(clSetKernelArg(kernel, 3, sizeof(cl_ulong), &offset1)); CL_CHECK(clSetKernelArg(kernel, 4, sizeof(cl_mem), &extrad->data_device)); - CL_CHECK(clSetKernelArg(kernel, 5, sizeof(cl_ulong), &extrad->offset)); + CL_CHECK(clSetKernelArg(kernel, 5, sizeof(cl_ulong), &offsetd)); CL_CHECK(clSetKernelArg(kernel, 6, sizeof(int), &ne00)); CL_CHECK(clSetKernelArg(kernel, 7, sizeof(int), &ne01)); CL_CHECK(clSetKernelArg(kernel, 8, sizeof(int), &ne02)); @@ -18545,6 +18542,26 @@ static void ggml_cl_mul_mat(ggml_backend_t backend, const ggml_tensor * src0, co GGML_ASSERT(ne00 == ne10); +#ifdef GGML_OPENCL_USE_ADRENO_KERNELS + // adreno GEMM/GEMV kernels do not support broadcast, assuming ne2 and ne3 are 1 for src1 + // so we handle broadcast here + if ((ne12 > 1 || ne13 > 1) && ne02 == 1 && ne03 == 1 && + src0t != GGML_TYPE_F16 && src0t != GGML_TYPE_F32) { + for (int i13 = 0; i13 < ne13; ++i13) { + for (int i12 = 0; i12 < ne12; ++i12) { + ggml_tensor s1 = *src1; + s1.ne[2] = 1; s1.ne[3] = 1; + s1.view_offs = src1->view_offs + (size_t)i12*nb12 + (size_t)i13*nb13; + ggml_tensor d = *dst; + d.ne[2] = 1; d.ne[3] = 1; + d.view_offs = dst->view_offs + (size_t)i12*nb2 + (size_t)i13*nb3; + ggml_cl_mul_mat(backend, src0, &s1, &d); + } + } + return; + } +#endif + int nth0 = 32; int nth1 = 1; int nrows = 1; From 1521a9ac31601e914513314331635272ad07541b Mon Sep 17 00:00:00 2001 From: Pascal Date: Mon, 20 Jul 2026 16:22:16 +0200 Subject: [PATCH 4/9] ui: enable the agentic flow when only the JS sandbox is active (#25865) The agentic gate counted MCP servers, builtin and custom tools but not frontend tools, so with the JS sandbox as the only tool source the agentic flow was skipped, no tools field reached the server and the chat template rendered without the tool system prompt. The sandbox is fully client-side: frontendTools derives from the Developer settings toggle alone, counting it in the gate restores that single source of truth. --- tools/ui/src/lib/stores/agentic.svelte.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/ui/src/lib/stores/agentic.svelte.ts b/tools/ui/src/lib/stores/agentic.svelte.ts index 4ced749ea..3eef745ef 100644 --- a/tools/ui/src/lib/stores/agentic.svelte.ts +++ b/tools/ui/src/lib/stores/agentic.svelte.ts @@ -288,6 +288,7 @@ class AgenticStore { const hasTools = mcpStore.hasEnabledServers(perChatOverrides) || toolsStore.builtinTools.length > 0 || + toolsStore.frontendTools.length > 0 || toolsStore.customTools.length > 0; return { enabled: hasTools && DEFAULT_AGENTIC_CONFIG.enabled, From 43b5e63589468cb29a65e460e218fc831f22f4ef Mon Sep 17 00:00:00 2001 From: Pascal Date: Mon, 20 Jul 2026 16:28:24 +0200 Subject: [PATCH 5/9] UI: fix Settings/Display tool call content toggle (#25783) * ui: fix Show tool call in progress toggle ignored The showToolCallInProgress setting was disconnected from the render path during the agentic content rework: getDefaultExpanded() returns a hardcoded false for tool call sections and an unconditional effect auto-expands the currently executing tool call regardless of the setting. Drive default expansion of all tool call section types from the setting and remove the now redundant auto-expand effect. Manual toggling still takes precedence over the default in both directions. * ui: rename Show tool call in progress to Always show tool call content The previous name suggested symmetry with Show thought in progress, which only applies while inference is running, but tool call content stays expanded after completion. Rename the label, the settings key and the syncable server key to alwaysShowToolCallContent. The synced parameter never worked under its previous name so no migration is needed. --- .../ChatMessageAgenticContent.svelte | 17 +++-------------- tools/ui/src/lib/constants/settings-keys.ts | 2 +- tools/ui/src/lib/constants/settings-registry.ts | 6 +++--- 3 files changed, 7 insertions(+), 18 deletions(-) diff --git a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte index 34c82cd76..751d13756 100644 --- a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte @@ -43,6 +43,7 @@ const renderThinkingAsMarkdown = $derived(config().renderThinkingAsMarkdown as boolean); const showThoughtInProgress = $derived(Boolean(config().showThoughtInProgress)); + const alwaysShowToolCallContent = $derived(Boolean(config().alwaysShowToolCallContent)); const showMessageStats = $derived(Boolean(config().showMessageStats)); const showAgenticTurnStats = $derived(showMessageStats && Boolean(config().showAgenticTurnStats)); @@ -98,19 +99,6 @@ isStreaming ? agenticExecutingToolCallId(message.convId) : null ); - // Skip sections the user manually collapsed - we never override an explicit false. - let lastSeenExecutingToolCallId: string | null = null; - $effect(() => { - const current = currentlyExecutingToolCallId; - const previous = lastSeenExecutingToolCallId; - lastSeenExecutingToolCallId = current; - if (!current || current === previous) return; - const idx = sections.findIndex((s) => s.toolCallId === current); - if (idx >= 0 && expandedStates[idx] === undefined) { - expandedStates[idx] = true; - } - }); - type TurnGroup = { sections: AgenticSection[]; flatIndices: number[]; @@ -149,10 +137,11 @@ function getDefaultExpanded(section: AgenticSection): boolean { if ( + section.type === AgenticSectionType.TOOL_CALL || section.type === AgenticSectionType.TOOL_CALL_PENDING || section.type === AgenticSectionType.TOOL_CALL_STREAMING ) { - return false; + return alwaysShowToolCallContent; } if (section.type === AgenticSectionType.REASONING_PENDING) { diff --git a/tools/ui/src/lib/constants/settings-keys.ts b/tools/ui/src/lib/constants/settings-keys.ts index 0761ef8f0..bbcdde435 100644 --- a/tools/ui/src/lib/constants/settings-keys.ts +++ b/tools/ui/src/lib/constants/settings-keys.ts @@ -60,7 +60,7 @@ export const SETTINGS_KEYS = { MCP_SERVERS: 'mcpServers', MCP_REQUEST_TIMEOUT_SECONDS: 'mcpRequestTimeoutSeconds', AGENTIC_MAX_TURNS: 'agenticMaxTurns', - SHOW_TOOL_CALL_IN_PROGRESS: 'showToolCallInProgress', + ALWAYS_SHOW_TOOL_CALL_CONTENT: 'alwaysShowToolCallContent', // Performance PRE_ENCODE_CONVERSATION: 'preEncodeConversation', // Developer diff --git a/tools/ui/src/lib/constants/settings-registry.ts b/tools/ui/src/lib/constants/settings-registry.ts index b1404093c..03d4ce4e9 100644 --- a/tools/ui/src/lib/constants/settings-registry.ts +++ b/tools/ui/src/lib/constants/settings-registry.ts @@ -253,14 +253,14 @@ const SETTINGS_REGISTRY: Record = { } }, { - key: SETTINGS_KEYS.SHOW_TOOL_CALL_IN_PROGRESS, - label: 'Show tool call in progress', + key: SETTINGS_KEYS.ALWAYS_SHOW_TOOL_CALL_CONTENT, + label: 'Always show tool call content', help: 'Automatically expand tool call details while executing and keep them expanded after completion.', defaultValue: false, type: SettingsFieldType.CHECKBOX, section: SETTINGS_SECTION_SLUGS.DISPLAY, sync: { - serverKey: SETTINGS_KEYS.SHOW_TOOL_CALL_IN_PROGRESS, + serverKey: SETTINGS_KEYS.ALWAYS_SHOW_TOOL_CALL_CONTENT, paramType: SyncableParameterType.BOOLEAN } }, From 4ee6a9af71faceee99fed5eb98538b71772d86c6 Mon Sep 17 00:00:00 2001 From: Pascal Date: Mon, 20 Jul 2026 16:28:43 +0200 Subject: [PATCH 6/9] ui: fix collapsed user bubble with markdown rendering (#25869) Edge paragraph margins are now zeroed at the source in markdown-content.css, but the user bubble and the system message still carried the -my-4 compensation for them. The uncompensated negative margins shrank the wrapper 2rem below its content, collapsing single-line user bubbles into a scrollable sliver and skewing the system message expand threshold. --- .../ChatMessage/ChatMessageSystem/ChatMessageSystem.svelte | 5 +---- .../ChatMessage/ChatMessageUser/ChatMessageUserBubble.svelte | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageSystem/ChatMessageSystem.svelte b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageSystem/ChatMessageSystem.svelte index 9d3d07a27..36798e228 100644 --- a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageSystem/ChatMessageSystem.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageSystem/ChatMessageSystem.svelte @@ -157,10 +157,7 @@ > {#if currentConfig.renderUserContentAsMarkdown}
- +
{:else} {#if renderMarkdown && currentConfig.renderUserContentAsMarkdown}
- +
{:else} From 91d2fc387529940230555abd297a8b5e99737d3f Mon Sep 17 00:00:00 2001 From: Aman Gupta Date: Mon, 20 Jul 2026 22:43:39 +0800 Subject: [PATCH 7/9] llama_dsv4: write only used rows in state (#25325) * llama_dsv4: write only used rows in state * add TODO about conflating token pos with kv rows --- src/llama-kv-cache-dsv4.cpp | 101 ++++++++++++++++++++++++++++-------- 1 file changed, 79 insertions(+), 22 deletions(-) diff --git a/src/llama-kv-cache-dsv4.cpp b/src/llama-kv-cache-dsv4.cpp index 7cb6cc18d..069da45f4 100644 --- a/src/llama-kv-cache-dsv4.cpp +++ b/src/llama-kv-cache-dsv4.cpp @@ -22,7 +22,7 @@ static constexpr uint32_t DSV4_STATE_MAGIC = 0x34565344; // DSV4 static constexpr uint32_t DSV4_STATE_VERSION = 1; static constexpr uint32_t DSV4_STATE_MODE_FULL = 0; static constexpr uint32_t DSV4_STATE_MODE_PARTIAL = 1; -static constexpr uint32_t DSV4_K_CACHE_STATE_VER = 1; +static constexpr uint32_t DSV4_K_CACHE_STATE_VER = 2; static constexpr uint32_t DSV4_COMP_STATE_VER = 1; static uint32_t dsv4_comp_size(uint32_t kv_size, uint32_t ratio) { @@ -38,6 +38,16 @@ static void dsv4_clear_tensor_stream(ggml_tensor * tensor, uint32_t stream) { ggml_backend_tensor_memset(tensor, 0, stream*stream_size, stream_size); } +static uint32_t dsv4_state_n_used_k_rows(llama_pos pos_max, uint32_t ratio, uint32_t kv_size) { + if (pos_max < 0) { + return 0; + } + + const uint64_t n_rows = ((uint64_t) pos_max + 1)/ratio; + + return (uint32_t) std::min(kv_size, n_rows); +} + static int64_t dsv4_stream_offset(uint32_t n_stream, llama_seq_id seq_id, uint32_t size) { if (n_stream <= 1) { return 0; @@ -239,6 +249,7 @@ static void dsv4_state_dst_stream_range( static void dsv4_state_write_tensor_streams( llama_io_write_i & io, ggml_tensor * tensor, + uint32_t tensor_rows, uint32_t n_rows, uint32_t s0, uint32_t ns) { @@ -247,20 +258,31 @@ static void dsv4_state_write_tensor_streams( const uint64_t rows = n_rows; const uint64_t row_size = ggml_row_size(tensor->type, tensor->ne[0]); + if (n_rows > tensor_rows) { + throw std::runtime_error("DSV4 state tensor row count exceeds storage"); + } + io.write(&type_i, sizeof(type_i)); io.write(&ne0, sizeof(ne0)); io.write(&rows, sizeof(rows)); io.write(&row_size, sizeof(row_size)); - const size_t offset = (size_t) s0*n_rows*row_size; - const size_t size = (size_t) ns*n_rows*row_size; + const size_t stream_stride = (size_t) tensor_rows*row_size; + const size_t size = (size_t) n_rows*row_size; + if (size == 0) { + return; + } - io.write_tensor(tensor, offset, size); + for (uint32_t s = 0; s < ns; ++s) { + const size_t offset = (size_t) (s0 + s)*stream_stride; + io.write_tensor(tensor, offset, size); + } } static void dsv4_state_read_tensor_streams( llama_io_read_i & io, ggml_tensor * tensor, + uint32_t tensor_rows, uint32_t n_rows, uint32_t s0, uint32_t ns) { @@ -282,18 +304,28 @@ static void dsv4_state_read_tensor_streams( if (type_i != type_i_ref || ne0 != ne0_ref || rows != rows_ref || row_size != row_size_ref) { throw std::runtime_error("DSV4 state tensor metadata mismatch"); } + if (n_rows > tensor_rows) { + throw std::runtime_error("DSV4 state tensor row count exceeds storage"); + } - const size_t offset = (size_t) s0*n_rows*row_size; - const size_t size = (size_t) ns*n_rows*row_size; + const size_t stream_stride = (size_t) tensor_rows*row_size; + const size_t size = (size_t) n_rows*row_size; + if (size == 0) { + return; + } - io.read_tensor(tensor, offset, size); + for (uint32_t s = 0; s < ns; ++s) { + const size_t offset = (size_t) (s0 + s)*stream_stride; + io.read_tensor(tensor, offset, size); + } } static void dsv4_state_write_k_cache( llama_io_write_i & io, const llama_kv_cache * kv, llama_seq_id seq_id, - llama_state_seq_flags flags) { + llama_state_seq_flags flags, + uint32_t n_rows) { GGML_UNUSED(flags); uint32_t s0; @@ -305,14 +337,18 @@ static void dsv4_state_write_k_cache( const auto layer_ids = kv->get_layer_ids(); const uint32_t n_layer = layer_ids.size(); + if (n_rows > kv_size) { + throw std::runtime_error("DSV4 K-cache state row count exceeds cache size"); + } + io.write(&version, sizeof(version)); - io.write(&kv_size, sizeof(kv_size)); + io.write(&n_rows, sizeof(n_rows)); io.write(&ns, sizeof(ns)); io.write(&n_layer, sizeof(n_layer)); for (uint32_t il : layer_ids) { io.write(&il, sizeof(il)); - dsv4_state_write_tensor_streams(io, kv->get_k_storage(il), kv_size, s0, ns); + dsv4_state_write_tensor_streams(io, kv->get_k_storage(il), kv_size, n_rows, s0, ns); } } @@ -324,19 +360,26 @@ static void dsv4_state_read_k_cache( GGML_UNUSED(flags); uint32_t version; - uint32_t kv_size_ref; + uint32_t n_rows_ref; uint32_t ns; uint32_t n_layer_ref; io.read(&version, sizeof(version)); - io.read(&kv_size_ref, sizeof(kv_size_ref)); + io.read(&n_rows_ref, sizeof(n_rows_ref)); io.read(&ns, sizeof(ns)); io.read(&n_layer_ref, sizeof(n_layer_ref)); - if (version != DSV4_K_CACHE_STATE_VER) { + if (version != 1 && version != DSV4_K_CACHE_STATE_VER) { throw std::runtime_error("DSV4 K-cache state version mismatch"); } - if (kv_size_ref != kv->get_size()) { + + const uint32_t kv_size = kv->get_size(); + if (version == 1 && n_rows_ref != kv_size) { + LLAMA_LOG_INFO("kv size ref %d kv %d\n", n_rows_ref, kv_size); + throw std::runtime_error("DSV4 K-cache state size mismatch"); + } + if (n_rows_ref > kv_size) { + LLAMA_LOG_INFO("kv rows ref %d kv %d\n", n_rows_ref, kv_size); throw std::runtime_error("DSV4 K-cache state size mismatch"); } @@ -355,7 +398,7 @@ static void dsv4_state_read_k_cache( throw std::runtime_error("DSV4 K-cache layer id mismatch"); } - dsv4_state_read_tensor_streams(io, kv->get_k_storage(il), kv->get_size(), s0, ns); + dsv4_state_read_tensor_streams(io, kv->get_k_storage(il), kv_size, n_rows_ref, s0, ns); } } @@ -882,8 +925,8 @@ void llama_dsv4_comp_state::state_write(llama_io_write_i & io, llama_seq_id seq_ for (const auto & layer : layers) { io.write(&layer.il, sizeof(layer.il)); - dsv4_state_write_tensor_streams(io, layer.kv, state_size, s0, ns); - dsv4_state_write_tensor_streams(io, layer.score, state_size, s0, ns); + dsv4_state_write_tensor_streams(io, layer.kv, state_size, state_size, s0, ns); + dsv4_state_write_tensor_streams(io, layer.score, state_size, state_size, s0, ns); } } @@ -924,8 +967,8 @@ void llama_dsv4_comp_state::state_read(llama_io_read_i & io, llama_seq_id seq_id throw std::runtime_error("DSV4 compressor state layer id mismatch"); } - dsv4_state_read_tensor_streams(io, layer.kv, state_size, s0, ns); - dsv4_state_read_tensor_streams(io, layer.score, state_size, s0, ns); + dsv4_state_read_tensor_streams(io, layer.kv, state_size, state_size, s0, ns); + dsv4_state_read_tensor_streams(io, layer.score, state_size, state_size, s0, ns); } } @@ -1328,9 +1371,19 @@ void llama_kv_cache_dsv4::state_write(llama_io_write_i & io, llama_seq_id seq_id kv_raw->state_write(io, seq_id, flags); if (!partial_only) { - dsv4_state_write_k_cache(io, kv_csa.get(), seq_id, flags); - dsv4_state_write_k_cache(io, kv_hca.get(), seq_id, flags); - dsv4_state_write_k_cache(io, kv_lid.get(), seq_id, flags); + const llama_pos pos_max = seq_id >= 0 ? kv_raw->seq_pos_max(seq_id) : -1; + + //FIXME : note that we conflate token positions with rows, which is not true for multi-modal case. + const uint32_t n_rows_csa = seq_id >= 0 ? + dsv4_state_n_used_k_rows(pos_max, DSV4_CSA_RATIO, kv_csa->get_size()) : kv_csa->get_size(); + const uint32_t n_rows_hca = seq_id >= 0 ? + dsv4_state_n_used_k_rows(pos_max, DSV4_HCA_RATIO, kv_hca->get_size()) : kv_hca->get_size(); + const uint32_t n_rows_lid = seq_id >= 0 ? + dsv4_state_n_used_k_rows(pos_max, DSV4_CSA_RATIO, kv_lid->get_size()) : kv_lid->get_size(); + + dsv4_state_write_k_cache(io, kv_csa.get(), seq_id, flags, n_rows_csa); + dsv4_state_write_k_cache(io, kv_hca.get(), seq_id, flags, n_rows_hca); + dsv4_state_write_k_cache(io, kv_lid.get(), seq_id, flags, n_rows_lid); } csa_state->state_write(io, seq_id, flags); @@ -1366,6 +1419,10 @@ void llama_kv_cache_dsv4::state_read(llama_io_read_i & io, llama_seq_id seq_id, kv_raw->state_read(io, seq_id, flags); if (!partial_only) { + kv_csa->clear(true); + kv_hca->clear(true); + kv_lid->clear(true); + dsv4_state_read_k_cache(io, kv_csa.get(), seq_id, flags); dsv4_state_read_k_cache(io, kv_hca.get(), seq_id, flags); dsv4_state_read_k_cache(io, kv_lid.get(), seq_id, flags); From 2beefef68825aed8de05f0d89981bf5d05266a3c Mon Sep 17 00:00:00 2001 From: Aleksander Grygier Date: Mon, 20 Jul 2026 23:40:08 +0200 Subject: [PATCH 8/9] ui: Sidebar Conversations Bulk Action + Improved Settings logic/UI (#25815) * feat: WIP * feat: Replace conversation rename flow with unified AlertDialog component * feat: Add radio group component and consolidate title generation settings * refactor: Remove JS Sandbox global toggle and migrate legacy user state * chore: Formatting * refactor: Cleanup Co-authored-by: Aleksander Grygier * refactor: Cleanup * refactor: Marquee selection hook * feat: UI improvements * refactor: Bulk db operations * fix: optimize bulk conversation deletion to handle ancestor chains * refactor: remove pairedKey mechanism from settings system * fix: remove redundant onclick handler from dialog cancel button * chore: pin @lucide/svelte to exact version * feat: Run JavaScript tool disabled by default * fix: correct active conversation deletion tracking in bulk delete * feat: improve shift-key multi-selection support in sidebar via keyboard * refactor: Retrieve JS Tool enabling via Developer Settings * nits: sync, dialog wording, cycle guard, and lockfile follow-ups - Restore titleGenerationUseLLM registry entry so it syncs across devices again - Mention fork cascade in the bulk delete confirmation dialog - Clear newParent on cycle guard break so children never point at a deleted conversation - Align @lucide/svelte in package-lock.json with the exact pin in package.json --------- Co-authored-by: Pascal --- tools/ui/package-lock.json | 8 +- tools/ui/package.json | 2 +- .../components/app/actions/ActionIcon.svelte | 9 +- .../dialogs/DialogConversationRename.svelte | 85 ++++++ .../DialogConversationSelection.svelte | 5 +- .../DialogConversationTitleUpdate.svelte | 46 ---- .../app/dialogs/DialogExportSettings.svelte | 1 + .../src/lib/components/app/dialogs/index.ts | 31 ++- .../app/misc/ConversationSelection.svelte | 84 +++--- .../SidebarNavigation.svelte | 251 ++++++++++++++---- .../SidebarNavigationActions.svelte | 14 +- .../SidebarNavigationConversationItem.svelte | 90 ++++++- .../SidebarNavigationConversationList.svelte | 204 +++++++++----- .../SidebarNavigationSearchResults.svelte | 20 +- .../SidebarNavigationSelectionBar.svelte | 163 ++++++++++++ .../lib/components/app/navigation/index.ts | 30 +++ .../SettingsChat/SettingsChatFields.svelte | 52 +++- .../SettingsChat/SettingsChatToolsTab.svelte | 15 +- .../components/ui/checkbox/checkbox.svelte | 2 +- .../lib/components/ui/radio-group/index.ts | 10 + .../ui/radio-group/radio-group-item.svelte | 31 +++ .../ui/radio-group/radio-group.svelte | 19 ++ tools/ui/src/lib/constants/settings-keys.ts | 3 +- .../ui/src/lib/constants/settings-registry.ts | 135 +++++----- tools/ui/src/lib/enums/settings.enums.ts | 3 +- .../lib/hooks/use-marquee-selection.svelte.ts | 239 +++++++++++++++++ tools/ui/src/lib/services/database.service.ts | 178 ++++++++++++- tools/ui/src/lib/stores/chat.svelte.ts | 6 +- .../ui/src/lib/stores/conversations.svelte.ts | 167 ++++++++---- tools/ui/src/lib/types/settings.d.ts | 4 + tools/ui/src/routes/+layout.svelte | 48 +--- 31 files changed, 1529 insertions(+), 426 deletions(-) create mode 100644 tools/ui/src/lib/components/app/dialogs/DialogConversationRename.svelte delete mode 100644 tools/ui/src/lib/components/app/dialogs/DialogConversationTitleUpdate.svelte create mode 100644 tools/ui/src/lib/components/app/navigation/SidebarNavigation/SidebarNavigationSelectionBar.svelte create mode 100644 tools/ui/src/lib/components/ui/radio-group/index.ts create mode 100644 tools/ui/src/lib/components/ui/radio-group/radio-group-item.svelte create mode 100644 tools/ui/src/lib/components/ui/radio-group/radio-group.svelte create mode 100644 tools/ui/src/lib/hooks/use-marquee-selection.svelte.ts diff --git a/tools/ui/package-lock.json b/tools/ui/package-lock.json index 7216de682..2fe44f4c3 100644 --- a/tools/ui/package-lock.json +++ b/tools/ui/package-lock.json @@ -12,7 +12,7 @@ "@eslint/compat": "1.4.1", "@eslint/js": "9.39.2", "@internationalized/date": "3.12.2", - "@lucide/svelte": "0.515.0", + "@lucide/svelte": "1.25.0", "@modelcontextprotocol/sdk": "1.26.0", "@playwright/test": "1.56.1", "@storybook/addon-a11y": "10.2.4", @@ -3065,9 +3065,9 @@ } }, "node_modules/@lucide/svelte": { - "version": "0.515.0", - "resolved": "https://registry.npmjs.org/@lucide/svelte/-/svelte-0.515.0.tgz", - "integrity": "sha512-CEAyqcZmNBfYzVgaRmK2RFJP5tnbXxekRyDk0XX/eZQRfsJmkDvmQwXNX8C869BgNeryzmrRyjHhUL6g9ZOHNA==", + "version": "1.25.0", + "resolved": "https://registry.npmjs.org/@lucide/svelte/-/svelte-1.25.0.tgz", + "integrity": "sha512-v9m+dD68jxVnqkU3K59mG/RSRFlPGzmKCGSyMfnXcaGv9jODDQMyQkcp1CGvk3Y/cUj9v7f8rw1n//K0B53xGQ==", "dev": true, "license": "ISC", "peerDependencies": { diff --git a/tools/ui/package.json b/tools/ui/package.json index 8b3516a02..4ea2bf703 100644 --- a/tools/ui/package.json +++ b/tools/ui/package.json @@ -31,7 +31,7 @@ "@eslint/compat": "1.4.1", "@eslint/js": "9.39.2", "@internationalized/date": "3.12.2", - "@lucide/svelte": "0.515.0", + "@lucide/svelte": "1.25.0", "@modelcontextprotocol/sdk": "1.26.0", "@playwright/test": "1.56.1", "@storybook/addon-a11y": "10.2.4", diff --git a/tools/ui/src/lib/components/app/actions/ActionIcon.svelte b/tools/ui/src/lib/components/app/actions/ActionIcon.svelte index 8a86557bb..608ff6fab 100644 --- a/tools/ui/src/lib/components/app/actions/ActionIcon.svelte +++ b/tools/ui/src/lib/components/app/actions/ActionIcon.svelte @@ -66,7 +66,14 @@ {#snippet child({ props })} - {@render button(props)} + {#if disabled} + + + {@render button({})} + + {:else} + {@render button(props)} + {/if} {/snippet} diff --git a/tools/ui/src/lib/components/app/dialogs/DialogConversationRename.svelte b/tools/ui/src/lib/components/app/dialogs/DialogConversationRename.svelte new file mode 100644 index 000000000..d85340f3f --- /dev/null +++ b/tools/ui/src/lib/components/app/dialogs/DialogConversationRename.svelte @@ -0,0 +1,85 @@ + + + + + + + + Rename conversation + + + Choose a new title for this conversation. + + +
+ + + +
+ + + Cancel + + + +
+
diff --git a/tools/ui/src/lib/components/app/dialogs/DialogConversationSelection.svelte b/tools/ui/src/lib/components/app/dialogs/DialogConversationSelection.svelte index 737325085..5f5b2f4ab 100644 --- a/tools/ui/src/lib/components/app/dialogs/DialogConversationSelection.svelte +++ b/tools/ui/src/lib/components/app/dialogs/DialogConversationSelection.svelte @@ -37,9 +37,9 @@ - + - + Select Conversations to {mode === 'export' ? 'Export' : 'Import'} @@ -58,6 +58,7 @@ - import * as AlertDialog from '$lib/components/ui/alert-dialog'; - import { Button } from '$lib/components/ui/button'; - - interface Props { - open: boolean; - currentTitle: string; - newTitle: string; - onConfirm: () => void; - onCancel: () => void; - } - - let { open = $bindable(), currentTitle, newTitle, onConfirm, onCancel }: Props = $props(); - - - - - - Update Conversation Title? - - - Do you want to update the conversation title to match the first message content? - - - -
-
-

Current title:

- -

{currentTitle}

-
- -
-

New title would be:

- -

{newTitle}

-
-
- - - - - - -
-
diff --git a/tools/ui/src/lib/components/app/dialogs/DialogExportSettings.svelte b/tools/ui/src/lib/components/app/dialogs/DialogExportSettings.svelte index c112bde9f..fe36dce56 100644 --- a/tools/ui/src/lib/components/app/dialogs/DialogExportSettings.svelte +++ b/tools/ui/src/lib/components/app/dialogs/DialogExportSettings.svelte @@ -68,6 +68,7 @@ Cancel + showTitleUpdate = false} + * bind:value={renameDraft} + * onConfirm={handleRenameConfirm} + * onCancel={() => (showRename = false)} * /> * ``` */ -export { default as DialogConversationTitleUpdate } from './DialogConversationTitleUpdate.svelte'; +export { default as DialogConversationRename } from './DialogConversationRename.svelte'; /** * diff --git a/tools/ui/src/lib/components/app/misc/ConversationSelection.svelte b/tools/ui/src/lib/components/app/misc/ConversationSelection.svelte index db14fd631..b6052b4a9 100644 --- a/tools/ui/src/lib/components/app/misc/ConversationSelection.svelte +++ b/tools/ui/src/lib/components/app/misc/ConversationSelection.svelte @@ -4,6 +4,7 @@ import SearchInput from '$lib/components/app/forms/SearchInput.svelte'; import { ScrollArea } from '$lib/components/ui/scroll-area'; import { SvelteSet } from 'svelte/reactivity'; + import { useMarqueeSelection } from '$lib/hooks/use-marquee-selection.svelte'; interface Props { conversations: DatabaseConversation[]; @@ -11,13 +12,20 @@ mode: 'export' | 'import'; onCancel: () => void; onConfirm: (selectedConversations: DatabaseConversation[]) => void; + isOpen?: boolean; } - let { conversations, messageCountMap = new Map(), mode, onCancel, onConfirm }: Props = $props(); + let { + conversations, + messageCountMap = new Map(), + mode, + onCancel, + onConfirm, + isOpen = true + }: Props = $props(); let searchQuery = $state(''); let selectedIds = $state.raw>(getInitialSelectedIds()); - let lastClickedId = $state(null); function getInitialSelectedIds(): SvelteSet { return new SvelteSet(conversations.map((c) => c.id)); @@ -30,6 +38,8 @@ }) ); + let orderedIds = $derived(filteredConversations.map((c) => c.id)); + let allSelected = $derived( filteredConversations.length > 0 && filteredConversations.every((conv) => selectedIds.has(conv.id)) @@ -39,54 +49,20 @@ filteredConversations.some((conv) => selectedIds.has(conv.id)) && !allSelected ); - function toggleConversation(id: string, shiftKey: boolean = false) { - const newSet = new SvelteSet(selectedIds); - - if (shiftKey && lastClickedId !== null) { - const lastIndex = filteredConversations.findIndex((c) => c.id === lastClickedId); - const currentIndex = filteredConversations.findIndex((c) => c.id === id); - - if (lastIndex !== -1 && currentIndex !== -1) { - const start = Math.min(lastIndex, currentIndex); - const end = Math.max(lastIndex, currentIndex); - - const shouldSelect = !newSet.has(id); - - for (let i = start; i <= end; i++) { - if (shouldSelect) { - newSet.add(filteredConversations[i].id); - } else { - newSet.delete(filteredConversations[i].id); - } - } - - selectedIds = newSet; - return; - } - } - - if (newSet.has(id)) { - newSet.delete(id); - } else { - newSet.add(id); - } - - selectedIds = newSet; - lastClickedId = id; - } + const marquee = useMarqueeSelection({ + selectedIds: () => selectedIds, + orderedIds: () => orderedIds, + enabled: () => isOpen + }); function toggleAll() { + const newSet = new SvelteSet(selectedIds); if (allSelected) { - const newSet = new SvelteSet(selectedIds); - filteredConversations.forEach((conv) => newSet.delete(conv.id)); - selectedIds = newSet; } else { - const newSet = new SvelteSet(selectedIds); - filteredConversations.forEach((conv) => newSet.add(conv.id)); - selectedIds = newSet; } + selectedIds = newSet; } function handleConfirm() { @@ -97,7 +73,7 @@ function handleCancel() { selectedIds = getInitialSelectedIds(); searchQuery = ''; - lastClickedId = null; + marquee.reset(); onCancel(); } @@ -105,7 +81,7 @@ export function reset() { selectedIds = getInitialSelectedIds(); searchQuery = ''; - lastClickedId = null; + marquee.reset(); } @@ -122,7 +98,7 @@
- + @@ -139,6 +115,7 @@ + {#if filteredConversations.length === 0} @@ -152,23 +129,28 @@ {:else} {#each filteredConversations as conv (conv.id)} + {@const checked = selectedIds.has(conv.id)} toggleConversation(conv.id, event.shiftKey)} + class="cursor-pointer border-b transition-colors hover:bg-muted/50 {checked + ? 'bg-muted/75' + : ''}" + data-conversation-row={conv.id} + onmousedown={(event) => marquee.rowMouseDown(conv.id, event)} + onclick={(event) => marquee.rowClick(conv.id, event.shiftKey)} > diff --git a/tools/ui/src/lib/components/app/navigation/SidebarNavigation/SidebarNavigation.svelte b/tools/ui/src/lib/components/app/navigation/SidebarNavigation/SidebarNavigation.svelte index a23f4682e..b5e4beeff 100644 --- a/tools/ui/src/lib/components/app/navigation/SidebarNavigation/SidebarNavigation.svelte +++ b/tools/ui/src/lib/components/app/navigation/SidebarNavigation/SidebarNavigation.svelte @@ -4,15 +4,22 @@ import { PanelLeftClose, PanelLeftOpen, X } from '@lucide/svelte'; import { ActionIcon, + DialogConversationRename, Logo, SidebarNavigationConversationList, SidebarNavigationActions } from '$lib/components/app'; import { ROUTES } from '$lib/constants'; import { fade } from 'svelte/transition'; + import { SvelteSet } from 'svelte/reactivity'; + import { useMarqueeSelection } from '$lib/hooks/use-marquee-selection.svelte'; import { useKeyboardShortcuts } from '$lib/hooks/use-keyboard-shortcuts.svelte'; - import { conversationsStore, conversations } from '$lib/stores/conversations.svelte'; + import { + buildConversationTree, + conversationsStore, + conversations + } from '$lib/stores/conversations.svelte'; import { chatStore } from '$lib/stores/chat.svelte'; import { config } from '$lib/stores/settings.svelte'; import { RouterService } from '$lib/services/router.service'; @@ -40,7 +47,6 @@ const isOnMobile = $derived(isMobile.current); const alwaysShowOnDesktop = $derived(config().alwaysShowSidebarOnDesktop as boolean); - // Keep the sidebar expanded on desktop when the user pins it open $effect(() => { if (alwaysShowOnDesktop && !isOnMobile) { isExpandedMode = true; @@ -58,13 +64,11 @@ if (!isExpandedMode) { isSearchModeActive = false; searchQuery = ''; + if (isSelectionMode) exitSelectionMode(); cancelMobileCollapse(); } }); - // On mobile the dedicated /search route hides the sidebar (see the aside - // render guard below). Collapse it as we enter /search so it doesn't - // reappear expanded when the user navigates back via the back button. $effect(() => { if (isMobile.current && page.url.hash.includes(ROUTES.SEARCH)) { isExpandedMode = false; @@ -89,6 +93,121 @@ return conversations(); }); + let isSelectionMode = $state(false); + let selectedIds = new SvelteSet(); + + let renameDialogOpen = $state(false); + let renameTargetConversationId = $state(null); + let renameDraft = $state(''); + let renameOriginalTitle = $state(''); + + const renderedOrderIds = $derived( + buildConversationTree(filteredConversations).map((t) => t.conversation.id) + ); + + const allSelectedArePinned = $derived.by(() => { + if (selectedIds.size === 0) return false; + const convs = conversations(); + for (const id of selectedIds) { + const c = convs.find((conv) => conv.id === id); + if (c && !c.pinned) return false; + } + return true; + }); + + const pinStateIsMixed = $derived.by(() => { + if (selectedIds.size === 0) return false; + const convs = conversations(); + let anyPinned = false; + let anyUnpinned = false; + for (const id of selectedIds) { + const c = convs.find((conv) => conv.id === id); + if (!c) continue; + if (c.pinned) anyPinned = true; + else anyUnpinned = true; + if (anyPinned && anyUnpinned) return true; + } + return false; + }); + + const visibleSelectionStats = $derived.by(() => { + const visibleIds = filteredConversations.map((c) => c.id); + let selectedVisible = 0; + for (const id of visibleIds) { + if (selectedIds.has(id)) selectedVisible++; + } + return { + visibleCount: visibleIds.length, + selectedVisibleCount: selectedVisible + }; + }); + + function enterSelectionMode(id?: string) { + isSelectionMode = true; + if (id !== undefined) { + selectedIds.add(id); + } + } + + function exitSelectionMode() { + isSelectionMode = false; + selectedIds.clear(); + } + + function toggleSelected(id: string) { + if (selectedIds.has(id)) { + selectedIds.delete(id); + } else { + selectedIds.add(id); + } + } + + function toggleSelectAllVisible() { + const visibleIds = filteredConversations.map((c) => c.id); + const allSelected = visibleIds.length > 0 && visibleIds.every((id) => selectedIds.has(id)); + + if (allSelected) { + for (const id of visibleIds) selectedIds.delete(id); + } else { + for (const id of visibleIds) selectedIds.add(id); + } + } + + async function handleBulkDelete() { + const ids = Array.from(selectedIds); + if (ids.length === 0) return; + await conversationsStore.bulkDeleteConversations(ids); + exitSelectionMode(); + } + + async function handleBulkPinToggle() { + const ids = Array.from(selectedIds); + if (ids.length === 0) return; + await conversationsStore.bulkToggleConversationPin(ids); + } + + async function handleBulkExport() { + const ids = Array.from(selectedIds); + if (ids.length === 0) return; + await conversationsStore.bulkExportConversations(ids); + } + + const marquee = useMarqueeSelection({ + selectedIds: () => selectedIds, + orderedIds: () => renderedOrderIds, + enabled: () => isSelectionMode + }); + + function handleRowMouseDown(id: string, event: MouseEvent) { + if (!isSelectionMode) return; + marquee.rowMouseDown(id, event); + } + + function handleSelectionClick(id: string, options: { shiftKey: boolean }): void { + if (!isSelectionMode) return; + marquee.rowClick(id, options.shiftKey); + } + async function selectConversation(id: string) { if (isMobile.current) { scheduleMobileCollapse(); @@ -100,10 +219,30 @@ const conversation = conversations().find((conv) => conv.id === id); if (!conversation) return; - const newName = window.prompt('Rename conversation', conversation.name); - if (newName && newName.trim()) { - await conversationsStore.updateConversationName(id, newName.trim()); - } + renameTargetConversationId = id; + renameOriginalTitle = conversation.name; + renameDraft = conversation.name; + renameDialogOpen = true; + } + + async function handleRenameConfirm() { + const id = renameTargetConversationId; + if (!id) return; + + const nextName = renameDraft.trim(); + if (!nextName || nextName === renameOriginalTitle.trim()) return; + + await conversationsStore.updateConversationName(id, nextName); + + renameDialogOpen = false; + renameTargetConversationId = null; + } + + function handleRenameCancel() { + renameDialogOpen = false; + renameTargetConversationId = null; + renameDraft = ''; + renameOriginalTitle = ''; } async function handleDeleteConversation(id: string) { @@ -148,9 +287,7 @@ {#if innerWidth > 768 || (!page.url.hash.includes(ROUTES.SETTINGS) && !page.url.hash.includes(ROUTES.MCP_SERVERS) && !page.url.hash.includes(ROUTES.SEARCH))} {/if} + +
Messages
{ event.preventDefault(); event.stopPropagation(); - toggleConversation(conv.id, event.shiftKey); + marquee.rowClick(conv.id, event.shiftKey); }} /> -
+
{conv.name || 'Untitled conversation'}