mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-08-27 02:27:34 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
925e117994 | ||
|
|
539f24529b | ||
|
|
0379a19f09 | ||
|
|
5e6a37cb11 |
@@ -4171,10 +4171,16 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) {
|
||||
const uint32_t subgroup_size_16 = std::max(device->subgroup_size, 16u);
|
||||
const uint32_t subgroup_size_32 = std::max(device->subgroup_size, 32u);
|
||||
|
||||
// clamp WARP for l_/m_ warptiles so WM <= BM (breaks on subgroupSize > 64)
|
||||
const uint32_t mm_warp_8 = std::min(subgroup_size_8, 64u);
|
||||
const uint32_t mm_warp_16 = std::min(subgroup_size_16, 64u);
|
||||
|
||||
const uint32_t mul_mat_subgroup_size = (device->vendor_id == VK_VENDOR_ID_INTEL && device->subgroup_size_control) ? device->subgroup_min_size : device->subgroup_size;
|
||||
const uint32_t mul_mat_subgroup_size_8 = std::max(mul_mat_subgroup_size, 8u);
|
||||
const uint32_t mul_mat_subgroup_size_16 = std::max(mul_mat_subgroup_size, 16u);
|
||||
const uint32_t mul_mat_subgroup_size_32 = std::max(mul_mat_subgroup_size, 32u);
|
||||
const uint32_t mul_mat_mm_warp_8 = std::min(mul_mat_subgroup_size_8, 64u);
|
||||
const uint32_t mul_mat_mm_warp_16 = std::min(mul_mat_subgroup_size_16, 64u);
|
||||
|
||||
const bool subgroup_min_size_16 = (!device->subgroup_size_control && device->subgroup_size >= 16) ||
|
||||
(device->subgroup_size_control && device->subgroup_max_size >= 16);
|
||||
@@ -4255,39 +4261,39 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) {
|
||||
|
||||
const uint32_t s_warptile_wm = device->subgroup_size == 8 ? 8 : 32;
|
||||
|
||||
l_warptile = { 128, 128, 128, 16, subgroup_size_8 * 2, 64, 2, tm_l, tn_l, tk_l, subgroup_size_8 };
|
||||
m_warptile = { 128, 64, 64, 16, subgroup_size_8, 32, 2, tm_m, tn_m, tk_m, subgroup_size_8 };
|
||||
s_warptile = { subgroup_size_32, 32, 32, 16, s_warptile_wm, 32, 2, tm_s, tn_s, tk_s, subgroup_size_8 };
|
||||
l_warptile = { 128, 128, 128, 16, mm_warp_8 * 2, 64, 2, tm_l, tn_l, tk_l, mm_warp_8 };
|
||||
m_warptile = { 128, 64, 64, 16, mm_warp_8, 32, 2, tm_m, tn_m, tk_m, mm_warp_8 };
|
||||
s_warptile = { subgroup_size_32, 32, 32, 16, s_warptile_wm, 32, 2, tm_s, tn_s, tk_s, subgroup_size_8 };
|
||||
|
||||
l_warptile_mmq = { 128, 128, 128, 32, subgroup_size_8 * 2, 64, 2, tm_l, tn_l, tk_l, subgroup_size_8 };
|
||||
m_warptile_mmq = { 128, 64, 64, 32, subgroup_size_8, 32, 2, tm_m, tn_m, tk_m, subgroup_size_8 };
|
||||
s_warptile_mmq = { subgroup_size_32, 32, 32, 32, s_warptile_wm, 32, 2, tm_s, tn_s, tk_s, subgroup_size_8 };
|
||||
l_warptile_mmq = { 128, 128, 128, 32, mm_warp_8 * 2, 64, 2, tm_l, tn_l, tk_l, mm_warp_8 };
|
||||
m_warptile_mmq = { 128, 64, 64, 32, mm_warp_8, 32, 2, tm_m, tn_m, tk_m, mm_warp_8 };
|
||||
s_warptile_mmq = { subgroup_size_32, 32, 32, 32, s_warptile_wm, 32, 2, tm_s, tn_s, tk_s, subgroup_size_8 };
|
||||
|
||||
// Integer MMQ has a smaller shared memory profile, but heavier register use
|
||||
l_warptile_mmq_int = { 128, 128, 128, 32, subgroup_size_8 * 2, 64, 2, 4, 4, 1, subgroup_size_8 };
|
||||
m_warptile_mmq_int = { 128, 64, 64, 32, subgroup_size_8, 32, 2, 2, 2, 1, subgroup_size_8 };
|
||||
s_warptile_mmq_int = { subgroup_size_32, 32, 32, 32, s_warptile_wm, 32, 2, 2, 1, 1, subgroup_size_8 };
|
||||
l_warptile_mmq_int = { 128, 128, 128, 32, mm_warp_8 * 2, 64, 2, 4, 4, 1, mm_warp_8 };
|
||||
m_warptile_mmq_int = { 128, 64, 64, 32, mm_warp_8, 32, 2, 2, 2, 1, mm_warp_8 };
|
||||
s_warptile_mmq_int = { subgroup_size_32, 32, 32, 32, s_warptile_wm, 32, 2, 2, 1, 1, subgroup_size_8 };
|
||||
|
||||
// K-quants use even more registers, mitigate by setting WMITER to 1
|
||||
l_warptile_mmq_int_k = { 128, 128, 128, 32, subgroup_size_8 * 2, 64, 1, 4, 4, 1, subgroup_size_8 };
|
||||
m_warptile_mmq_int_k = { 128, 64, 64, 32, subgroup_size_8, 32, 1, 2, 2, 1, subgroup_size_8 };
|
||||
s_warptile_mmq_int_k = { subgroup_size_32, 32, 32, 32, s_warptile_wm, 32, 1, 2, 1, 1, subgroup_size_8 };
|
||||
l_warptile_mmq_int_k = { 128, 128, 128, 32, mm_warp_8 * 2, 64, 1, 4, 4, 1, mm_warp_8 };
|
||||
m_warptile_mmq_int_k = { 128, 64, 64, 32, mm_warp_8, 32, 1, 2, 2, 1, mm_warp_8 };
|
||||
s_warptile_mmq_int_k = { subgroup_size_32, 32, 32, 32, s_warptile_wm, 32, 1, 2, 1, 1, subgroup_size_8 };
|
||||
|
||||
l_warptile_id = { 128, 128, 128, 16, mul_mat_subgroup_size_16 * 2, 64, 2, tm_l, tn_l, tk_l, mul_mat_subgroup_size_16 };
|
||||
m_warptile_id = { 128, 64, 64, 16, mul_mat_subgroup_size_16, 32, 2, tm_m, tn_m, tk_m, mul_mat_subgroup_size_16 };
|
||||
s_warptile_id = { mul_mat_subgroup_size_16, 32, 32, 16, s_warptile_wm, 32, 2, tm_s, tn_s, tk_s, mul_mat_subgroup_size_16 };
|
||||
l_warptile_id = { 128, 128, 128, 16, mul_mat_mm_warp_16 * 2, 64, 2, tm_l, tn_l, tk_l, mul_mat_mm_warp_16 };
|
||||
m_warptile_id = { 128, 64, 64, 16, mul_mat_mm_warp_16, 32, 2, tm_m, tn_m, tk_m, mul_mat_mm_warp_16 };
|
||||
s_warptile_id = { mul_mat_subgroup_size_16, 32, 32, 16, s_warptile_wm, 32, 2, tm_s, tn_s, tk_s, mul_mat_subgroup_size_16 };
|
||||
|
||||
l_warptile_mmqid = { 128, 128, 128, 32, mul_mat_subgroup_size_8 * 2, 64, 2, tm_l, tn_l, tk_l, mul_mat_subgroup_size_8 };
|
||||
m_warptile_mmqid = { 128, 64, 64, 32, mul_mat_subgroup_size_8, 32, 2, tm_m, tn_m, tk_m, mul_mat_subgroup_size_8 };
|
||||
s_warptile_mmqid = { mul_mat_subgroup_size_32, 32, 32, 32, s_warptile_wm, 32, 2, tm_s, tn_s, tk_s, mul_mat_subgroup_size_8 };
|
||||
l_warptile_mmqid = { 128, 128, 128, 32, mul_mat_mm_warp_8 * 2, 64, 2, tm_l, tn_l, tk_l, mul_mat_mm_warp_8 };
|
||||
m_warptile_mmqid = { 128, 64, 64, 32, mul_mat_mm_warp_8, 32, 2, tm_m, tn_m, tk_m, mul_mat_mm_warp_8 };
|
||||
s_warptile_mmqid = { mul_mat_subgroup_size_32, 32, 32, 32, s_warptile_wm, 32, 2, tm_s, tn_s, tk_s, mul_mat_subgroup_size_8 };
|
||||
|
||||
l_warptile_mmqid_int = { 128, 128, 128, 32, mul_mat_subgroup_size_8 * 2, 64, 2, 4, 4, 1, mul_mat_subgroup_size_8 };
|
||||
m_warptile_mmqid_int = { 128, 64, 64, 32, mul_mat_subgroup_size_8, 32, 2, 2, 2, 1, mul_mat_subgroup_size_8 };
|
||||
s_warptile_mmqid_int = { mul_mat_subgroup_size_32, 32, 32, 32, s_warptile_wm, 32, 2, 2, 1, 1, mul_mat_subgroup_size_8 };
|
||||
l_warptile_mmqid_int = { 128, 128, 128, 32, mul_mat_mm_warp_8 * 2, 64, 2, 4, 4, 1, mul_mat_mm_warp_8 };
|
||||
m_warptile_mmqid_int = { 128, 64, 64, 32, mul_mat_mm_warp_8, 32, 2, 2, 2, 1, mul_mat_mm_warp_8 };
|
||||
s_warptile_mmqid_int = { mul_mat_subgroup_size_32, 32, 32, 32, s_warptile_wm, 32, 2, 2, 1, 1, mul_mat_subgroup_size_8 };
|
||||
|
||||
l_warptile_mmqid_int_k = { 128, 128, 128, 32, mul_mat_subgroup_size_16 * 2, 64, 1, 4, 4, 1, mul_mat_subgroup_size_16 };
|
||||
m_warptile_mmqid_int_k = { 128, 64, 64, 32, mul_mat_subgroup_size_16, 32, 1, 2, 2, 1, mul_mat_subgroup_size_16 };
|
||||
s_warptile_mmqid_int_k = { mul_mat_subgroup_size_32, 32, 32, 32, s_warptile_wm, 32, 1, 2, 1, 1, mul_mat_subgroup_size_16 };
|
||||
l_warptile_mmqid_int_k = { 128, 128, 128, 32, mul_mat_mm_warp_16 * 2, 64, 1, 4, 4, 1, mul_mat_mm_warp_16 };
|
||||
m_warptile_mmqid_int_k = { 128, 64, 64, 32, mul_mat_mm_warp_16, 32, 1, 2, 2, 1, mul_mat_mm_warp_16 };
|
||||
s_warptile_mmqid_int_k = { mul_mat_subgroup_size_32, 32, 32, 32, s_warptile_wm, 32, 1, 2, 1, 1, mul_mat_subgroup_size_16 };
|
||||
|
||||
// chip specific tuning
|
||||
if ((device->architecture == AMD_GCN) && (device->driver_id != vk::DriverId::eAmdProprietary)) {
|
||||
@@ -4295,13 +4301,13 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) {
|
||||
m_warptile_mmqid = m_warptile_mmqid_int = { 256, 64, 64, 32, 16, 16, 2, 2, 2, 1, 16 };
|
||||
} else if (device->vendor_id == VK_VENDOR_ID_AMD && device->coopmat_support && device->driver_id != vk::DriverId::eAmdProprietary) {
|
||||
// This is intentionally using tx_m values, slight performance increase
|
||||
l_warptile = { 256, 128, 128, 16, subgroup_size_8, 64, 2, tm_m, tn_m, tk_m, subgroup_size_8 };
|
||||
l_warptile_mmq = l_warptile_mmq_int = { 256, 128, 128, 32, subgroup_size_8, 64, 2, tm_m, tn_m, tk_m, subgroup_size_8 };
|
||||
l_warptile_mmq_int_k = { 256, 128, 128, 32, subgroup_size_16, 64, 1, 4, 2, 1, subgroup_size_16 };
|
||||
l_warptile = { 256, 128, 128, 16, mm_warp_8, 64, 2, tm_m, tn_m, tk_m, mm_warp_8 };
|
||||
l_warptile_mmq = l_warptile_mmq_int = { 256, 128, 128, 32, mm_warp_8, 64, 2, tm_m, tn_m, tk_m, mm_warp_8 };
|
||||
l_warptile_mmq_int_k = { 256, 128, 128, 32, mm_warp_16, 64, 1, 4, 2, 1, mm_warp_16 };
|
||||
} else if (device->vendor_id == VK_VENDOR_ID_INTEL && device->coopmat_support) {
|
||||
// Xe2/Xe3 with coopmat enabled - warptile performance tuning
|
||||
l_warptile = { 512, 128, 128, 16, subgroup_size_8, 32, 2, tm_m, tn_m, tk_m, subgroup_size_8 };
|
||||
l_warptile_mmq = { 512, 128, 128, 32, subgroup_size_8, 32, 2, tm_m, tn_m, tk_m, subgroup_size_8 };
|
||||
l_warptile = { 512, 128, 128, 16, mm_warp_8, 32, 2, tm_m, tn_m, tk_m, mm_warp_8 };
|
||||
l_warptile_mmq = { 512, 128, 128, 32, mm_warp_8, 32, 2, tm_m, tn_m, tk_m, mm_warp_8 };
|
||||
}
|
||||
|
||||
l_mmq_wg_denoms = l_wg_denoms = {128, 128, 1 };
|
||||
@@ -5174,8 +5180,8 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) {
|
||||
const uint32_t s_warptile_wm = device->subgroup_size == 8 ? 8 : 32;
|
||||
|
||||
// use scalar tile sizes
|
||||
l_warptile = { 128, 128, 128, 16, subgroup_size_8 * 2, 64, 2, 4, 4, 1, subgroup_size_8 };
|
||||
m_warptile = { 128, 64, 64, 16, subgroup_size_8, 32, 2, 4, 2, 1, subgroup_size_8 };
|
||||
l_warptile = { 128, 128, 128, 16, mm_warp_8 * 2, 64, 2, 4, 4, 1, mm_warp_8 };
|
||||
m_warptile = { 128, 64, 64, 16, mm_warp_8, 32, 2, 4, 2, 1, mm_warp_8 };
|
||||
s_warptile = { subgroup_size_32, 32, 32, 16, s_warptile_wm, 32, 2, 2, 2, 1, subgroup_size_8 };
|
||||
|
||||
l_wg_denoms = {128, 128, 1 };
|
||||
|
||||
+2
-2
@@ -43,10 +43,10 @@
|
||||
#define LLAMA_FILE_MAGIC_GGSQ 0x67677371u // 'ggsq'
|
||||
|
||||
#define LLAMA_SESSION_MAGIC LLAMA_FILE_MAGIC_GGSN
|
||||
#define LLAMA_SESSION_VERSION 9
|
||||
#define LLAMA_SESSION_VERSION 10
|
||||
|
||||
#define LLAMA_STATE_SEQ_MAGIC LLAMA_FILE_MAGIC_GGSQ
|
||||
#define LLAMA_STATE_SEQ_VERSION 2
|
||||
#define LLAMA_STATE_SEQ_VERSION 3
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
|
||||
+92
-11
@@ -12,6 +12,7 @@
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <stdexcept>
|
||||
#include <unordered_map>
|
||||
|
||||
static bool ggml_is_power_of_2(int n) {
|
||||
return (n & (n - 1)) == 0;
|
||||
@@ -1128,11 +1129,18 @@ void llama_kv_cache::apply_ubatch(const slot_info & sinfo, const llama_ubatch &
|
||||
|
||||
cells.pos_set(idx, ubatch.pos[i]);
|
||||
|
||||
if (ubatch.is_pos_2d()) {
|
||||
llama_kv_cell_ext ext {
|
||||
/*.x =*/ ubatch.pos[i + ubatch.n_tokens*2],
|
||||
/*.y =*/ ubatch.pos[i + ubatch.n_tokens],
|
||||
};
|
||||
if (ubatch.is_pos_2d() || ubatch.token) {
|
||||
llama_kv_cell_ext ext;
|
||||
|
||||
if (ubatch.is_pos_2d()) {
|
||||
ext.x = ubatch.pos[i + ubatch.n_tokens*2];
|
||||
ext.y = ubatch.pos[i + ubatch.n_tokens];
|
||||
}
|
||||
|
||||
if (ubatch.token) {
|
||||
ext.tok = ubatch.token[i];
|
||||
}
|
||||
|
||||
cells.ext_set(idx, ext);
|
||||
}
|
||||
|
||||
@@ -1805,6 +1813,69 @@ void llama_kv_cache::set_input_v_rot(ggml_tensor * dst) const {
|
||||
memcpy(dst->data, attn_rot_hadamard.at(n_rot).data(), ggml_nbytes(dst));
|
||||
}
|
||||
|
||||
bool llama_kv_cache::has_cell_ext() const {
|
||||
return hparams.n_pos_per_embd() > 1;
|
||||
}
|
||||
|
||||
void llama_kv_cache::get_prev_tokens(const llama_ubatch & ubatch, uint32_t n, std::vector<llama_token> & res) const {
|
||||
const uint32_t n_tokens = ubatch.n_tokens;
|
||||
|
||||
res.clear();
|
||||
res.resize(n_tokens*n, LLAMA_TOKEN_NULL);
|
||||
|
||||
if (n == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// note: apply_ubatch() has already stored the current ubatch
|
||||
// the window below thus covers tokens of this very ubatch as well, which is what we want
|
||||
llama_pos p_min = std::numeric_limits<llama_pos>::max();
|
||||
llama_pos p_max = std::numeric_limits<llama_pos>::min();
|
||||
|
||||
std::bitset<LLAMA_MAX_SEQ> seqs;
|
||||
|
||||
for (uint32_t i = 0; i < n_tokens; ++i) {
|
||||
p_min = std::min(p_min, ubatch.pos[i]);
|
||||
p_max = std::max(p_max, ubatch.pos[i]);
|
||||
}
|
||||
|
||||
for (uint32_t s = 0; s < ubatch.n_seqs_unq; ++s) {
|
||||
seqs.set(ubatch.seq_id_unq[s]);
|
||||
}
|
||||
|
||||
// (seq_id, pos) -> token, for every cell that could be a predecessor of a ubatch token
|
||||
std::unordered_map<uint64_t, llama_token> hist;
|
||||
|
||||
const auto key = [](llama_seq_id seq_id, llama_pos pos) {
|
||||
return ((uint64_t) seq_id << 32) | (uint32_t) pos;
|
||||
};
|
||||
|
||||
for (uint32_t s = 0; s < n_stream; ++s) {
|
||||
v_cells[s].for_each_token_in(seqs, p_min - (llama_pos) n, p_max,
|
||||
[&](llama_seq_id seq_id, llama_pos pos, llama_token tok) {
|
||||
hist[key(seq_id, pos)] = tok;
|
||||
});
|
||||
}
|
||||
|
||||
for (uint32_t i = 0; i < n_tokens; ++i) {
|
||||
// TODO: a token that belongs to more than one sequence has an ambiguous history.
|
||||
// the n-gram architectures have to reject such batches
|
||||
const llama_seq_id seq_id = ubatch.seq_id[i][0];
|
||||
|
||||
for (uint32_t j = 0; j < n; ++j) {
|
||||
const llama_pos p = ubatch.pos[i] - (llama_pos) (n - j);
|
||||
if (p < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto it = hist.find(key(seq_id, p));
|
||||
if (it != hist.end()) {
|
||||
res[i*n + j] = it->second;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
size_t llama_kv_cache::total_size() const {
|
||||
size_t size = 0;
|
||||
|
||||
@@ -2106,7 +2177,7 @@ void llama_kv_cache::state_write_meta(llama_io_write_i & io, const cell_ranges_t
|
||||
io.write(&pos, sizeof(pos));
|
||||
io.write(&n_seq_id, sizeof(n_seq_id));
|
||||
|
||||
if (hparams.n_pos_per_embd() > 1) {
|
||||
if (has_cell_ext()) {
|
||||
const llama_kv_cell_ext ext = cells.ext_get(i);
|
||||
io.write(&ext, sizeof(ext));
|
||||
}
|
||||
@@ -2243,12 +2314,17 @@ bool llama_kv_cache::state_read_meta(llama_io_read_i & io, uint32_t strm, uint32
|
||||
return false;
|
||||
}
|
||||
|
||||
if (hparams.n_pos_per_embd() > 1) {
|
||||
if (has_cell_ext()) {
|
||||
llama_kv_cell_ext ext;
|
||||
io.read(&ext, sizeof(ext));
|
||||
|
||||
ubatch.pos[i + ubatch.n_tokens] = ext.y;
|
||||
ubatch.pos[i + ubatch.n_tokens*2] = ext.x;
|
||||
if (hparams.n_pos_per_embd() > 1) {
|
||||
ubatch.pos[i + ubatch.n_tokens] = ext.y;
|
||||
ubatch.pos[i + ubatch.n_tokens*2] = ext.x;
|
||||
}
|
||||
|
||||
// apply_ubatch() below restores ext.tok from the ubatch tokens
|
||||
ubatch.token[i] = ext.tok;
|
||||
}
|
||||
|
||||
// read the sequence id, but directly discard it - we will use dest_seq_id instead
|
||||
@@ -2268,7 +2344,8 @@ bool llama_kv_cache::state_read_meta(llama_io_read_i & io, uint32_t strm, uint32
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: we cannot yet restore llama_kv_cell_ext as the apply_ubatch() does not support it yet
|
||||
// note: apply_ubatch() rebuilds llama_kv_cell_ext from the ubatch
|
||||
// only ext.tok and the M-RoPE 2D position round-trip through it
|
||||
// see: https://github.com/ggml-org/llama.cpp/pull/16825#issuecomment-3460868350
|
||||
apply_ubatch(sinfo, ubatch);
|
||||
|
||||
@@ -2301,7 +2378,7 @@ bool llama_kv_cache::state_read_meta(llama_io_read_i & io, uint32_t strm, uint32
|
||||
|
||||
cells.pos_set(i, pos);
|
||||
|
||||
if (hparams.n_pos_per_embd() > 1) {
|
||||
if (has_cell_ext()) {
|
||||
llama_kv_cell_ext ext;
|
||||
io.read(&ext, sizeof(ext));
|
||||
cells.ext_set(i, ext);
|
||||
@@ -2652,3 +2729,7 @@ void llama_kv_cache_context::set_input_k_rot(ggml_tensor * dst) const {
|
||||
void llama_kv_cache_context::set_input_v_rot(ggml_tensor * dst) const {
|
||||
kv->set_input_v_rot(dst);
|
||||
}
|
||||
|
||||
void llama_kv_cache_context::get_prev_tokens(const llama_ubatch & ubatch, uint32_t n, std::vector<llama_token> & res) const {
|
||||
kv->get_prev_tokens(ubatch, n, res);
|
||||
}
|
||||
|
||||
@@ -219,6 +219,14 @@ public:
|
||||
void set_input_k_rot(ggml_tensor * dst) const;
|
||||
void set_input_v_rot(ggml_tensor * dst) const;
|
||||
|
||||
// true if llama_kv_cell_ext holds information that has to survive a state save/restore
|
||||
bool has_cell_ext() const;
|
||||
|
||||
// for every token of the ubatch, the ids of the n tokens that precede it in its sequence
|
||||
// entries with no matching cell are set to LLAMA_TOKEN_NULL
|
||||
// note: used by n-gram input embeddings
|
||||
void get_prev_tokens(const llama_ubatch & ubatch, uint32_t n, std::vector<llama_token> & res) const;
|
||||
|
||||
private:
|
||||
const llama_model & model;
|
||||
const llama_hparams & hparams;
|
||||
@@ -401,6 +409,9 @@ public:
|
||||
void set_input_k_rot(ggml_tensor * dst) const;
|
||||
void set_input_v_rot(ggml_tensor * dst) const;
|
||||
|
||||
// see llama_kv_cache::get_prev_tokens()
|
||||
void get_prev_tokens(const llama_ubatch & ubatch, uint32_t n, std::vector<llama_token> & res) const;
|
||||
|
||||
private:
|
||||
llama_memory_status status;
|
||||
|
||||
|
||||
+28
-1
@@ -15,6 +15,10 @@ struct llama_kv_cell_ext {
|
||||
llama_pos x = 0;
|
||||
llama_pos y = 0;
|
||||
|
||||
// when tok = LLAMA_TOKEN_NULL when the cell is produced by embedding input (i.e. multimodal)
|
||||
// use case: n-gram embeddings hash
|
||||
llama_token tok = LLAMA_TOKEN_NULL;
|
||||
|
||||
// return true if the current 2D spatial position is greater than other
|
||||
bool is_2d_gt(llama_pos ox, llama_pos oy) const {
|
||||
return (y > oy) || (y == oy && x > ox);
|
||||
@@ -23,7 +27,7 @@ struct llama_kv_cell_ext {
|
||||
void reset() {
|
||||
static_assert(std::is_trivially_copyable_v<llama_kv_cell_ext>);
|
||||
|
||||
memset(this, 0, sizeof(*this));
|
||||
*this = llama_kv_cell_ext{};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -305,6 +309,29 @@ public:
|
||||
return seq[i].test(seq_id);
|
||||
}
|
||||
|
||||
// gather the token ids of the cells in `seqs` with position in [p0, p1)
|
||||
// the callback receives (seq_id, pos, token) for every such (cell, seq) pair
|
||||
// note: used by n-gram input embeddings to recover the tokens preceding a ubatch
|
||||
template<typename F>
|
||||
void for_each_token_in(const std::bitset<LLAMA_MAX_SEQ> & seqs, llama_pos p0, llama_pos p1, F && f) const {
|
||||
for (const auto & i : used) {
|
||||
if (pos[i] < p0 || pos[i] >= p1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto m = seq[i] & seqs;
|
||||
if (m.none()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (llama_seq_id s = 0; s < LLAMA_MAX_SEQ; ++s) {
|
||||
if (m.test(s)) {
|
||||
f(s, pos[i], ext[i].tok);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// note: call only if the cell is not empty and the seq_id is not in the cell
|
||||
void seq_add(uint32_t i, llama_seq_id seq_id) {
|
||||
assert(i < pos.size());
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
ChatFormInputFileInputInvisible,
|
||||
ChatFormMcpResourcesList,
|
||||
ChatFormPickers,
|
||||
DialogMcpResourcesBrowser
|
||||
DialogMcpResourcesBrowser,
|
||||
DialogMcpServers
|
||||
} from '$lib/components/app';
|
||||
import {
|
||||
CLIPBOARD_CONTENT_QUOTE_PREFIX,
|
||||
@@ -183,6 +184,9 @@
|
||||
let isResourceDialogOpen = $state(false);
|
||||
let preSelectedResourceUri = $state<string | undefined>(undefined);
|
||||
|
||||
// MCP Servers Dialog State
|
||||
let isMcpServersDialogOpen = $state(false);
|
||||
|
||||
let currentConfig = $derived(settingsStore.config);
|
||||
|
||||
let pasteLongTextToFileLength = $derived.by(() => {
|
||||
@@ -618,6 +622,7 @@
|
||||
onFileUpload={handleFileUpload}
|
||||
onMcpPromptClick={showMcpPromptButton ? () => pickers.openPromptPicker() : undefined}
|
||||
onMcpResourcesClick={() => (isResourceDialogOpen = true)}
|
||||
onMcpSettingsClick={() => (isMcpServersDialogOpen = true)}
|
||||
onMicClick={handleMicClick}
|
||||
{onStop}
|
||||
onSystemPromptClick={() => onSystemPromptClick?.({ files: uploadedFiles, message: value })}
|
||||
@@ -656,3 +661,5 @@
|
||||
}}
|
||||
preSelectedUri={preSelectedResourceUri}
|
||||
/>
|
||||
|
||||
<DialogMcpServers bind:open={isMcpServersDialogOpen} />
|
||||
|
||||
+9
-39
@@ -1,10 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { File, FolderOpen, MessageSquare, Plus, Zap } from '@lucide/svelte';
|
||||
import {
|
||||
ChatFormActionAddMcpServersSubmenu,
|
||||
ChatFormActionAddReasoningSubmenu,
|
||||
ChatFormActionAddToolsSubmenu
|
||||
} from '$lib/components/app';
|
||||
import { File, MessageSquare, Plus } from '@lucide/svelte';
|
||||
import { ChatFormActionAddToolsSubmenu, McpLogo } from '$lib/components/app';
|
||||
import { buttonVariants } from '$lib/components/ui/button';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||
@@ -31,11 +27,6 @@
|
||||
// must not restore focus to the trigger on close
|
||||
let suppressCloseAutoFocus = false;
|
||||
|
||||
function handleMcpSettingsClick() {
|
||||
dropdownOpen = false;
|
||||
chatFormActions.onMcpSettingsClick?.();
|
||||
}
|
||||
|
||||
const attachmentMenu = useAttachmentMenu(
|
||||
() => ({
|
||||
hasAudioModality: chatFormActions.hasAudioModality,
|
||||
@@ -93,10 +84,6 @@
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ChatFormActionAddReasoningSubmenu />
|
||||
|
||||
<DropdownMenu.Separator />
|
||||
|
||||
<DropdownMenu.Sub>
|
||||
<DropdownMenu.SubTrigger class="flex cursor-pointer items-center gap-2">
|
||||
<File class={ICON_CLASS_DEFAULT} />
|
||||
@@ -156,31 +143,14 @@
|
||||
|
||||
<ChatFormActionAddToolsSubmenu />
|
||||
|
||||
<ChatFormActionAddMcpServersSubmenu onMcpSettingsClick={handleMcpSettingsClick} />
|
||||
<DropdownMenu.Item
|
||||
class="flex cursor-pointer items-center gap-2"
|
||||
onclick={chatFormActions.onMcpSettingsClick}
|
||||
>
|
||||
<McpLogo class={ICON_CLASS_DEFAULT} />
|
||||
|
||||
{#if chatFormActions.hasMcpPromptsSupport}
|
||||
<DropdownMenu.Separator />
|
||||
|
||||
<DropdownMenu.Item
|
||||
class="flex cursor-pointer items-center gap-2"
|
||||
onclick={chatFormActions.onMcpPromptClick}
|
||||
>
|
||||
<Zap class={ICON_CLASS_DEFAULT} />
|
||||
|
||||
<span>MCP Prompt</span>
|
||||
</DropdownMenu.Item>
|
||||
{/if}
|
||||
|
||||
{#if chatFormActions.hasMcpResourcesSupport}
|
||||
<DropdownMenu.Item
|
||||
class="flex cursor-pointer items-center gap-2"
|
||||
onclick={chatFormActions.onMcpResourcesClick}
|
||||
>
|
||||
<FolderOpen class={ICON_CLASS_DEFAULT} />
|
||||
|
||||
<span>MCP Resources</span>
|
||||
</DropdownMenu.Item>
|
||||
{/if}
|
||||
<span>MCP Servers</span>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</div>
|
||||
|
||||
-152
@@ -1,152 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { Plus, Settings } from '@lucide/svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { DropdownMenuSearchable, McpLogo, McpServerIdentity } from '$lib/components/app';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||
import { Switch } from '$lib/components/ui/switch';
|
||||
import { ICON_CLASS_DEFAULT, ROUTES } from '$lib/constants';
|
||||
import { HealthCheckStatus } from '$lib/enums';
|
||||
import { conversationsStore, mcpStore } from '$lib/stores';
|
||||
import type { MCPServerSettingsEntry } from '$lib/types';
|
||||
|
||||
interface Props {
|
||||
onMcpSettingsClick?: () => void;
|
||||
}
|
||||
|
||||
let { onMcpSettingsClick }: Props = $props();
|
||||
|
||||
let mcpSearchQuery = $state('');
|
||||
// Every configured server is listed; `enabled` is an on/off state,
|
||||
// not a visibility filter, so a disabled server stays toggleable.
|
||||
let mcpServers = $derived(mcpStore.getServers());
|
||||
let hasMcpServers = $derived(mcpServers.length > 0);
|
||||
let filteredMcpServers = $derived.by(() => {
|
||||
const query = mcpSearchQuery.toLowerCase().trim();
|
||||
|
||||
if (!query) return mcpServers;
|
||||
|
||||
return mcpServers.filter((s) => {
|
||||
const name = getServerLabel(s).toLowerCase();
|
||||
const url = s.url.toLowerCase();
|
||||
|
||||
return name.includes(query) || url.includes(query);
|
||||
});
|
||||
});
|
||||
|
||||
function getServerLabel(server: MCPServerSettingsEntry): string {
|
||||
return mcpStore.getServerLabel(server);
|
||||
}
|
||||
|
||||
function isServerEnabledForChat(serverId: string): boolean {
|
||||
return conversationsStore.preferences.isMcpServerEnabledForChat(serverId);
|
||||
}
|
||||
|
||||
async function toggleServerForChat(serverId: string) {
|
||||
await conversationsStore.preferences.toggleMcpServerForChat(serverId);
|
||||
}
|
||||
|
||||
function handleMcpSubMenuOpen(open: boolean) {
|
||||
if (open) {
|
||||
mcpSearchQuery = '';
|
||||
mcpStore.runHealthChecksForServers(mcpServers);
|
||||
}
|
||||
}
|
||||
|
||||
function handleMcpSettingsClick() {
|
||||
onMcpSettingsClick?.();
|
||||
|
||||
goto(`${hasMcpServers ? '' : '?add'}${ROUTES.MCP_SERVERS}`);
|
||||
}
|
||||
</script>
|
||||
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Sub onOpenChange={handleMcpSubMenuOpen}>
|
||||
<DropdownMenu.SubTrigger class="flex cursor-pointer items-center gap-2">
|
||||
<McpLogo class={ICON_CLASS_DEFAULT} />
|
||||
|
||||
<span>MCP Servers</span>
|
||||
</DropdownMenu.SubTrigger>
|
||||
|
||||
<DropdownMenu.SubContent class="w-72 pt-0">
|
||||
{#if hasMcpServers}
|
||||
<DropdownMenuSearchable
|
||||
bind:searchValue={mcpSearchQuery}
|
||||
emptyMessage="No servers found"
|
||||
isEmpty={filteredMcpServers.length === 0}
|
||||
placeholder="Search servers..."
|
||||
>
|
||||
<div class="max-h-64 overflow-y-auto">
|
||||
{#each filteredMcpServers as server (server.id)}
|
||||
{@const healthState = mcpStore.getHealthCheckState(server.id)}
|
||||
{@const hasError = healthState.status === HealthCheckStatus.ERROR}
|
||||
{@const isEnabledForChat = isServerEnabledForChat(server.id)}
|
||||
{@const displayName = getServerLabel(server)}
|
||||
{@const faviconUrl = mcpStore.getServerFavicon(server.id)}
|
||||
|
||||
<button
|
||||
class="flex w-full items-center justify-between gap-2 rounded-sm px-2 py-2 text-left transition-colors hover:bg-accent disabled:cursor-not-allowed disabled:opacity-50"
|
||||
disabled={hasError}
|
||||
onclick={() => !hasError && toggleServerForChat(server.id)}
|
||||
type="button"
|
||||
>
|
||||
<div class="flex min-w-0 flex-1 items-center gap-2">
|
||||
<div class="min-w-0 flex-1">
|
||||
<McpServerIdentity
|
||||
{displayName}
|
||||
{faviconUrl}
|
||||
iconClass={ICON_CLASS_DEFAULT}
|
||||
iconRounded="rounded-sm"
|
||||
nameClass="text-sm"
|
||||
showVersion={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if hasError}
|
||||
<span
|
||||
class="shrink-0 rounded bg-destructive/15 px-1.5 py-0.5 text-xs text-destructive"
|
||||
>
|
||||
Error
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<Switch
|
||||
checked={isEnabledForChat}
|
||||
disabled={hasError}
|
||||
onCheckedChange={() => toggleServerForChat(server.id)}
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#snippet footer()}
|
||||
<DropdownMenu.Item
|
||||
class="flex cursor-pointer items-center gap-2"
|
||||
onclick={handleMcpSettingsClick}
|
||||
>
|
||||
<Settings class={ICON_CLASS_DEFAULT} />
|
||||
|
||||
<span>Manage MCP Servers</span>
|
||||
</DropdownMenu.Item>
|
||||
{/snippet}
|
||||
</DropdownMenuSearchable>
|
||||
{:else}
|
||||
<div class="px-2 py-3 text-center text-sm text-muted-foreground">
|
||||
No MCP servers configured
|
||||
</div>
|
||||
|
||||
<DropdownMenu.Separator />
|
||||
|
||||
<DropdownMenu.Item
|
||||
class="flex cursor-pointer items-center gap-2"
|
||||
onclick={handleMcpSettingsClick}
|
||||
>
|
||||
<Plus class={ICON_CLASS_DEFAULT} />
|
||||
|
||||
<span>Add MCP Servers</span>
|
||||
</DropdownMenu.Item>
|
||||
{/if}
|
||||
</DropdownMenu.SubContent>
|
||||
</DropdownMenu.Sub>
|
||||
</DropdownMenu.Root>
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
<script lang="ts">
|
||||
import { FolderOpen, Server, Zap } from '@lucide/svelte';
|
||||
import { McpLogo } from '$lib/components/app';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||
import { ICON_CLASS_DEFAULT } from '$lib/constants';
|
||||
import { getChatFormActionsContext } from '$lib/contexts';
|
||||
|
||||
const chatFormActions = getChatFormActionsContext();
|
||||
|
||||
function handleServersClick() {
|
||||
chatFormActions.onMcpSettingsClick?.();
|
||||
}
|
||||
</script>
|
||||
|
||||
<DropdownMenu.Sub>
|
||||
<DropdownMenu.SubTrigger class="flex cursor-pointer items-center gap-2">
|
||||
<McpLogo class={ICON_CLASS_DEFAULT} />
|
||||
|
||||
<span>MCP</span>
|
||||
</DropdownMenu.SubTrigger>
|
||||
|
||||
<DropdownMenu.SubContent class="w-48">
|
||||
<DropdownMenu.Item class="flex cursor-pointer items-center gap-2" onclick={handleServersClick}>
|
||||
<Server class={ICON_CLASS_DEFAULT} />
|
||||
|
||||
<span>Servers</span>
|
||||
</DropdownMenu.Item>
|
||||
|
||||
{#if chatFormActions.hasMcpPromptsSupport}
|
||||
<DropdownMenu.Item
|
||||
class="flex cursor-pointer items-center gap-2"
|
||||
onclick={chatFormActions.onMcpPromptClick}
|
||||
>
|
||||
<Zap class={ICON_CLASS_DEFAULT} />
|
||||
|
||||
<span>Prompts</span>
|
||||
</DropdownMenu.Item>
|
||||
{/if}
|
||||
|
||||
{#if chatFormActions.hasMcpResourcesSupport}
|
||||
<DropdownMenu.Item
|
||||
class="flex cursor-pointer items-center gap-2"
|
||||
onclick={chatFormActions.onMcpResourcesClick}
|
||||
>
|
||||
<FolderOpen class={ICON_CLASS_DEFAULT} />
|
||||
|
||||
<span>Resources</span>
|
||||
</DropdownMenu.Item>
|
||||
{/if}
|
||||
</DropdownMenu.SubContent>
|
||||
</DropdownMenu.Sub>
|
||||
+4
-3
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { SkipForward, Square } from '@lucide/svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import {
|
||||
ChatFormActionModels,
|
||||
@@ -10,7 +9,7 @@
|
||||
ChatFormContextGauge
|
||||
} from '$lib/components/app';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { ICON_CLASS_DEFAULT, ROUTES } from '$lib/constants';
|
||||
import { ICON_CLASS_DEFAULT } from '$lib/constants';
|
||||
import { setChatFormActionsContext } from '$lib/contexts';
|
||||
import { FileTypeCategory, MessageRole } from '$lib/enums';
|
||||
import { ChatService } from '$lib/services';
|
||||
@@ -34,6 +33,7 @@
|
||||
onSystemPromptClick?: () => void;
|
||||
onMcpPromptClick?: () => void;
|
||||
onMcpResourcesClick?: () => void;
|
||||
onMcpSettingsClick?: () => void;
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -47,6 +47,7 @@
|
||||
onFileUpload,
|
||||
onMcpPromptClick,
|
||||
onMcpResourcesClick,
|
||||
onMcpSettingsClick,
|
||||
onMicClick,
|
||||
onStop,
|
||||
onSystemPromptClick,
|
||||
@@ -163,7 +164,7 @@
|
||||
return onMcpResourcesClick;
|
||||
},
|
||||
get onMcpSettingsClick() {
|
||||
return () => goto(ROUTES.MCP_SERVERS);
|
||||
return onMcpSettingsClick;
|
||||
},
|
||||
get onSystemPromptClick() {
|
||||
return onSystemPromptClick;
|
||||
|
||||
@@ -221,25 +221,17 @@ export { default as ChatFormActionModels } from './ChatForm/ChatFormActions/Chat
|
||||
export { default as ChatFormActionAddToolsSubmenu } from './ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddToolsSubmenu.svelte';
|
||||
|
||||
/**
|
||||
* Dropdown submenu for managing MCP servers in the chat form.
|
||||
* Dropdown submenu for MCP prompts and resources in the chat form.
|
||||
*
|
||||
* Displays a searchable list of enabled MCP servers with toggle switches
|
||||
* to enable/disable each server for chat. Shows server favicon, health status,
|
||||
* and a "Manage MCP Servers" settings link.
|
||||
*
|
||||
* Features:
|
||||
* - Search/filter servers by name or URL
|
||||
* - Per-server toggle to enable/disable for chat
|
||||
* - Health check indicator (shows "Error" badge for failed servers)
|
||||
* - Server favicon display
|
||||
* - Settings link to manage MCP server configuration
|
||||
* Shows an "MCP" sub-menu item with entries for MCP Prompts and MCP
|
||||
* Resources. Only visible when the server supports them.
|
||||
*
|
||||
* @example
|
||||
* ```svelte
|
||||
* <ChatFormActionAddMcpServersSubmenu onMcpSettingsClick={handleMcpSettingsClick} />
|
||||
* <ChatFormActionAddMcpSubmenu />
|
||||
* ```
|
||||
*/
|
||||
export { default as ChatFormActionAddMcpServersSubmenu } from './ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddMcpServersSubmenu.svelte';
|
||||
export { default as ChatFormActionAddMcpSubmenu } from './ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddMcpSubmenu.svelte';
|
||||
|
||||
/**
|
||||
* Dropdown submenu for selecting reasoning effort level.
|
||||
|
||||
@@ -253,7 +253,7 @@
|
||||
</script>
|
||||
|
||||
<Dialog.Root onOpenChange={handleOpenChange} {open}>
|
||||
<Dialog.Content class="max-h-[80vh] !max-w-4xl overflow-hidden p-0">
|
||||
<Dialog.Content class="max-h-[80vh] md:max-w-4xl! w-full! overflow-hidden p-0">
|
||||
<Dialog.Header class="border-b border-border/30 px-6 py-4">
|
||||
<Dialog.Title class="flex items-center gap-2">
|
||||
<FolderOpen class="h-5 w-5" />
|
||||
|
||||
@@ -246,7 +246,7 @@
|
||||
</script>
|
||||
|
||||
<Dialog.Root onOpenChange={handleOpenChange} {open}>
|
||||
<Dialog.Content class="sm:max-w-2xl">
|
||||
<Dialog.Content class="max-w-2xl!">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title class="select-none">Add New MCP Server</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<script lang="ts">
|
||||
import { McpLogo } from '$lib/components/app';
|
||||
import { SettingsMcpServers } from '$lib/components/app/settings';
|
||||
import * as Dialog from '$lib/components/ui/dialog';
|
||||
|
||||
interface Props {
|
||||
open?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
}
|
||||
|
||||
let { onOpenChange, open = $bindable(false) }: Props = $props();
|
||||
|
||||
function handleOpenChange(value: boolean) {
|
||||
open = value;
|
||||
onOpenChange?.(value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog.Root onOpenChange={handleOpenChange} {open}>
|
||||
<Dialog.Content
|
||||
class="md:h-[calc(100vh-4rem)]! md:max-h-240! md:w-[calc(100vw-4rem)]! md:max-w-360! flex flex-col"
|
||||
>
|
||||
<Dialog.Header>
|
||||
<Dialog.Title class="flex items-center gap-2">
|
||||
<McpLogo class="h-5 w-5" />
|
||||
|
||||
<span>MCP Servers</span>
|
||||
</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
|
||||
<SettingsMcpServers class="mt-4" />
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
@@ -14,6 +14,7 @@
|
||||
<Dialog.Root bind:open {onOpenChange}>
|
||||
<Dialog.Content
|
||||
class="z-999999 grid max-h-full max-w-full! grid-rows-[1fr_auto] overflow-hidden p-0 md:h-[90vh] md:max-w-[90vw]!"
|
||||
showCloseButton
|
||||
>
|
||||
<MermaidPreview {svgHtml} />
|
||||
</Dialog.Content>
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<script lang="ts">
|
||||
import { Settings } from '@lucide/svelte';
|
||||
import { SettingsChat } from '$lib/components/app/settings';
|
||||
import * as Dialog from '$lib/components/ui/dialog';
|
||||
|
||||
interface Props {
|
||||
open?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
initialSection?: string;
|
||||
}
|
||||
|
||||
let { initialSection, onOpenChange, open = $bindable(false) }: Props = $props();
|
||||
|
||||
function handleOpenChange(value: boolean) {
|
||||
open = value;
|
||||
onOpenChange?.(value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog.Root onOpenChange={handleOpenChange} {open}>
|
||||
<Dialog.Content
|
||||
class="md:h-[calc(100vh-4rem)]! md:max-h-240! md:w-[calc(100vw-4rem)]! md:max-w-6xl! flex flex-col p-0 md:p-6 gap-0"
|
||||
>
|
||||
<Dialog.Header class="md:p-0 p-4">
|
||||
<Dialog.Title class="flex items-center gap-2">
|
||||
<Settings class="h-5 w-5" />
|
||||
|
||||
<span>Settings</span>
|
||||
</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
|
||||
<SettingsChat {initialSection} onClose={() => (open = false)} onSectionChange={() => {}} />
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
@@ -18,6 +18,23 @@
|
||||
*/
|
||||
export { default as DialogMcpServerAddNew } from './DialogMcpServerAddNew.svelte';
|
||||
|
||||
/**
|
||||
* **DialogMcpServers** - MCP servers dialog shown from the chat form
|
||||
*
|
||||
* Shows the same MCP server list as the `/mcp-servers` route inside a modal
|
||||
* dialog.
|
||||
*/
|
||||
export { default as DialogMcpServers } from './DialogMcpServers.svelte';
|
||||
|
||||
/**
|
||||
* **DialogSettingsChat** - Chat settings shown in a modal dialog
|
||||
*
|
||||
* Wraps the full SettingsChat layout (sidebar, mobile header, fields, footer)
|
||||
* inside a ShadCN Dialog instead of a dedicated route. Section switching is
|
||||
* handled in-app via `onSectionChange` rather than URL navigation.
|
||||
*/
|
||||
export { default as DialogSettingsChat } from './DialogSettingsChat.svelte';
|
||||
|
||||
/**
|
||||
* **DialogExportSettings** - Settings export dialog with sensitive data warning
|
||||
*
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
<script lang="ts">
|
||||
import { Database, FileText, ListChecks, MessageSquare, Sparkles, Wrench } from '@lucide/svelte';
|
||||
import {
|
||||
Database,
|
||||
ExternalLink,
|
||||
FileText,
|
||||
ListChecks,
|
||||
MessageSquare,
|
||||
Sparkles,
|
||||
Wrench
|
||||
} from '@lucide/svelte';
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
import type { MCPCapabilitiesInfo } from '$lib/types';
|
||||
|
||||
interface Props {
|
||||
capabilities?: MCPCapabilitiesInfo;
|
||||
onBrowseResources?: () => void;
|
||||
}
|
||||
|
||||
let { capabilities }: Props = $props();
|
||||
let { capabilities, onBrowseResources }: Props = $props();
|
||||
</script>
|
||||
|
||||
{#if capabilities}
|
||||
@@ -20,10 +29,24 @@
|
||||
{/if}
|
||||
|
||||
{#if capabilities.server.resources}
|
||||
<Badge class="h-5 gap-1 bg-blue-50 px-1.5 text-[10px] dark:bg-blue-950" variant="outline">
|
||||
<Badge
|
||||
class="h-5 cursor-pointer gap-1 bg-blue-50 px-1.5 text-[10px] transition-colors hover:bg-blue-100 dark:bg-blue-950 dark:hover:bg-blue-900"
|
||||
onclick={onBrowseResources}
|
||||
onkeydown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
onBrowseResources?.();
|
||||
}
|
||||
}}
|
||||
role="button"
|
||||
tabindex={0}
|
||||
variant="outline"
|
||||
>
|
||||
<Database class="h-3 w-3 text-blue-600 dark:text-blue-400" />
|
||||
|
||||
Resources
|
||||
|
||||
<ExternalLink class="h-3 w-3 text-blue-600 dark:text-blue-400" />
|
||||
</Badge>
|
||||
{/if}
|
||||
|
||||
|
||||
@@ -22,9 +22,10 @@
|
||||
onToggle: (enabled: boolean) => void;
|
||||
onUpdate: (updates: Partial<MCPServerSettingsEntry>) => void;
|
||||
onDelete: () => void;
|
||||
onBrowseResources?: () => void;
|
||||
}
|
||||
|
||||
let { enabled, onDelete, onToggle, onUpdate, server }: Props = $props();
|
||||
let { enabled, onBrowseResources, onDelete, onToggle, onUpdate, server }: Props = $props();
|
||||
|
||||
let healthState = $derived<HealthCheckState>(mcpStore.getHealthCheckState(server.id));
|
||||
let displayName = $derived(mcpStore.getServerLabel(server));
|
||||
@@ -125,6 +126,7 @@
|
||||
{displayName}
|
||||
enabled={enabled ?? server.enabled}
|
||||
{faviconUrl}
|
||||
{onBrowseResources}
|
||||
{onToggle}
|
||||
{serverInfo}
|
||||
{transportType}
|
||||
@@ -191,12 +193,14 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<McpServerCardActions
|
||||
{isHealthChecking}
|
||||
onDelete={handleDeleteClick}
|
||||
onEdit={startEditing}
|
||||
onRefresh={handleHealthCheck}
|
||||
/>
|
||||
<div class="flex items-center gap-2">
|
||||
<McpServerCardActions
|
||||
{isHealthChecking}
|
||||
onDelete={handleDeleteClick}
|
||||
onEdit={startEditing}
|
||||
onRefresh={handleHealthCheck}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</Card.Root>
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
enabled: boolean;
|
||||
disabled?: boolean;
|
||||
onToggle: (enabled: boolean) => void;
|
||||
onBrowseResources?: () => void;
|
||||
serverInfo?: MCPServerInfo;
|
||||
capabilities?: MCPCapabilitiesInfo;
|
||||
transportType?: MCPTransportType;
|
||||
@@ -23,6 +24,7 @@
|
||||
displayName,
|
||||
enabled,
|
||||
faviconUrl,
|
||||
onBrowseResources,
|
||||
onToggle,
|
||||
serverInfo,
|
||||
transportType
|
||||
@@ -57,7 +59,7 @@
|
||||
{/if}
|
||||
|
||||
{#if capabilities}
|
||||
<McpCapabilitiesBadges {capabilities} />
|
||||
<McpCapabilitiesBadges {capabilities} {onBrowseResources} />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
+6
-1
@@ -5,6 +5,7 @@
|
||||
import {
|
||||
ActionIcon,
|
||||
DialogConversationRename,
|
||||
DialogSettingsChat,
|
||||
Logo,
|
||||
SidebarNavigationActions,
|
||||
SidebarNavigationConversationList
|
||||
@@ -91,6 +92,7 @@
|
||||
let selectedIds = new SvelteSet<string>();
|
||||
|
||||
let renameDialogOpen = $state(false);
|
||||
let settingsDialogOpen = $state(false);
|
||||
let renameTargetConversationId = $state<string | null>(null);
|
||||
let renameDraft = $state('');
|
||||
let renameOriginalTitle = $state('');
|
||||
@@ -308,7 +310,7 @@
|
||||
|
||||
<svelte:window bind:innerWidth onkeydown={handleKeydown} />
|
||||
|
||||
{#if innerWidth > 768 || (!page.url.hash.includes(ROUTES.SETTINGS) && !page.url.hash.includes(ROUTES.MCP_SERVERS) && !page.url.hash.includes(ROUTES.SEARCH))}
|
||||
{#if innerWidth > 768 || !page.url.hash.includes(ROUTES.SEARCH)}
|
||||
<aside
|
||||
class={[
|
||||
'fixed md:sticky top-2 left-2 md:left-0 md:ml-2 md:mt-2 pt-2 z-10 w-[calc(100dvw-1rem)]',
|
||||
@@ -400,6 +402,7 @@
|
||||
isSearchModeActive = false;
|
||||
searchQuery = '';
|
||||
}}
|
||||
onSettingsClick={() => (settingsDialogOpen = true)}
|
||||
/>
|
||||
|
||||
{#if uiStore.isSidebarExpanded || isOnMobile}
|
||||
@@ -447,6 +450,8 @@
|
||||
onConfirm={handleRenameConfirm}
|
||||
/>
|
||||
|
||||
<DialogSettingsChat bind:open={settingsDialogOpen} />
|
||||
|
||||
<style>
|
||||
aside {
|
||||
@media (max-width: 768px) {
|
||||
|
||||
+22
-16
@@ -26,6 +26,7 @@
|
||||
onSearchDeactivated?: () => void;
|
||||
onSearchClick?: () => void;
|
||||
onNewChat?: () => void;
|
||||
onSettingsClick?: () => void;
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -35,6 +36,7 @@
|
||||
onNewChat,
|
||||
onSearchClick,
|
||||
onSearchDeactivated,
|
||||
onSettingsClick,
|
||||
searchQuery = $bindable('')
|
||||
}: Props = $props();
|
||||
|
||||
@@ -115,14 +117,16 @@
|
||||
onNewChat?.();
|
||||
void conversationsStore.openNewChat();
|
||||
}
|
||||
: item.route
|
||||
? () => {
|
||||
onNewChat?.();
|
||||
goto(item.route!);
|
||||
}
|
||||
: isSearchOnMobile
|
||||
? undefined
|
||||
: onSearchClick}
|
||||
: item.action === SidebarAction.SETTINGS
|
||||
? () => onSettingsClick?.()
|
||||
: item.route
|
||||
? () => {
|
||||
onNewChat?.();
|
||||
goto(item.route!);
|
||||
}
|
||||
: isSearchOnMobile
|
||||
? undefined
|
||||
: onSearchClick}
|
||||
{@const itemTransition = {
|
||||
delay: !initialized ? i * ICON_STRIP_TRANSITION_DELAY_MULTIPLIER : 0,
|
||||
duration: ICON_STRIP_TRANSITION_DURATION,
|
||||
@@ -169,14 +173,16 @@
|
||||
onNewChat?.();
|
||||
void conversationsStore.openNewChat();
|
||||
}
|
||||
: item.route
|
||||
? () => {
|
||||
onNewChat?.();
|
||||
goto(item.route!);
|
||||
}
|
||||
: isSearchOnMobile
|
||||
? undefined
|
||||
: onSearchClick}
|
||||
: item.action === SidebarAction.SETTINGS
|
||||
? () => onSettingsClick?.()
|
||||
: item.route
|
||||
? () => {
|
||||
onNewChat?.();
|
||||
goto(item.route!);
|
||||
}
|
||||
: isSearchOnMobile
|
||||
? undefined
|
||||
: onSearchClick}
|
||||
{@const itemTransition = {
|
||||
delay: !initialized ? i * ICON_STRIP_TRANSITION_DELAY_MULTIPLIER : 0,
|
||||
duration: ICON_STRIP_TRANSITION_DURATION,
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { RefreshCw } from '@lucide/svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import {
|
||||
SettingsChatDesktopSidebar,
|
||||
SettingsChatFields,
|
||||
@@ -18,21 +16,29 @@
|
||||
SETTINGS_SECTION_SLUGS
|
||||
} from '$lib/constants';
|
||||
import { ColorMode } from '$lib/enums/ui.enums';
|
||||
import { RouterService } from '$lib/services/router.service';
|
||||
import { modelsStore, serverStore, settingsReferrer, settingsStore } from '$lib/stores';
|
||||
import type { SettingsSection } from '$lib/types';
|
||||
import { modelsStore, serverStore, settingsStore } from '$lib/stores';
|
||||
import type { SettingsSection, SettingsSectionTitle } from '$lib/types';
|
||||
import { setMode } from 'mode-watcher';
|
||||
import { fade } from 'svelte/transition';
|
||||
interface Props {
|
||||
initialSection?: string;
|
||||
getSectionHref?: (section: SettingsSection) => string;
|
||||
onSectionChange?: (section: SettingsSectionTitle) => void;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
let { getSectionHref, initialSection }: Props = $props();
|
||||
let { initialSection, onClose, onSectionChange }: Props = $props();
|
||||
|
||||
let activeSlug = $derived(
|
||||
initialSection ?? (page.params as Record<string, string | undefined>).section ?? 'general'
|
||||
);
|
||||
let activeSlug = $derived(initialSection ?? 'general');
|
||||
|
||||
function handleSectionChange(section: SettingsSectionTitle) {
|
||||
const found = SETTINGS_CHAT_SECTIONS.find((s) => s.title === section);
|
||||
|
||||
if (found) {
|
||||
activeSlug = found.slug;
|
||||
}
|
||||
|
||||
onSectionChange?.(section);
|
||||
}
|
||||
|
||||
let currentSection = $derived(
|
||||
SETTINGS_CHAT_SECTIONS.find((section) => section.slug === activeSlug) ||
|
||||
@@ -115,7 +121,7 @@
|
||||
}
|
||||
|
||||
settingsStore.updateMultipleConfig(processedConfig);
|
||||
goto(settingsReferrer.url);
|
||||
onClose?.();
|
||||
}
|
||||
|
||||
export function reset() {
|
||||
@@ -123,32 +129,24 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div in:fade={{ duration: 150 }} class="mx-auto flex h-full w-full flex-col md:pl-8">
|
||||
<div class="flex flex-1 flex-col gap-4 md:flex-row">
|
||||
<div in:fade={{ duration: 150 }} class="mx-auto flex h-full w-full flex-col">
|
||||
<div class="flex flex-1 flex-col md:flex-row md:gap-4">
|
||||
<SettingsChatDesktopSidebar
|
||||
getHref={getSectionHref ??
|
||||
((section: SettingsSection) => RouterService.settings(section.slug))}
|
||||
isActive={(section: SettingsSection) => section.slug === activeSlug}
|
||||
onSectionChange={handleSectionChange}
|
||||
sections={SETTINGS_CHAT_SECTIONS}
|
||||
/>
|
||||
|
||||
<SettingsChatMobileHeader
|
||||
bind:this={mobileHeader}
|
||||
getHref={getSectionHref ??
|
||||
((section: SettingsSection) => RouterService.settings(section.slug))}
|
||||
isActive={(section: SettingsSection) => section.slug === activeSlug}
|
||||
onSectionChange={handleSectionChange}
|
||||
sections={SETTINGS_CHAT_SECTIONS}
|
||||
/>
|
||||
|
||||
<div class="mx-auto max-w-3xl flex-1">
|
||||
<div class="space-y-6 p-4 md:p-6 md:pt-28">
|
||||
<div class="mx-auto max-w-2xl px-4 flex-1 md:mt-4">
|
||||
<div class="space-y-6 pt-3">
|
||||
<div class="grid">
|
||||
<div class="mb-6 flex items-center gap-2 border-b border-border/30 pb-6 md:flex">
|
||||
<currentSection.icon class="h-5 w-5" />
|
||||
|
||||
<h3 class="text-lg font-semibold">{currentSection.title}</h3>
|
||||
</div>
|
||||
|
||||
{#if currentSection.slug === SETTINGS_SECTION_SLUGS.TOOLS}
|
||||
<SettingsChatToolsTab />
|
||||
{:else if currentSection.slug === SETTINGS_SECTION_SLUGS.IMPORT_EXPORT}
|
||||
|
||||
@@ -1,54 +1,31 @@
|
||||
<script lang="ts">
|
||||
import { Settings } from '@lucide/svelte';
|
||||
import { ICON_CLASS_DEFAULT } from '$lib/constants';
|
||||
import type { SettingsSection, SettingsSectionTitle } from '$lib/types';
|
||||
|
||||
interface Props {
|
||||
sections: SettingsSection[];
|
||||
isActive: (section: SettingsSection) => boolean;
|
||||
getHref?: (section: SettingsSection) => string;
|
||||
onSectionChange?: (section: SettingsSectionTitle) => void;
|
||||
}
|
||||
|
||||
let { getHref, isActive, onSectionChange, sections }: Props = $props();
|
||||
let { isActive, onSectionChange, sections }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="sticky top-2 hidden w-64 flex-col self-start bg-background py-4 md:flex gap-6">
|
||||
<div class="flex items-center gap-2 py-2">
|
||||
<Settings class="h-5 w-5 md:h-6 md:w-6" />
|
||||
|
||||
<h1 class="text-xl font-semibold md:text-2xl">Settings</h1>
|
||||
</div>
|
||||
|
||||
<div class="sticky top-12 hidden w-64 flex-col self-start bg-background md:flex gap-6">
|
||||
<nav class="space-y-1">
|
||||
{#each sections as section (section.title)}
|
||||
{#if getHref}
|
||||
<a
|
||||
class="flex w-full cursor-pointer items-center gap-3 rounded-lg px-3 py-2 text-left text-sm no-underline transition-colors hover:bg-accent {isActive(
|
||||
section
|
||||
)
|
||||
? 'bg-accent text-accent-foreground'
|
||||
: 'text-muted-foreground'}"
|
||||
href={getHref(section)}
|
||||
>
|
||||
<section.icon class={ICON_CLASS_DEFAULT} />
|
||||
<button
|
||||
class="flex w-full cursor-pointer items-center gap-3 rounded-lg px-3 py-2 text-left text-sm transition-colors hover:bg-accent {isActive(
|
||||
section
|
||||
)
|
||||
? 'bg-accent text-accent-foreground'
|
||||
: 'text-muted-foreground'}"
|
||||
onclick={() => onSectionChange?.(section.title)}
|
||||
>
|
||||
<section.icon class={ICON_CLASS_DEFAULT} />
|
||||
|
||||
<span class="ml-2">{section.title}</span>
|
||||
</a>
|
||||
{:else}
|
||||
<button
|
||||
class="flex w-full cursor-pointer items-center gap-3 rounded-lg px-3 py-2 text-left text-sm transition-colors hover:bg-accent {isActive(
|
||||
section
|
||||
)
|
||||
? 'bg-accent text-accent-foreground'
|
||||
: 'text-muted-foreground'}"
|
||||
onclick={() => onSectionChange?.(section.title)}
|
||||
>
|
||||
<section.icon class={ICON_CLASS_DEFAULT} />
|
||||
|
||||
<span class="ml-2">{section.title}</span>
|
||||
</button>
|
||||
{/if}
|
||||
<span class="ml-2">{section.title}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import { Settings } from '@lucide/svelte';
|
||||
import { ScrollCarousel } from '$lib/components/app';
|
||||
import { ICON_CLASS_DEFAULT, UI_DATA_ATTRS } from '$lib/constants';
|
||||
import { BooleanString } from '$lib/enums';
|
||||
@@ -10,11 +9,10 @@
|
||||
interface Props {
|
||||
sections: SettingsSection[];
|
||||
isActive: (section: SettingsSection) => boolean;
|
||||
getHref?: (section: SettingsSection) => string;
|
||||
onSectionChange?: (section: SettingsSectionTitle) => void;
|
||||
}
|
||||
|
||||
let { getHref, isActive, onSectionChange, sections }: Props = $props();
|
||||
let { isActive, onSectionChange, sections }: Props = $props();
|
||||
|
||||
const carousel = useScrollCarousel();
|
||||
|
||||
@@ -37,51 +35,26 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="sticky top-0 z-10 flex flex-col bg-background md:hidden">
|
||||
<div class="flex items-center gap-2 px-4 pt-4 pb-2 md:pt-6">
|
||||
<Settings class="h-5 w-5 md:h-6 md:w-6" />
|
||||
|
||||
<h1 class="text-xl font-semibold md:text-2xl">Settings</h1>
|
||||
</div>
|
||||
|
||||
<div class="border-b border-border/30 py-2">
|
||||
<div class="flex flex-col bg-background md:hidden sticky top-13 z-50">
|
||||
<div class="border-b border-border/30">
|
||||
<ScrollCarousel alwaysShowArrows {carousel} containerClass="py-2" innerClass="gap-2">
|
||||
{#each sections as section (section.title)}
|
||||
{#if getHref}
|
||||
<a
|
||||
class="flex cursor-pointer items-center gap-2 rounded-lg px-3 py-2 text-sm whitespace-nowrap no-underline transition-colors first:ml-4 last:mr-4 hover:bg-accent {isActive(
|
||||
section
|
||||
)
|
||||
? 'bg-accent text-accent-foreground'
|
||||
: 'text-muted-foreground'}"
|
||||
{...{ [UI_DATA_ATTRS.ACTIVE]: isActive(section) }}
|
||||
href={getHref(section)}
|
||||
onclick={(e: MouseEvent) => {
|
||||
carousel.scrollToCenter(e.currentTarget as HTMLElement);
|
||||
}}
|
||||
>
|
||||
<section.icon class="{ICON_CLASS_DEFAULT} flex-shrink-0" />
|
||||
<button
|
||||
class="flex cursor-pointer items-center gap-2 rounded-lg px-3 py-2 text-sm whitespace-nowrap transition-colors first:ml-4 last:mr-4 hover:bg-accent {isActive(
|
||||
section
|
||||
)
|
||||
? 'bg-accent text-accent-foreground'
|
||||
: 'text-muted-foreground'}"
|
||||
{...{ [UI_DATA_ATTRS.ACTIVE]: isActive(section) }}
|
||||
onclick={(e: MouseEvent) => {
|
||||
onSectionChange?.(section.title);
|
||||
carousel.scrollToCenter(e.currentTarget as HTMLElement);
|
||||
}}
|
||||
>
|
||||
<section.icon class="{ICON_CLASS_DEFAULT} flex-shrink-0" />
|
||||
|
||||
<span>{section.title}</span>
|
||||
</a>
|
||||
{:else}
|
||||
<button
|
||||
class="flex cursor-pointer items-center gap-2 rounded-lg px-3 py-2 text-sm whitespace-nowrap transition-colors first:ml-4 last:mr-4 hover:bg-accent {isActive(
|
||||
section
|
||||
)
|
||||
? 'bg-accent text-accent-foreground'
|
||||
: 'text-muted-foreground'}"
|
||||
{...{ [UI_DATA_ATTRS.ACTIVE]: isActive(section) }}
|
||||
onclick={(e: MouseEvent) => {
|
||||
onSectionChange?.(section.title);
|
||||
carousel.scrollToCenter(e.currentTarget as HTMLElement);
|
||||
}}
|
||||
>
|
||||
<section.icon class="{ICON_CLASS_DEFAULT} flex-shrink-0" />
|
||||
|
||||
<span>{section.title}</span>
|
||||
</button>
|
||||
{/if}
|
||||
<span>{section.title}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</ScrollCarousel>
|
||||
</div>
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="sticky bottom-0 mx-auto mt-4 flex w-full justify-between p-6">
|
||||
<div class="sticky bottom-0 mx-auto mt-4 flex w-full justify-between pb-4 md:pb-0">
|
||||
<div class="flex gap-2">
|
||||
<Button onclick={handleResetClick} variant="outline">
|
||||
<RotateCcw class="h-3 w-3" />
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
<script lang="ts">
|
||||
import McpLogo from '../mcp/McpLogo.svelte';
|
||||
import { Plus, X } from '@lucide/svelte';
|
||||
import { browser } from '$app/environment';
|
||||
import { goto, replaceState } from '$app/navigation';
|
||||
import { Plus } from '@lucide/svelte';
|
||||
import { replaceState } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import { ActionIcon, McpServerCard, McpServerCardSkeleton } from '$lib/components/app';
|
||||
import { DialogMcpServerAddNew } from '$lib/components/app/dialogs';
|
||||
import { McpServerCard, McpServerCardSkeleton } from '$lib/components/app';
|
||||
import { DialogMcpResourcesBrowser, DialogMcpServerAddNew } from '$lib/components/app/dialogs';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Empty from '$lib/components/ui/empty';
|
||||
import { ROUTES } from '$lib/constants';
|
||||
import { HealthCheckStatus } from '$lib/enums';
|
||||
import { conversationsStore, mcpStore, toolsStore } from '$lib/stores';
|
||||
import { onMount } from 'svelte';
|
||||
@@ -23,26 +20,7 @@
|
||||
let servers = $derived(mcpStore.getServers());
|
||||
|
||||
let isAddingServer = $state(false);
|
||||
|
||||
let previousRouteId = $state<string | null>(null);
|
||||
|
||||
$effect(() => {
|
||||
const currentId = page.route.id;
|
||||
|
||||
return () => {
|
||||
previousRouteId = currentId;
|
||||
};
|
||||
});
|
||||
|
||||
function handleClose() {
|
||||
const prevIsMcpServers = previousRouteId === '/mcp-servers';
|
||||
|
||||
if (browser && window.history.length > 1 && !prevIsMcpServers) {
|
||||
history.back();
|
||||
} else {
|
||||
goto(ROUTES.START);
|
||||
}
|
||||
}
|
||||
let isResourcesDialogOpen = $state(false);
|
||||
|
||||
onMount(() => {
|
||||
if (page.url.searchParams.has('add')) {
|
||||
@@ -71,25 +49,13 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div in:fade={{ duration: 150 }} class="flex min-h-[calc(100dvh-4rem)] flex-col">
|
||||
<div class="fixed top-4.5 right-4 z-50 md:hidden">
|
||||
<ActionIcon icon={X} onclick={handleClose} tooltip="Close" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="sticky top-0 z-10 mt-4 mb-2 flex items-start gap-4 md:p-4 p-0 px-4 md:justify-between md:px-8"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<McpLogo class="h-5 w-5 md:h-6 md:w-6" />
|
||||
|
||||
<h1 class="text-lg font-semibold md:text-2xl">MCP Servers</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div in:fade={{ duration: 150 }} class="flex flex-col h-full">
|
||||
<DialogMcpServerAddNew bind:open={isAddingServer} />
|
||||
|
||||
<DialogMcpResourcesBrowser bind:open={isResourcesDialogOpen} />
|
||||
|
||||
{#if servers.length === 0}
|
||||
<div class="flex flex-1 items-center justify-center py-16">
|
||||
<div class="flex flex-1 items-center justify-center pb-20 pt-10 my-auto">
|
||||
<Empty.Root class="max-w-md">
|
||||
<Empty.Header>
|
||||
<Empty.Media variant="icon">
|
||||
@@ -112,8 +78,8 @@
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class="grid gap-3 {className}"
|
||||
style="grid-template-columns: repeat(auto-fill, minmax(min(32rem, calc(100dvw - 2rem)), 1fr));"
|
||||
class="grid gap-4 {className}"
|
||||
style="grid-template-columns: repeat(auto-fill, minmax(min(25rem, calc(100dvw - 4rem)), 1fr));"
|
||||
>
|
||||
{#each servers as server (server.id)}
|
||||
{#if isServerPending(server.id, server.enabled)}
|
||||
@@ -121,6 +87,7 @@
|
||||
{:else}
|
||||
<McpServerCard
|
||||
enabled={conversationsStore.preferences.isMcpServerEnabledForChat(server.id)}
|
||||
onBrowseResources={() => (isResourcesDialogOpen = true)}
|
||||
onDelete={() => mcpStore.removeServer(server.id)}
|
||||
onToggle={async () => {
|
||||
const wasEnabled = conversationsStore.preferences.isMcpServerEnabledForChat(
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
/**
|
||||
* Full chat settings page layout with sidebar, mobile header, and content area.
|
||||
* Manages local configuration state, section navigation, and context setup.
|
||||
* Accepts an optional `initialSection` prop to override the URL-based section resolution.
|
||||
* Accepts an optional `initialSection` prop to set the initial active section.
|
||||
*/
|
||||
export { default as SettingsChat } from './SettingsChat/SettingsChat.svelte';
|
||||
|
||||
/**
|
||||
* Desktop sidebar navigation for chat settings.
|
||||
* Displays a list of settings sections with icons and titles.
|
||||
* Supports both hash-link navigation (via `getHref`) and in-app section switching (via `onSectionChange`).
|
||||
* Switches sections in-app via `onSectionChange`.
|
||||
*/
|
||||
export { default as SettingsChatDesktopSidebar } from './SettingsChatDesktopSidebar.svelte';
|
||||
|
||||
/**
|
||||
* Mobile header with a horizontally scrollable section picker for chat settings.
|
||||
* Shows chevron buttons for scroll navigation and highlights the active section.
|
||||
* Supports both hash-link navigation (via `getHref`) and in-app section switching (via `onSectionChange`).
|
||||
* Switches sections in-app via `onSectionChange`.
|
||||
*/
|
||||
export { default as SettingsChatMobileHeader } from './SettingsChatMobileHeader.svelte';
|
||||
|
||||
|
||||
@@ -5,3 +5,9 @@
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Close bind:ref data-slot="dialog-close" {...restProps} />
|
||||
|
||||
<style>
|
||||
:global([data-dialog-close]) {
|
||||
z-index: 999;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
class: className,
|
||||
portalProps,
|
||||
ref = $bindable(null),
|
||||
showCloseButton = true,
|
||||
showCloseButton = false,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<DialogPrimitive.ContentProps> & {
|
||||
portalProps?: DialogPrimitive.PortalProps;
|
||||
@@ -25,7 +25,8 @@
|
||||
<DialogPrimitive.Content
|
||||
bind:ref
|
||||
class={cn(
|
||||
`fixed top-[50%] left-[50%] z-50 grid max-h-[100dvh] w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 overflow-y-auto rounded-lg border border-border/30 bg-background p-6 shadow-lg duration-200 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:fill-mode-forwards data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:max-w-lg md:max-h-[100vh]`,
|
||||
`fixed top-[50%] left-[50%] z-50 grid translate-x-[-50%] translate-y-[-50%] gap-4 overflow-y-auto rounded-lg border border-border/30 bg-background p-6 shadow-lg duration-200 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:fill-mode-forwards data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95`,
|
||||
'max-h-[100dvh] max-w-full size-auto sm:max-w-lg md:w-auto md:size-auto md:max-w-[calc(100%-2rem)]',
|
||||
className
|
||||
)}
|
||||
data-slot="dialog-content"
|
||||
|
||||
@@ -1,20 +1,43 @@
|
||||
<script lang="ts">
|
||||
import XIcon from '@lucide/svelte/icons/x';
|
||||
import { cn, type WithElementRef } from '$lib/components/ui/utils';
|
||||
import { Dialog as DialogPrimitive } from 'bits-ui';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
children,
|
||||
class: className,
|
||||
ref = $bindable(null),
|
||||
showCloseButton = true,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
||||
showCloseButton?: boolean;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<!--
|
||||
Header is `sticky`, so it stays at the top while the dialog body scrolls. The close
|
||||
button lives here (not in the body) so it sticks together with the title. `sticky`
|
||||
makes it the containing block, so the close can be absolutely placed at its corner.
|
||||
-->
|
||||
<div
|
||||
bind:this={ref}
|
||||
class={cn('flex flex-col gap-2 text-center sm:text-left', className)}
|
||||
class={cn(
|
||||
'flex flex-col gap-2 text-center sm:text-left sticky top-0 z-50 bg-background md:bg-transparent',
|
||||
className
|
||||
)}
|
||||
data-slot="dialog-header"
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
|
||||
{#if showCloseButton}
|
||||
<DialogPrimitive.Close
|
||||
class="absolute top-0 right-0 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
||||
>
|
||||
<XIcon />
|
||||
|
||||
<span class="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -15,10 +15,6 @@ export const ROUTES = {
|
||||
MCP_SERVERS: '#/mcp-servers',
|
||||
/** Search — mobile-only full-page conversation search. */
|
||||
SEARCH: '#/search',
|
||||
/** Settings base — for dynamic settings URLs use RouterService. */
|
||||
SETTINGS: '#/settings',
|
||||
/** Exit destination for the settings view (fallback when no referrer). */
|
||||
SETTINGS_EXIT: '#/',
|
||||
/** Root — start of the app. */
|
||||
START: '#/'
|
||||
} as const;
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { ROUTES } from './routes.constants';
|
||||
import { Package, Search, Settings, SquarePen } from '@lucide/svelte';
|
||||
import McpLogo from '$lib/components/app/mcp/McpLogo.svelte';
|
||||
import { SidebarAction, ToolSource } from '$lib/enums';
|
||||
import type { DesktopIconStripItem } from '$lib/types';
|
||||
|
||||
@@ -64,15 +62,8 @@ export const SIDEBAR_ACTIONS_ITEMS: DesktopIconStripItem[] = [
|
||||
},
|
||||
{ icon: Search, keys: ['cmd', 'k'], tooltip: 'Search' },
|
||||
{
|
||||
activeRouteId: '/mcp-servers',
|
||||
icon: McpLogo,
|
||||
route: ROUTES.MCP_SERVERS,
|
||||
tooltip: 'MCP Servers'
|
||||
},
|
||||
{
|
||||
activeUrlIncludes: '#/settings',
|
||||
action: SidebarAction.SETTINGS,
|
||||
icon: Settings,
|
||||
route: `${ROUTES.SETTINGS}/general`,
|
||||
tooltip: 'Settings'
|
||||
}
|
||||
];
|
||||
|
||||
@@ -23,7 +23,8 @@ export enum ScrollCarouselVariant {
|
||||
* Sidebar icon strip actions handled directly by the sidebar.
|
||||
*/
|
||||
export enum SidebarAction {
|
||||
NEW_CHAT = 'new-chat'
|
||||
NEW_CHAT = 'new-chat',
|
||||
SETTINGS = 'settings'
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import { beforeNavigate } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import { ROUTES } from '$lib/constants';
|
||||
import { settingsReferrer } from '$lib/stores';
|
||||
|
||||
export interface ChatSettings {
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
export function useSettingsNavigation() {
|
||||
const subroute = $state({
|
||||
activePanel: 'chat' as 'chat' | 'settings' | 'mcp',
|
||||
chatSettingsRef: undefined as ChatSettings | undefined
|
||||
});
|
||||
const isSettingsRoute = $derived(!!page.route.id?.startsWith('/settings'));
|
||||
|
||||
beforeNavigate(({ from, to }) => {
|
||||
if (to?.route?.id?.startsWith('/settings') && !from?.route?.id?.startsWith('/settings')) {
|
||||
settingsReferrer.url = window.location.hash || ROUTES.START;
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (subroute.activePanel === 'settings' && subroute.chatSettingsRef) {
|
||||
subroute.chatSettingsRef.reset();
|
||||
}
|
||||
});
|
||||
|
||||
// Return to chat when navigating to a new route
|
||||
$effect(() => {
|
||||
void page.url;
|
||||
|
||||
subroute.activePanel = 'chat';
|
||||
});
|
||||
|
||||
return {
|
||||
get isSettingsRoute() {
|
||||
return isSettingsRoute;
|
||||
},
|
||||
|
||||
get panel() {
|
||||
return subroute;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -307,7 +307,6 @@ export { SandboxService } from './sandbox.service';
|
||||
*
|
||||
* **Key Responsibilities:**
|
||||
* - Build chat URLs for specific conversations: `RouterService.chat(id)` → `#/chat/:id`
|
||||
* - Build settings URLs for sections: `RouterService.settings(section)` → `#/settings/:section`
|
||||
*
|
||||
* @see ROUTES in constants/routes.ts — static route base paths
|
||||
*/
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
/**
|
||||
* RouterService - Builds app route paths
|
||||
*
|
||||
* Returns chat and settings route strings from a single source of truth
|
||||
* (ROUTES). No state.
|
||||
* Returns chat route strings from a single source of truth (ROUTES). No state.
|
||||
*/
|
||||
|
||||
import { ROUTES } from '$lib/constants';
|
||||
@@ -11,8 +10,4 @@ export class RouterService {
|
||||
static chat(id: string): string {
|
||||
return `${ROUTES.CHAT}/${id}`;
|
||||
}
|
||||
|
||||
static settings(section: string): string {
|
||||
return `${ROUTES.SETTINGS}/${section}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,8 +49,6 @@ export { uiStore } from './ui.svelte';
|
||||
// SETTINGS / UI PREFERENCES
|
||||
export { settingsStore } from './settings/index.svelte';
|
||||
|
||||
export { settingsReferrer } from './settings/referrer.svelte';
|
||||
|
||||
export { permissionsStore } from './permissions.svelte';
|
||||
|
||||
// TOOLS
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
/**
|
||||
* settingsReferrer - Remembers the settings route to return to after exit
|
||||
*
|
||||
* Tracks the last settings section the user was on so the app can return
|
||||
* there after a fallback exit. Standalone reactive value, no host.
|
||||
*/
|
||||
|
||||
import { ROUTES } from '$lib/constants';
|
||||
|
||||
let _url = $state<string>(ROUTES.SETTINGS_EXIT);
|
||||
|
||||
export const settingsReferrer = {
|
||||
get url() {
|
||||
return _url;
|
||||
},
|
||||
set url(value: string) {
|
||||
_url = value;
|
||||
}
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { SettingsMcpServers } from '$lib/components/app/settings';
|
||||
</script>
|
||||
|
||||
<SettingsMcpServers class="mx-auto w-full p-4 md:p-8 md:py-8" />
|
||||
@@ -1,38 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { X } from '@lucide/svelte';
|
||||
import { browser } from '$app/environment';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import { ActionIcon } from '$lib/components/app';
|
||||
import { ROUTES } from '$lib/constants';
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
let previousRouteId = $state<string | null>(null);
|
||||
|
||||
$effect(() => {
|
||||
const currentId = page.route.id;
|
||||
|
||||
return () => {
|
||||
previousRouteId = currentId;
|
||||
};
|
||||
});
|
||||
|
||||
function handleClose() {
|
||||
const prevIsSettings = previousRouteId?.startsWith('/settings');
|
||||
|
||||
if (browser && window.history.length > 1 && !prevIsSettings) {
|
||||
history.back();
|
||||
} else {
|
||||
goto(ROUTES.SETTINGS_EXIT);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="fixed top-4.5 right-4 z-50 md:hidden">
|
||||
<ActionIcon icon={X} onclick={handleClose} tooltip="Close" />
|
||||
</div>
|
||||
|
||||
<div class="min-h-full">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -1,15 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { afterNavigate, replaceState } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import { SettingsChat } from '$lib/components/app/settings';
|
||||
import { SETTINGS_SECTION_SLUGS } from '$lib/constants';
|
||||
import { RouterService } from '$lib/services';
|
||||
|
||||
afterNavigate(() => {
|
||||
if (!page.params.section) {
|
||||
replaceState(RouterService.settings(SETTINGS_SECTION_SLUGS.GENERAL), {});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<SettingsChat initialSection={(page.params as Record<string, string | undefined>).section} />
|
||||
Reference in New Issue
Block a user