diff --git a/.github/workflows/deploy_cloudflare.yml b/.github/workflows/deploy_cloudflare.yml index cb0dfc2187a06cf62255b049b7e5fe74b10c505a..37f23b20d2825e9f3d26c456903962a10c2d0081 100644 --- a/.github/workflows/deploy_cloudflare.yml +++ b/.github/workflows/deploy_cloudflare.yml @@ -26,6 +26,7 @@ jobs: CC: clang CXX: clang++ DOCS_AMPLITUDE_API_KEY: ${{ secrets.DOCS_AMPLITUDE_API_KEY }} + DOCS_CONSENT_IO_INSTANCE: ${{ secrets.DOCS_CONSENT_IO_INSTANCE }} - name: Deploy Docs uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 # v3 diff --git a/Cargo.lock b/Cargo.lock index 96caec077edd4bdf8c02a3e1ff1fc10340d2b9b0..b1ff28fcf52e118830e2100d35a6cdbca6f6f013 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -76,6 +76,7 @@ dependencies = [ "clock", "collections", "ctor", + "fs", "futures 0.3.31", "gpui", "indoc", @@ -1352,6 +1353,7 @@ version = "0.1.0" dependencies = [ "anyhow", "log", + "scopeguard", "simplelog", "tempfile", "windows 0.61.3", @@ -7597,7 +7599,7 @@ dependencies = [ "mach2 0.5.0", "media", "metal", - "naga", + "naga 28.0.0", "num_cpus", "objc", "objc2", @@ -10701,6 +10703,30 @@ name = "naga" version = "28.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "618f667225063219ddfc61251087db8a9aec3c3f0950c916b614e403486f1135" +dependencies = [ + "arrayvec", + "bit-set", + "bitflags 2.10.0", + "cfg-if", + "cfg_aliases 0.2.1", + "codespan-reporting 0.12.0", + "half", + "hashbrown 0.16.1", + "hexf-parse", + "indexmap", + "libm", + "log", + "num-traits", + "once_cell", + "rustc-hash 1.1.0", + "thiserror 2.0.17", + "unicode-ident", +] + +[[package]] +name = "naga" +version = "28.0.1" +source = "git+https://github.com/zed-industries/wgpu?rev=6e0c2546d99dad72ce6ffb5b04349e6a4ce96e6d#6e0c2546d99dad72ce6ffb5b04349e6a4ce96e6d" dependencies = [ "arrayvec", "bit-set", @@ -19825,6 +19851,7 @@ version = "0.1.0" dependencies = [ "anyhow", "client", + "cloud_api_types", "cloud_llm_client", "futures 0.3.31", "gpui", @@ -19889,9 +19916,8 @@ checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3" [[package]] name = "wgpu" -version = "28.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9cb534d5ffd109c7d1135f34cdae29e60eab94855a625dcfe1705f8bc7ad79f" +version = "28.0.1" +source = "git+https://github.com/zed-industries/wgpu?rev=6e0c2546d99dad72ce6ffb5b04349e6a4ce96e6d#6e0c2546d99dad72ce6ffb5b04349e6a4ce96e6d" dependencies = [ "arrayvec", "bitflags 2.10.0", @@ -19902,7 +19928,7 @@ dependencies = [ "hashbrown 0.16.1", "js-sys", "log", - "naga", + "naga 28.0.1", "parking_lot", "portable-atomic", "profiling", @@ -19919,9 +19945,8 @@ dependencies = [ [[package]] name = "wgpu-core" -version = "28.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bb4c8b5db5f00e56f1f08869d870a0dff7c8bc7ebc01091fec140b0cf0211a9" +version = "28.0.1" +source = "git+https://github.com/zed-industries/wgpu?rev=6e0c2546d99dad72ce6ffb5b04349e6a4ce96e6d#6e0c2546d99dad72ce6ffb5b04349e6a4ce96e6d" dependencies = [ "arrayvec", "bit-set", @@ -19933,7 +19958,7 @@ dependencies = [ "hashbrown 0.16.1", "indexmap", "log", - "naga", + "naga 28.0.1", "once_cell", "parking_lot", "portable-atomic", @@ -19951,36 +19976,32 @@ dependencies = [ [[package]] name = "wgpu-core-deps-apple" -version = "28.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87b7b696b918f337c486bf93142454080a32a37832ba8a31e4f48221890047da" +version = "28.0.1" +source = "git+https://github.com/zed-industries/wgpu?rev=6e0c2546d99dad72ce6ffb5b04349e6a4ce96e6d#6e0c2546d99dad72ce6ffb5b04349e6a4ce96e6d" dependencies = [ "wgpu-hal", ] [[package]] name = "wgpu-core-deps-emscripten" -version = "28.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34b251c331f84feac147de3c4aa3aa45112622a95dd7ee1b74384fa0458dbd79" +version = "28.0.1" +source = "git+https://github.com/zed-industries/wgpu?rev=6e0c2546d99dad72ce6ffb5b04349e6a4ce96e6d#6e0c2546d99dad72ce6ffb5b04349e6a4ce96e6d" dependencies = [ "wgpu-hal", ] [[package]] name = "wgpu-core-deps-windows-linux-android" -version = "28.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ca976e72b2c9964eb243e281f6ce7f14a514e409920920dcda12ae40febaae" +version = "28.0.1" +source = "git+https://github.com/zed-industries/wgpu?rev=6e0c2546d99dad72ce6ffb5b04349e6a4ce96e6d#6e0c2546d99dad72ce6ffb5b04349e6a4ce96e6d" dependencies = [ "wgpu-hal", ] [[package]] name = "wgpu-hal" -version = "28.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "293080d77fdd14d6b08a67c5487dfddbf874534bb7921526db56a7b75d7e3bef" +version = "28.0.1" +source = "git+https://github.com/zed-industries/wgpu?rev=6e0c2546d99dad72ce6ffb5b04349e6a4ce96e6d#6e0c2546d99dad72ce6ffb5b04349e6a4ce96e6d" dependencies = [ "android_system_properties", "arrayvec", @@ -20003,7 +20024,7 @@ dependencies = [ "libloading", "log", "metal", - "naga", + "naga 28.0.1", "ndk-sys", "objc", "once_cell", @@ -20026,9 +20047,8 @@ dependencies = [ [[package]] name = "wgpu-types" -version = "28.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e18308757e594ed2cd27dddbb16a139c42a683819d32a2e0b1b0167552f5840c" +version = "28.0.1" +source = "git+https://github.com/zed-industries/wgpu?rev=6e0c2546d99dad72ce6ffb5b04349e6a4ce96e6d#6e0c2546d99dad72ce6ffb5b04349e6a4ce96e6d" dependencies = [ "bitflags 2.10.0", "bytemuck", diff --git a/Cargo.toml b/Cargo.toml index 8e1312f032e19b2c2c189677f144f04dd7f4589c..15d39992804b5ed7ad99fadd46e350b1357b17d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -770,7 +770,7 @@ wax = "0.7" which = "6.0.0" wasm-bindgen = "0.2.113" web-time = "1.1.0" -wgpu = "28.0" +wgpu = { git = "https://github.com/zed-industries/wgpu", rev = "6e0c2546d99dad72ce6ffb5b04349e6a4ce96e6d" } windows-core = "0.61" yawc = "0.2.5" zeroize = "1.8" @@ -815,6 +815,7 @@ features = [ "Win32_System_Ole", "Win32_System_Performance", "Win32_System_Pipes", + "Win32_System_RestartManager", "Win32_System_SystemInformation", "Win32_System_SystemServices", "Win32_System_Threading", diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 9b8f2d337b1f1073bca818cf0b9c66773a3ce4e9..87e76829966b501df4139d4942de604c4fc42d65 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -204,6 +204,7 @@ { "context": "Editor && editor_agent_diff", "bindings": { + "alt-y": "agent::Keep", "ctrl-alt-y": "agent::Keep", "ctrl-alt-z": "agent::Reject", "shift-alt-y": "agent::KeepAll", @@ -214,6 +215,7 @@ { "context": "AgentDiff", "bindings": { + "alt-y": "agent::Keep", "ctrl-alt-y": "agent::Keep", "ctrl-alt-z": "agent::Reject", "shift-alt-y": "agent::KeepAll", diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 5f210cb4da35f9909767035c941289ee24a2ee3f..ccb3a7fa9116b0771dda94e75e467c4572cdaf2c 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -242,6 +242,7 @@ "context": "AgentDiff", "use_key_equivalents": true, "bindings": { + "cmd-y": "agent::Keep", "cmd-alt-y": "agent::Keep", "cmd-alt-z": "agent::Reject", "shift-alt-y": "agent::KeepAll", @@ -252,6 +253,7 @@ "context": "Editor && editor_agent_diff", "use_key_equivalents": true, "bindings": { + "cmd-y": "agent::Keep", "cmd-alt-y": "agent::Keep", "cmd-alt-z": "agent::Reject", "shift-alt-y": "agent::KeepAll", @@ -448,6 +450,13 @@ "down": "search::NextHistoryQuery", }, }, + { + "context": "BufferSearchBar || ProjectSearchBar", + "use_key_equivalents": true, + "bindings": { + "ctrl-enter": "editor::Newline", + }, + }, { "context": "ProjectSearchBar", "use_key_equivalents": true, diff --git a/assets/keymaps/default-windows.json b/assets/keymaps/default-windows.json index 19f75f858cd45192c4cf30dd6bd0799046c26268..251c3d6541a611737027900e659a94271ed36526 100644 --- a/assets/keymaps/default-windows.json +++ b/assets/keymaps/default-windows.json @@ -203,6 +203,7 @@ "context": "Editor && editor_agent_diff", "use_key_equivalents": true, "bindings": { + "alt-y": "agent::Keep", "ctrl-alt-y": "agent::Keep", "ctrl-alt-z": "agent::Reject", "shift-alt-y": "agent::KeepAll", @@ -214,6 +215,7 @@ "context": "AgentDiff", "use_key_equivalents": true, "bindings": { + "alt-y": "agent::Keep", "ctrl-alt-y": "agent::Keep", "ctrl-alt-z": "agent::Reject", "shift-alt-y": "agent::KeepAll", diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index f57ce1f4d188e260624bd90187a21890379fe6b6..1b9271918884dc020986577926d9578e3a6f049c 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -972,6 +972,8 @@ pub struct AcpThread { had_error: bool, /// The user's unsent prompt text, persisted so it can be restored when reloading the thread. draft_prompt: Option>, + /// The initial scroll position for the thread view, set during session registration. + ui_scroll_position: Option, } impl From<&AcpThread> for ActionLogTelemetry { @@ -1210,6 +1212,7 @@ impl AcpThread { pending_terminal_exit: HashMap::default(), had_error: false, draft_prompt: None, + ui_scroll_position: None, } } @@ -1229,6 +1232,14 @@ impl AcpThread { self.draft_prompt = prompt; } + pub fn ui_scroll_position(&self) -> Option { + self.ui_scroll_position + } + + pub fn set_ui_scroll_position(&mut self, position: Option) { + self.ui_scroll_position = position; + } + pub fn connection(&self) -> &Rc { &self.connection } diff --git a/crates/action_log/Cargo.toml b/crates/action_log/Cargo.toml index 8488df691e40ea3bcfc04f4f6f74964fba7863dd..b1a1bf824fb770b8378e596fd0c799a7cf98b13d 100644 --- a/crates/action_log/Cargo.toml +++ b/crates/action_log/Cargo.toml @@ -20,6 +20,7 @@ buffer_diff.workspace = true log.workspace = true clock.workspace = true collections.workspace = true +fs.workspace = true futures.workspace = true gpui.workspace = true language.workspace = true diff --git a/crates/action_log/src/action_log.rs b/crates/action_log/src/action_log.rs index 5f8a639c0559c10546fc5640dc240aeba9dde487..5679f3c58fe52057f7a4a0faa24d5b5db2b5e497 100644 --- a/crates/action_log/src/action_log.rs +++ b/crates/action_log/src/action_log.rs @@ -1,14 +1,20 @@ use anyhow::{Context as _, Result}; use buffer_diff::BufferDiff; use clock; -use collections::BTreeMap; +use collections::{BTreeMap, HashMap}; +use fs::MTime; use futures::{FutureExt, StreamExt, channel::mpsc}; use gpui::{ App, AppContext, AsyncApp, Context, Entity, SharedString, Subscription, Task, WeakEntity, }; use language::{Anchor, Buffer, BufferEvent, Point, ToOffset, ToPoint}; use project::{Project, ProjectItem, lsp_store::OpenLspBufferHandle}; -use std::{cmp, ops::Range, sync::Arc}; +use std::{ + cmp, + ops::Range, + path::{Path, PathBuf}, + sync::Arc, +}; use text::{Edit, Patch, Rope}; use util::{RangeExt, ResultExt as _}; @@ -54,6 +60,8 @@ pub struct ActionLog { linked_action_log: Option>, /// Stores undo information for the most recent reject operation last_reject_undo: Option, + /// Tracks the last time files were read by the agent, to detect external modifications + file_read_times: HashMap, } impl ActionLog { @@ -64,6 +72,7 @@ impl ActionLog { project, linked_action_log: None, last_reject_undo: None, + file_read_times: HashMap::default(), } } @@ -76,6 +85,32 @@ impl ActionLog { &self.project } + pub fn file_read_time(&self, path: &Path) -> Option { + self.file_read_times.get(path).copied() + } + + fn update_file_read_time(&mut self, buffer: &Entity, cx: &App) { + let buffer = buffer.read(cx); + if let Some(file) = buffer.file() { + if let Some(local_file) = file.as_local() { + if let Some(mtime) = file.disk_state().mtime() { + let abs_path = local_file.abs_path(cx); + self.file_read_times.insert(abs_path, mtime); + } + } + } + } + + fn remove_file_read_time(&mut self, buffer: &Entity, cx: &App) { + let buffer = buffer.read(cx); + if let Some(file) = buffer.file() { + if let Some(local_file) = file.as_local() { + let abs_path = local_file.abs_path(cx); + self.file_read_times.remove(&abs_path); + } + } + } + fn track_buffer_internal( &mut self, buffer: Entity, @@ -506,24 +541,69 @@ impl ActionLog { /// Track a buffer as read by agent, so we can notify the model about user edits. pub fn buffer_read(&mut self, buffer: Entity, cx: &mut Context) { - if let Some(linked_action_log) = &mut self.linked_action_log { - linked_action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx)); + self.buffer_read_impl(buffer, true, cx); + } + + fn buffer_read_impl( + &mut self, + buffer: Entity, + record_file_read_time: bool, + cx: &mut Context, + ) { + if let Some(linked_action_log) = &self.linked_action_log { + // We don't want to share read times since the other agent hasn't read it necessarily + linked_action_log.update(cx, |log, cx| { + log.buffer_read_impl(buffer.clone(), false, cx); + }); + } + if record_file_read_time { + self.update_file_read_time(&buffer, cx); } self.track_buffer_internal(buffer, false, cx); } /// Mark a buffer as created by agent, so we can refresh it in the context pub fn buffer_created(&mut self, buffer: Entity, cx: &mut Context) { - if let Some(linked_action_log) = &mut self.linked_action_log { - linked_action_log.update(cx, |log, cx| log.buffer_created(buffer.clone(), cx)); + self.buffer_created_impl(buffer, true, cx); + } + + fn buffer_created_impl( + &mut self, + buffer: Entity, + record_file_read_time: bool, + cx: &mut Context, + ) { + if let Some(linked_action_log) = &self.linked_action_log { + // We don't want to share read times since the other agent hasn't read it necessarily + linked_action_log.update(cx, |log, cx| { + log.buffer_created_impl(buffer.clone(), false, cx); + }); + } + if record_file_read_time { + self.update_file_read_time(&buffer, cx); } self.track_buffer_internal(buffer, true, cx); } /// Mark a buffer as edited by agent, so we can refresh it in the context pub fn buffer_edited(&mut self, buffer: Entity, cx: &mut Context) { - if let Some(linked_action_log) = &mut self.linked_action_log { - linked_action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); + self.buffer_edited_impl(buffer, true, cx); + } + + fn buffer_edited_impl( + &mut self, + buffer: Entity, + record_file_read_time: bool, + cx: &mut Context, + ) { + if let Some(linked_action_log) = &self.linked_action_log { + // We don't want to share read times since the other agent hasn't read it necessarily + linked_action_log.update(cx, |log, cx| { + log.buffer_edited_impl(buffer.clone(), false, cx); + }); + } + if record_file_read_time { + self.update_file_read_time(&buffer, cx); } let new_version = buffer.read(cx).version(); let tracked_buffer = self.track_buffer_internal(buffer, false, cx); @@ -536,6 +616,8 @@ impl ActionLog { } pub fn will_delete_buffer(&mut self, buffer: Entity, cx: &mut Context) { + // Ok to propagate file read time removal to linked action log + self.remove_file_read_time(&buffer, cx); let has_linked_action_log = self.linked_action_log.is_some(); let tracked_buffer = self.track_buffer_internal(buffer.clone(), false, cx); match tracked_buffer.status { @@ -2976,6 +3058,196 @@ mod tests { ); } + #[gpui::test] + async fn test_file_read_time_recorded_on_buffer_read(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree(path!("/dir"), json!({"file": "hello world"})) + .await; + let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; + let action_log = cx.new(|_| ActionLog::new(project.clone())); + + let file_path = project + .read_with(cx, |project, cx| project.find_project_path("dir/file", cx)) + .unwrap(); + let buffer = project + .update(cx, |project, cx| project.open_buffer(file_path, cx)) + .await + .unwrap(); + + let abs_path = PathBuf::from(path!("/dir/file")); + assert!( + action_log.read_with(cx, |log, _| log.file_read_time(&abs_path).is_none()), + "file_read_time should be None before buffer_read" + ); + + cx.update(|cx| { + action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx)); + }); + + assert!( + action_log.read_with(cx, |log, _| log.file_read_time(&abs_path).is_some()), + "file_read_time should be recorded after buffer_read" + ); + } + + #[gpui::test] + async fn test_file_read_time_recorded_on_buffer_edited(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree(path!("/dir"), json!({"file": "hello world"})) + .await; + let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; + let action_log = cx.new(|_| ActionLog::new(project.clone())); + + let file_path = project + .read_with(cx, |project, cx| project.find_project_path("dir/file", cx)) + .unwrap(); + let buffer = project + .update(cx, |project, cx| project.open_buffer(file_path, cx)) + .await + .unwrap(); + + let abs_path = PathBuf::from(path!("/dir/file")); + assert!( + action_log.read_with(cx, |log, _| log.file_read_time(&abs_path).is_none()), + "file_read_time should be None before buffer_edited" + ); + + cx.update(|cx| { + action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); + }); + + assert!( + action_log.read_with(cx, |log, _| log.file_read_time(&abs_path).is_some()), + "file_read_time should be recorded after buffer_edited" + ); + } + + #[gpui::test] + async fn test_file_read_time_recorded_on_buffer_created(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree(path!("/dir"), json!({"file": "existing content"})) + .await; + let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; + let action_log = cx.new(|_| ActionLog::new(project.clone())); + + let file_path = project + .read_with(cx, |project, cx| project.find_project_path("dir/file", cx)) + .unwrap(); + let buffer = project + .update(cx, |project, cx| project.open_buffer(file_path, cx)) + .await + .unwrap(); + + let abs_path = PathBuf::from(path!("/dir/file")); + assert!( + action_log.read_with(cx, |log, _| log.file_read_time(&abs_path).is_none()), + "file_read_time should be None before buffer_created" + ); + + cx.update(|cx| { + action_log.update(cx, |log, cx| log.buffer_created(buffer.clone(), cx)); + }); + + assert!( + action_log.read_with(cx, |log, _| log.file_read_time(&abs_path).is_some()), + "file_read_time should be recorded after buffer_created" + ); + } + + #[gpui::test] + async fn test_file_read_time_removed_on_delete(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree(path!("/dir"), json!({"file": "hello world"})) + .await; + let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; + let action_log = cx.new(|_| ActionLog::new(project.clone())); + + let file_path = project + .read_with(cx, |project, cx| project.find_project_path("dir/file", cx)) + .unwrap(); + let buffer = project + .update(cx, |project, cx| project.open_buffer(file_path, cx)) + .await + .unwrap(); + + let abs_path = PathBuf::from(path!("/dir/file")); + + cx.update(|cx| { + action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx)); + }); + assert!( + action_log.read_with(cx, |log, _| log.file_read_time(&abs_path).is_some()), + "file_read_time should exist after buffer_read" + ); + + cx.update(|cx| { + action_log.update(cx, |log, cx| log.will_delete_buffer(buffer.clone(), cx)); + }); + assert!( + action_log.read_with(cx, |log, _| log.file_read_time(&abs_path).is_none()), + "file_read_time should be removed after will_delete_buffer" + ); + } + + #[gpui::test] + async fn test_file_read_time_not_forwarded_to_linked_action_log(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree(path!("/dir"), json!({"file": "hello world"})) + .await; + let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; + let parent_log = cx.new(|_| ActionLog::new(project.clone())); + let child_log = + cx.new(|_| ActionLog::new(project.clone()).with_linked_action_log(parent_log.clone())); + + let file_path = project + .read_with(cx, |project, cx| project.find_project_path("dir/file", cx)) + .unwrap(); + let buffer = project + .update(cx, |project, cx| project.open_buffer(file_path, cx)) + .await + .unwrap(); + + let abs_path = PathBuf::from(path!("/dir/file")); + + cx.update(|cx| { + child_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx)); + }); + assert!( + child_log.read_with(cx, |log, _| log.file_read_time(&abs_path).is_some()), + "child should record file_read_time on buffer_read" + ); + assert!( + parent_log.read_with(cx, |log, _| log.file_read_time(&abs_path).is_none()), + "parent should NOT get file_read_time from child's buffer_read" + ); + + cx.update(|cx| { + child_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); + }); + assert!( + parent_log.read_with(cx, |log, _| log.file_read_time(&abs_path).is_none()), + "parent should NOT get file_read_time from child's buffer_edited" + ); + + cx.update(|cx| { + child_log.update(cx, |log, cx| log.buffer_created(buffer.clone(), cx)); + }); + assert!( + parent_log.read_with(cx, |log, _| log.file_read_time(&abs_path).is_none()), + "parent should NOT get file_read_time from child's buffer_created" + ); + } + #[derive(Debug, PartialEq)] struct HunkStatus { range: Range, diff --git a/crates/agent/src/agent.rs b/crates/agent/src/agent.rs index 7cf9416840a6bd2870327c9c68135857c01f7c9b..5421538ca736028a4ea7290c09ef81036e055b81 100644 --- a/crates/agent/src/agent.rs +++ b/crates/agent/src/agent.rs @@ -352,6 +352,8 @@ impl NativeAgent { let parent_session_id = thread.parent_thread_id(); let title = thread.title(); let draft_prompt = thread.draft_prompt().map(Vec::from); + let scroll_position = thread.ui_scroll_position(); + let token_usage = thread.latest_token_usage(); let project = thread.project.clone(); let action_log = thread.action_log.clone(); let prompt_capabilities_rx = thread.prompt_capabilities_rx.clone(); @@ -367,6 +369,8 @@ impl NativeAgent { cx, ); acp_thread.set_draft_prompt(draft_prompt); + acp_thread.set_ui_scroll_position(scroll_position); + acp_thread.update_token_usage(token_usage, cx); acp_thread }); @@ -1917,7 +1921,9 @@ mod internal_tests { use gpui::TestAppContext; use indoc::formatdoc; use language_model::fake_provider::{FakeLanguageModel, FakeLanguageModelProvider}; - use language_model::{LanguageModelProviderId, LanguageModelProviderName}; + use language_model::{ + LanguageModelCompletionEvent, LanguageModelProviderId, LanguageModelProviderName, + }; use serde_json::json; use settings::SettingsStore; use util::{path, rel_path::rel_path}; @@ -2549,6 +2555,13 @@ mod internal_tests { cx.run_until_parked(); model.send_last_completion_stream_text_chunk("Lorem."); + model.send_last_completion_stream_event(LanguageModelCompletionEvent::UsageUpdate( + language_model::TokenUsage { + input_tokens: 150, + output_tokens: 75, + ..Default::default() + }, + )); model.end_last_completion_stream(); cx.run_until_parked(); summary_model @@ -2587,6 +2600,12 @@ mod internal_tests { acp_thread.update(cx, |thread, _cx| { thread.set_draft_prompt(Some(draft_blocks.clone())); }); + thread.update(cx, |thread, _cx| { + thread.set_ui_scroll_position(Some(gpui::ListOffset { + item_ix: 5, + offset_in_item: gpui::px(12.5), + })); + }); thread.update(cx, |_thread, cx| cx.notify()); cx.run_until_parked(); @@ -2632,6 +2651,24 @@ mod internal_tests { acp_thread.read_with(cx, |thread, _| { assert_eq!(thread.draft_prompt(), Some(draft_blocks.as_slice())); }); + + // Ensure token usage survived the round-trip. + acp_thread.read_with(cx, |thread, _| { + let usage = thread + .token_usage() + .expect("token usage should be restored after reload"); + assert_eq!(usage.input_tokens, 150); + assert_eq!(usage.output_tokens, 75); + }); + + // Ensure scroll position survived the round-trip. + acp_thread.read_with(cx, |thread, _| { + let scroll = thread + .ui_scroll_position() + .expect("scroll position should be restored after reload"); + assert_eq!(scroll.item_ix, 5); + assert_eq!(scroll.offset_in_item, gpui::px(12.5)); + }); } fn thread_entries( diff --git a/crates/agent/src/db.rs b/crates/agent/src/db.rs index 3a7af37cac85065d8853fbb5332093ef3fd20592..10ecb643b9a17dd6b02b47a416c526a662d12632 100644 --- a/crates/agent/src/db.rs +++ b/crates/agent/src/db.rs @@ -66,6 +66,14 @@ pub struct DbThread { pub thinking_effort: Option, #[serde(default)] pub draft_prompt: Option>, + #[serde(default)] + pub ui_scroll_position: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub struct SerializedScrollPosition { + pub item_ix: usize, + pub offset_in_item: f32, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -108,6 +116,7 @@ impl SharedThread { thinking_enabled: false, thinking_effort: None, draft_prompt: None, + ui_scroll_position: None, } } @@ -286,6 +295,7 @@ impl DbThread { thinking_enabled: false, thinking_effort: None, draft_prompt: None, + ui_scroll_position: None, }) } } @@ -637,6 +647,7 @@ mod tests { thinking_enabled: false, thinking_effort: None, draft_prompt: None, + ui_scroll_position: None, } } @@ -841,4 +852,53 @@ mod tests { assert_eq!(threads.len(), 1); assert!(threads[0].folder_paths.is_empty()); } + + #[test] + fn test_scroll_position_defaults_to_none() { + let json = r#"{ + "title": "Old Thread", + "messages": [], + "updated_at": "2024-01-01T00:00:00Z" + }"#; + + let db_thread: DbThread = serde_json::from_str(json).expect("Failed to deserialize"); + + assert!( + db_thread.ui_scroll_position.is_none(), + "Legacy threads without scroll_position field should default to None" + ); + } + + #[gpui::test] + async fn test_scroll_position_roundtrips_through_save_load(cx: &mut TestAppContext) { + let database = ThreadsDatabase::new(cx.executor()).unwrap(); + + let thread_id = session_id("thread-with-scroll"); + + let mut thread = make_thread( + "Thread With Scroll", + Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(), + ); + thread.ui_scroll_position = Some(SerializedScrollPosition { + item_ix: 42, + offset_in_item: 13.5, + }); + + database + .save_thread(thread_id.clone(), thread, PathList::default()) + .await + .unwrap(); + + let loaded = database + .load_thread(thread_id) + .await + .unwrap() + .expect("thread should exist"); + + let scroll = loaded + .ui_scroll_position + .expect("scroll_position should be restored"); + assert_eq!(scroll.item_ix, 42); + assert!((scroll.offset_in_item - 13.5).abs() < f32::EPSILON); + } } diff --git a/crates/agent/src/tests/edit_file_thread_test.rs b/crates/agent/src/tests/edit_file_thread_test.rs index 069bf0349299e6f4952f673cbf7607e52d48d9c5..3beb5cb0d51abc55fbf3cf0849ced248a9d1fa5c 100644 --- a/crates/agent/src/tests/edit_file_thread_test.rs +++ b/crates/agent/src/tests/edit_file_thread_test.rs @@ -50,9 +50,9 @@ async fn test_edit_file_tool_in_thread_context(cx: &mut TestAppContext) { // Add just the tools we need for this test let language_registry = project.read(cx).languages().clone(); thread.add_tool(crate::ReadFileTool::new( - cx.weak_entity(), project.clone(), thread.action_log().clone(), + true, )); thread.add_tool(crate::EditFileTool::new( project.clone(), diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index 2e693a85cd1f86d232e392860d8bd83509ce131a..616ae414d4d51a384a18460e8339fd07770fa6b9 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -893,14 +893,13 @@ pub struct Thread { pub(crate) prompt_capabilities_rx: watch::Receiver, pub(crate) project: Entity, pub(crate) action_log: Entity, - /// Tracks the last time files were read by the agent, to detect external modifications - pub(crate) file_read_times: HashMap, /// True if this thread was imported from a shared thread and can be synced. imported: bool, /// If this is a subagent thread, contains context about the parent subagent_context: Option, /// The user's unsent prompt text, persisted so it can be restored when reloading the thread. draft_prompt: Option>, + ui_scroll_position: Option, /// Weak references to running subagent threads for cancellation propagation running_subagents: Vec>, } @@ -1013,10 +1012,10 @@ impl Thread { prompt_capabilities_rx, project, action_log, - file_read_times: HashMap::default(), imported: false, subagent_context: None, draft_prompt: None, + ui_scroll_position: None, running_subagents: Vec::new(), } } @@ -1229,10 +1228,13 @@ impl Thread { updated_at: db_thread.updated_at, prompt_capabilities_tx, prompt_capabilities_rx, - file_read_times: HashMap::default(), imported: db_thread.imported, subagent_context: db_thread.subagent_context, draft_prompt: db_thread.draft_prompt, + ui_scroll_position: db_thread.ui_scroll_position.map(|sp| gpui::ListOffset { + item_ix: sp.item_ix, + offset_in_item: gpui::px(sp.offset_in_item), + }), running_subagents: Vec::new(), } } @@ -1258,6 +1260,12 @@ impl Thread { thinking_enabled: self.thinking_enabled, thinking_effort: self.thinking_effort.clone(), draft_prompt: self.draft_prompt.clone(), + ui_scroll_position: self.ui_scroll_position.map(|lo| { + crate::db::SerializedScrollPosition { + item_ix: lo.item_ix, + offset_in_item: lo.offset_in_item.as_f32(), + } + }), }; cx.background_spawn(async move { @@ -1307,6 +1315,14 @@ impl Thread { self.draft_prompt = prompt; } + pub fn ui_scroll_position(&self) -> Option { + self.ui_scroll_position + } + + pub fn set_ui_scroll_position(&mut self, position: Option) { + self.ui_scroll_position = position; + } + pub fn model(&self) -> Option<&Arc> { self.model.as_ref() } @@ -1416,6 +1432,9 @@ impl Thread { environment: Rc, cx: &mut Context, ) { + // Only update the agent location for the root thread, not for subagents. + let update_agent_location = self.parent_thread_id().is_none(); + let language_registry = self.project.read(cx).languages().clone(); self.add_tool(CopyPathTool::new(self.project.clone())); self.add_tool(CreateDirectoryTool::new(self.project.clone())); @@ -1443,9 +1462,9 @@ impl Thread { self.add_tool(NowTool); self.add_tool(OpenTool::new(self.project.clone())); self.add_tool(ReadFileTool::new( - cx.weak_entity(), self.project.clone(), self.action_log.clone(), + update_agent_location, )); self.add_tool(SaveFileTool::new(self.project.clone())); self.add_tool(RestoreFileFromDiskTool::new(self.project.clone())); @@ -2617,7 +2636,8 @@ impl Thread { } } - let use_streaming_edit_tool = cx.has_flag::(); + let use_streaming_edit_tool = + cx.has_flag::() && model.supports_streaming_tools(); let mut tools = self .tools diff --git a/crates/agent/src/thread_store.rs b/crates/agent/src/thread_store.rs index f944377e489a88ac0fa6dbb802edf9702e86f5f2..e26820ddacc3132d42946de3b27d25f4424fae02 100644 --- a/crates/agent/src/thread_store.rs +++ b/crates/agent/src/thread_store.rs @@ -146,6 +146,7 @@ mod tests { thinking_enabled: false, thinking_effort: None, draft_prompt: None, + ui_scroll_position: None, } } diff --git a/crates/agent/src/tools/edit_file_tool.rs b/crates/agent/src/tools/edit_file_tool.rs index d8c380eba326d089b848563cca04557e903ba0f4..29b08ac09db4417123403fd3915b8575791b2a4e 100644 --- a/crates/agent/src/tools/edit_file_tool.rs +++ b/crates/agent/src/tools/edit_file_tool.rs @@ -305,13 +305,13 @@ impl AgentTool for EditFileTool { // Check if the file has been modified since the agent last read it if let Some(abs_path) = abs_path.as_ref() { - let (last_read_mtime, current_mtime, is_dirty, has_save_tool, has_restore_tool) = self.thread.update(cx, |thread, cx| { - let last_read = thread.file_read_times.get(abs_path).copied(); + let last_read_mtime = action_log.read_with(cx, |log, _| log.file_read_time(abs_path)); + let (current_mtime, is_dirty, has_save_tool, has_restore_tool) = self.thread.read_with(cx, |thread, cx| { let current = buffer.read(cx).file().and_then(|file| file.disk_state().mtime()); let dirty = buffer.read(cx).is_dirty(); let has_save = thread.has_tool(SaveFileTool::NAME); let has_restore = thread.has_tool(RestoreFileFromDiskTool::NAME); - (last_read, current, dirty, has_save, has_restore) + (current, dirty, has_save, has_restore) })?; // Check for unsaved changes first - these indicate modifications we don't know about @@ -470,17 +470,6 @@ impl AgentTool for EditFileTool { log.buffer_edited(buffer.clone(), cx); }); - // Update the recorded read time after a successful edit so consecutive edits work - if let Some(abs_path) = abs_path.as_ref() { - if let Some(new_mtime) = buffer.read_with(cx, |buffer, _| { - buffer.file().and_then(|file| file.disk_state().mtime()) - }) { - self.thread.update(cx, |thread, _| { - thread.file_read_times.insert(abs_path.to_path_buf(), new_mtime); - })?; - } - } - let new_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot()); let (new_text, unified_diff) = cx .background_spawn({ @@ -2212,14 +2201,18 @@ mod tests { let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone()); // Initially, file_read_times should be empty - let is_empty = thread.read_with(cx, |thread, _| thread.file_read_times.is_empty()); + let is_empty = action_log.read_with(cx, |action_log, _| { + action_log + .file_read_time(path!("/root/test.txt").as_ref()) + .is_none() + }); assert!(is_empty, "file_read_times should start empty"); // Create read tool let read_tool = Arc::new(crate::ReadFileTool::new( - thread.downgrade(), project.clone(), - action_log, + action_log.clone(), + true, )); // Read the file to record the read time @@ -2238,12 +2231,9 @@ mod tests { .unwrap(); // Verify that file_read_times now contains an entry for the file - let has_entry = thread.read_with(cx, |thread, _| { - thread.file_read_times.len() == 1 - && thread - .file_read_times - .keys() - .any(|path| path.ends_with("test.txt")) + let has_entry = action_log.read_with(cx, |log, _| { + log.file_read_time(path!("/root/test.txt").as_ref()) + .is_some() }); assert!( has_entry, @@ -2265,11 +2255,14 @@ mod tests { .await .unwrap(); - // Should still have exactly one entry - let has_one_entry = thread.read_with(cx, |thread, _| thread.file_read_times.len() == 1); + // Should still have an entry after re-reading + let has_entry = action_log.read_with(cx, |log, _| { + log.file_read_time(path!("/root/test.txt").as_ref()) + .is_some() + }); assert!( - has_one_entry, - "file_read_times should still have one entry after re-reading" + has_entry, + "file_read_times should still have an entry after re-reading" ); } @@ -2309,11 +2302,7 @@ mod tests { let languages = project.read_with(cx, |project, _| project.languages().clone()); let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone()); - let read_tool = Arc::new(crate::ReadFileTool::new( - thread.downgrade(), - project.clone(), - action_log, - )); + let read_tool = Arc::new(crate::ReadFileTool::new(project.clone(), action_log, true)); let edit_tool = Arc::new(EditFileTool::new( project.clone(), thread.downgrade(), @@ -2423,11 +2412,7 @@ mod tests { let languages = project.read_with(cx, |project, _| project.languages().clone()); let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone()); - let read_tool = Arc::new(crate::ReadFileTool::new( - thread.downgrade(), - project.clone(), - action_log, - )); + let read_tool = Arc::new(crate::ReadFileTool::new(project.clone(), action_log, true)); let edit_tool = Arc::new(EditFileTool::new( project.clone(), thread.downgrade(), @@ -2534,11 +2519,7 @@ mod tests { let languages = project.read_with(cx, |project, _| project.languages().clone()); let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone()); - let read_tool = Arc::new(crate::ReadFileTool::new( - thread.downgrade(), - project.clone(), - action_log, - )); + let read_tool = Arc::new(crate::ReadFileTool::new(project.clone(), action_log, true)); let edit_tool = Arc::new(EditFileTool::new( project.clone(), thread.downgrade(), diff --git a/crates/agent/src/tools/read_file_tool.rs b/crates/agent/src/tools/read_file_tool.rs index 8cfc16ddf6174a190ffe7cc11921dc204b05b79d..f7a75bc63a1c461b65c3a2e6f74f2c70e0ca15f6 100644 --- a/crates/agent/src/tools/read_file_tool.rs +++ b/crates/agent/src/tools/read_file_tool.rs @@ -2,7 +2,7 @@ use action_log::ActionLog; use agent_client_protocol::{self as acp, ToolCallUpdateFields}; use anyhow::{Context as _, Result, anyhow}; use futures::FutureExt as _; -use gpui::{App, Entity, SharedString, Task, WeakEntity}; +use gpui::{App, Entity, SharedString, Task}; use indoc::formatdoc; use language::Point; use language_model::{LanguageModelImage, LanguageModelToolResultContent}; @@ -21,7 +21,7 @@ use super::tool_permissions::{ ResolvedProjectPath, authorize_symlink_access, canonicalize_worktree_roots, resolve_project_path, }; -use crate::{AgentTool, Thread, ToolCallEventStream, ToolInput, outline}; +use crate::{AgentTool, ToolCallEventStream, ToolInput, outline}; /// Reads the content of the given file in the project. /// @@ -56,21 +56,21 @@ pub struct ReadFileToolInput { } pub struct ReadFileTool { - thread: WeakEntity, project: Entity, action_log: Entity, + update_agent_location: bool, } impl ReadFileTool { pub fn new( - thread: WeakEntity, project: Entity, action_log: Entity, + update_agent_location: bool, ) -> Self { Self { - thread, project, action_log, + update_agent_location, } } } @@ -119,7 +119,6 @@ impl AgentTool for ReadFileTool { cx: &mut App, ) -> Task> { let project = self.project.clone(); - let thread = self.thread.clone(); let action_log = self.action_log.clone(); cx.spawn(async move |cx| { let input = input @@ -257,20 +256,6 @@ impl AgentTool for ReadFileTool { return Err(tool_content_err(format!("{file_path} not found"))); } - // Record the file read time and mtime - if let Some(mtime) = buffer.read_with(cx, |buffer, _| { - buffer.file().and_then(|file| file.disk_state().mtime()) - }) { - thread - .update(cx, |thread, _| { - thread.file_read_times.insert(abs_path.to_path_buf(), mtime); - }) - .ok(); - } - - - let update_agent_location = self.thread.read_with(cx, |thread, _cx| !thread.is_subagent()).unwrap_or_default(); - let mut anchor = None; // Check if specific line ranges are provided @@ -330,7 +315,7 @@ impl AgentTool for ReadFileTool { }; project.update(cx, |project, cx| { - if update_agent_location { + if self.update_agent_location { project.set_agent_location( Some(AgentLocation { buffer: buffer.downgrade(), @@ -362,13 +347,10 @@ impl AgentTool for ReadFileTool { #[cfg(test)] mod test { use super::*; - use crate::{ContextServerRegistry, Templates, Thread}; use agent_client_protocol as acp; use fs::Fs as _; use gpui::{AppContext, TestAppContext, UpdateGlobal as _}; - use language_model::fake_provider::FakeLanguageModel; use project::{FakeFs, Project}; - use prompt_store::ProjectContext; use serde_json::json; use settings::SettingsStore; use std::path::PathBuf; @@ -383,20 +365,7 @@ mod test { fs.insert_tree(path!("/root"), json!({})).await; let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; let action_log = cx.new(|_| ActionLog::new(project.clone())); - let context_server_registry = - cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); - let model = Arc::new(FakeLanguageModel::default()); - let thread = cx.new(|cx| { - Thread::new( - project.clone(), - cx.new(|_cx| ProjectContext::default()), - context_server_registry, - Templates::new(), - Some(model), - cx, - ) - }); - let tool = Arc::new(ReadFileTool::new(thread.downgrade(), project, action_log)); + let tool = Arc::new(ReadFileTool::new(project, action_log, true)); let (event_stream, _) = ToolCallEventStream::test(); let result = cx @@ -429,20 +398,7 @@ mod test { .await; let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; let action_log = cx.new(|_| ActionLog::new(project.clone())); - let context_server_registry = - cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); - let model = Arc::new(FakeLanguageModel::default()); - let thread = cx.new(|cx| { - Thread::new( - project.clone(), - cx.new(|_cx| ProjectContext::default()), - context_server_registry, - Templates::new(), - Some(model), - cx, - ) - }); - let tool = Arc::new(ReadFileTool::new(thread.downgrade(), project, action_log)); + let tool = Arc::new(ReadFileTool::new(project, action_log, true)); let result = cx .update(|cx| { let input = ReadFileToolInput { @@ -476,20 +432,7 @@ mod test { let language_registry = project.read_with(cx, |project, _| project.languages().clone()); language_registry.add(language::rust_lang()); let action_log = cx.new(|_| ActionLog::new(project.clone())); - let context_server_registry = - cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); - let model = Arc::new(FakeLanguageModel::default()); - let thread = cx.new(|cx| { - Thread::new( - project.clone(), - cx.new(|_cx| ProjectContext::default()), - context_server_registry, - Templates::new(), - Some(model), - cx, - ) - }); - let tool = Arc::new(ReadFileTool::new(thread.downgrade(), project, action_log)); + let tool = Arc::new(ReadFileTool::new(project, action_log, true)); let result = cx .update(|cx| { let input = ReadFileToolInput { @@ -569,20 +512,7 @@ mod test { let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; let action_log = cx.new(|_| ActionLog::new(project.clone())); - let context_server_registry = - cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); - let model = Arc::new(FakeLanguageModel::default()); - let thread = cx.new(|cx| { - Thread::new( - project.clone(), - cx.new(|_cx| ProjectContext::default()), - context_server_registry, - Templates::new(), - Some(model), - cx, - ) - }); - let tool = Arc::new(ReadFileTool::new(thread.downgrade(), project, action_log)); + let tool = Arc::new(ReadFileTool::new(project, action_log, true)); let result = cx .update(|cx| { let input = ReadFileToolInput { @@ -614,20 +544,7 @@ mod test { .await; let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; let action_log = cx.new(|_| ActionLog::new(project.clone())); - let context_server_registry = - cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); - let model = Arc::new(FakeLanguageModel::default()); - let thread = cx.new(|cx| { - Thread::new( - project.clone(), - cx.new(|_cx| ProjectContext::default()), - context_server_registry, - Templates::new(), - Some(model), - cx, - ) - }); - let tool = Arc::new(ReadFileTool::new(thread.downgrade(), project, action_log)); + let tool = Arc::new(ReadFileTool::new(project, action_log, true)); // start_line of 0 should be treated as 1 let result = cx @@ -757,20 +674,7 @@ mod test { let project = Project::test(fs.clone(), [path!("/project_root").as_ref()], cx).await; let action_log = cx.new(|_| ActionLog::new(project.clone())); - let context_server_registry = - cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); - let model = Arc::new(FakeLanguageModel::default()); - let thread = cx.new(|cx| { - Thread::new( - project.clone(), - cx.new(|_cx| ProjectContext::default()), - context_server_registry, - Templates::new(), - Some(model), - cx, - ) - }); - let tool = Arc::new(ReadFileTool::new(thread.downgrade(), project, action_log)); + let tool = Arc::new(ReadFileTool::new(project, action_log, true)); // Reading a file outside the project worktree should fail let result = cx @@ -965,20 +869,7 @@ mod test { let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; let action_log = cx.new(|_| ActionLog::new(project.clone())); - let context_server_registry = - cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); - let model = Arc::new(FakeLanguageModel::default()); - let thread = cx.new(|cx| { - Thread::new( - project.clone(), - cx.new(|_cx| ProjectContext::default()), - context_server_registry, - Templates::new(), - Some(model), - cx, - ) - }); - let tool = Arc::new(ReadFileTool::new(thread.downgrade(), project, action_log)); + let tool = Arc::new(ReadFileTool::new(project, action_log, true)); let (event_stream, mut event_rx) = ToolCallEventStream::test(); let read_task = cx.update(|cx| { @@ -1084,24 +975,7 @@ mod test { .await; let action_log = cx.new(|_| ActionLog::new(project.clone())); - let context_server_registry = - cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); - let model = Arc::new(FakeLanguageModel::default()); - let thread = cx.new(|cx| { - Thread::new( - project.clone(), - cx.new(|_cx| ProjectContext::default()), - context_server_registry, - Templates::new(), - Some(model), - cx, - ) - }); - let tool = Arc::new(ReadFileTool::new( - thread.downgrade(), - project.clone(), - action_log.clone(), - )); + let tool = Arc::new(ReadFileTool::new(project.clone(), action_log.clone(), true)); // Test reading allowed files in worktree1 let result = cx @@ -1288,24 +1162,7 @@ mod test { cx.executor().run_until_parked(); let action_log = cx.new(|_| ActionLog::new(project.clone())); - let context_server_registry = - cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); - let model = Arc::new(FakeLanguageModel::default()); - let thread = cx.new(|cx| { - Thread::new( - project.clone(), - cx.new(|_cx| ProjectContext::default()), - context_server_registry, - Templates::new(), - Some(model), - cx, - ) - }); - let tool = Arc::new(ReadFileTool::new( - thread.downgrade(), - project.clone(), - action_log, - )); + let tool = Arc::new(ReadFileTool::new(project.clone(), action_log, true)); let (event_stream, mut event_rx) = ToolCallEventStream::test(); let task = cx.update(|cx| { @@ -1364,24 +1221,7 @@ mod test { cx.executor().run_until_parked(); let action_log = cx.new(|_| ActionLog::new(project.clone())); - let context_server_registry = - cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); - let model = Arc::new(FakeLanguageModel::default()); - let thread = cx.new(|cx| { - Thread::new( - project.clone(), - cx.new(|_cx| ProjectContext::default()), - context_server_registry, - Templates::new(), - Some(model), - cx, - ) - }); - let tool = Arc::new(ReadFileTool::new( - thread.downgrade(), - project.clone(), - action_log, - )); + let tool = Arc::new(ReadFileTool::new(project.clone(), action_log, true)); let (event_stream, mut event_rx) = ToolCallEventStream::test(); let task = cx.update(|cx| { @@ -1444,24 +1284,7 @@ mod test { cx.executor().run_until_parked(); let action_log = cx.new(|_| ActionLog::new(project.clone())); - let context_server_registry = - cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); - let model = Arc::new(FakeLanguageModel::default()); - let thread = cx.new(|cx| { - Thread::new( - project.clone(), - cx.new(|_cx| ProjectContext::default()), - context_server_registry, - Templates::new(), - Some(model), - cx, - ) - }); - let tool = Arc::new(ReadFileTool::new( - thread.downgrade(), - project.clone(), - action_log, - )); + let tool = Arc::new(ReadFileTool::new(project.clone(), action_log, true)); let (event_stream, mut event_rx) = ToolCallEventStream::test(); let result = cx diff --git a/crates/agent/src/tools/spawn_agent_tool.rs b/crates/agent/src/tools/spawn_agent_tool.rs index b75c41775258db49577024dca3eb1770937e52e8..162de68b86115056e9579d22a8623d675245cc91 100644 --- a/crates/agent/src/tools/spawn_agent_tool.rs +++ b/crates/agent/src/tools/spawn_agent_tool.rs @@ -161,29 +161,42 @@ impl AgentTool for SpawnAgentTool { Ok((subagent, session_info)) })?; - match subagent.send(input.message, cx).await { - Ok(output) => { - session_info.message_end_index = - cx.update(|cx| Some(subagent.num_entries(cx).saturating_sub(1))); - event_stream.update_fields_with_meta( - acp::ToolCallUpdateFields::new().content(vec![output.clone().into()]), - Some(acp::Meta::from_iter([( - SUBAGENT_SESSION_INFO_META_KEY.into(), - serde_json::json!(&session_info), - )])), - ); + let send_result = subagent.send(input.message, cx).await; + + session_info.message_end_index = + cx.update(|cx| Some(subagent.num_entries(cx).saturating_sub(1))); + + let meta = Some(acp::Meta::from_iter([( + SUBAGENT_SESSION_INFO_META_KEY.into(), + serde_json::json!(&session_info), + )])); + + let (output, result) = match send_result { + Ok(output) => ( + output.clone(), Ok(SpawnAgentToolOutput::Success { session_id: session_info.session_id.clone(), session_info, output, - }) + }), + ), + Err(e) => { + let error = e.to_string(); + ( + error.clone(), + Err(SpawnAgentToolOutput::Error { + session_id: Some(session_info.session_id.clone()), + error, + session_info: Some(session_info), + }), + ) } - Err(e) => Err(SpawnAgentToolOutput::Error { - session_id: Some(session_info.session_id.clone()), - error: e.to_string(), - session_info: Some(session_info), - }), - } + }; + event_stream.update_fields_with_meta( + acp::ToolCallUpdateFields::new().content(vec![output.into()]), + meta, + ); + result }) } diff --git a/crates/agent/src/tools/streaming_edit_file_tool.rs b/crates/agent/src/tools/streaming_edit_file_tool.rs index 7e023d7d7e00c2eb13ea78467776816b13151796..62b96d569f34d65889abee6be803674dfa42e709 100644 --- a/crates/agent/src/tools/streaming_edit_file_tool.rs +++ b/crates/agent/src/tools/streaming_edit_file_tool.rs @@ -483,7 +483,12 @@ impl EditSession { .await .map_err(|e| StreamingEditFileToolOutput::error(e.to_string()))?; - ensure_buffer_saved(&buffer, &abs_path, tool, cx)?; + let action_log = tool + .thread + .read_with(cx, |thread, _cx| thread.action_log().clone()) + .ok(); + + ensure_buffer_saved(&buffer, &abs_path, tool, action_log.as_ref(), cx)?; let diff = cx.new(|cx| Diff::new(buffer.clone(), cx)); event_stream.update_diff(diff.clone()); @@ -495,13 +500,9 @@ impl EditSession { } }) as Box); - tool.thread - .update(cx, |thread, cx| { - thread - .action_log() - .update(cx, |log, cx| log.buffer_read(buffer.clone(), cx)) - }) - .ok(); + if let Some(action_log) = &action_log { + action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx)); + } let old_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot()); let old_text = cx @@ -637,18 +638,6 @@ impl EditSession { log.buffer_edited(buffer.clone(), cx); }); - if let Some(new_mtime) = buffer.read_with(cx, |buffer, _| { - buffer.file().and_then(|file| file.disk_state().mtime()) - }) { - tool.thread - .update(cx, |thread, _| { - thread - .file_read_times - .insert(abs_path.to_path_buf(), new_mtime); - }) - .map_err(|e| StreamingEditFileToolOutput::error(e.to_string()))?; - } - let new_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot()); let (new_text, unified_diff) = cx .background_spawn({ @@ -1018,10 +1007,12 @@ fn ensure_buffer_saved( buffer: &Entity, abs_path: &PathBuf, tool: &StreamingEditFileTool, + action_log: Option<&Entity>, cx: &mut AsyncApp, ) -> Result<(), StreamingEditFileToolOutput> { - let check_result = tool.thread.update(cx, |thread, cx| { - let last_read = thread.file_read_times.get(abs_path).copied(); + let last_read_mtime = + action_log.and_then(|log| log.read_with(cx, |log, _| log.file_read_time(abs_path))); + let check_result = tool.thread.read_with(cx, |thread, cx| { let current = buffer .read(cx) .file() @@ -1029,12 +1020,10 @@ fn ensure_buffer_saved( let dirty = buffer.read(cx).is_dirty(); let has_save = thread.has_tool(SaveFileTool::NAME); let has_restore = thread.has_tool(RestoreFileFromDiskTool::NAME); - (last_read, current, dirty, has_save, has_restore) + (current, dirty, has_save, has_restore) }); - let Ok((last_read_mtime, current_mtime, is_dirty, has_save_tool, has_restore_tool)) = - check_result - else { + let Ok((current_mtime, is_dirty, has_save_tool, has_restore_tool)) = check_result else { return Ok(()); }; @@ -4006,11 +3995,7 @@ mod tests { let languages = project.read_with(cx, |project, _| project.languages().clone()); let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone()); - let read_tool = Arc::new(crate::ReadFileTool::new( - thread.downgrade(), - project.clone(), - action_log, - )); + let read_tool = Arc::new(crate::ReadFileTool::new(project.clone(), action_log, true)); let edit_tool = Arc::new(StreamingEditFileTool::new( project.clone(), thread.downgrade(), @@ -4112,11 +4097,7 @@ mod tests { let languages = project.read_with(cx, |project, _| project.languages().clone()); let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone()); - let read_tool = Arc::new(crate::ReadFileTool::new( - thread.downgrade(), - project.clone(), - action_log, - )); + let read_tool = Arc::new(crate::ReadFileTool::new(project.clone(), action_log, true)); let edit_tool = Arc::new(StreamingEditFileTool::new( project.clone(), thread.downgrade(), @@ -4225,11 +4206,7 @@ mod tests { let languages = project.read_with(cx, |project, _| project.languages().clone()); let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone()); - let read_tool = Arc::new(crate::ReadFileTool::new( - thread.downgrade(), - project.clone(), - action_log, - )); + let read_tool = Arc::new(crate::ReadFileTool::new(project.clone(), action_log, true)); let edit_tool = Arc::new(StreamingEditFileTool::new( project.clone(), thread.downgrade(), diff --git a/crates/agent_ui/src/connection_view.rs b/crates/agent_ui/src/connection_view.rs index 835ff611288c2bf6867a885ed2be8c6a66679cdb..07e34ccd56f0bd867135fe62894a5a3ff388c85e 100644 --- a/crates/agent_ui/src/connection_view.rs +++ b/crates/agent_ui/src/connection_view.rs @@ -845,6 +845,10 @@ impl ConnectionView { ); }); + if let Some(scroll_position) = thread.read(cx).ui_scroll_position() { + list_state.scroll_to(scroll_position); + } + AgentDiff::set_active_thread(&self.workspace, thread.clone(), window, cx); let connection = thread.read(cx).connection().clone(); diff --git a/crates/agent_ui/src/connection_view/thread_view.rs b/crates/agent_ui/src/connection_view/thread_view.rs index 8ce4da360664774342c4167f7c8dfbce914b647e..8a1a7d2ea5b0f01ba559e83051861b9d6324985f 100644 --- a/crates/agent_ui/src/connection_view/thread_view.rs +++ b/crates/agent_ui/src/connection_view/thread_view.rs @@ -248,7 +248,8 @@ pub struct ThreadView { pub resumed_without_history: bool, pub resume_thread_metadata: Option, pub _cancel_task: Option>, - _draft_save_task: Option>, + _save_task: Option>, + _draft_resolve_task: Option>, pub skip_queue_processing_count: usize, pub user_interrupted_generation: bool, pub can_fast_track_queue: bool, @@ -396,7 +397,7 @@ impl ThreadView { } else { Some(editor.update(cx, |editor, cx| editor.draft_contents(cx))) }; - this._draft_save_task = Some(cx.spawn(async move |this, cx| { + this._draft_resolve_task = Some(cx.spawn(async move |this, cx| { let draft = if let Some(task) = draft_contents_task { let blocks = task.await.ok().filter(|b| !b.is_empty()); blocks @@ -407,15 +408,7 @@ impl ThreadView { this.thread.update(cx, |thread, _cx| { thread.set_draft_prompt(draft); }); - }) - .ok(); - cx.background_executor() - .timer(SERIALIZATION_THROTTLE_TIME) - .await; - this.update(cx, |this, cx| { - if let Some(thread) = this.as_native_thread(cx) { - thread.update(cx, |_thread, cx| cx.notify()); - } + this.schedule_save(cx); }) .ok(); })); @@ -471,7 +464,8 @@ impl ThreadView { is_loading_contents: false, new_server_version_available: None, _cancel_task: None, - _draft_save_task: None, + _save_task: None, + _draft_resolve_task: None, skip_queue_processing_count: 0, user_interrupted_generation: false, can_fast_track_queue: false, @@ -487,12 +481,50 @@ impl ThreadView { _history_subscription: history_subscription, show_codex_windows_warning, }; + let list_state_for_scroll = this.list_state.clone(); + let thread_view = cx.entity().downgrade(); + this.list_state + .set_scroll_handler(move |_event, _window, cx| { + let list_state = list_state_for_scroll.clone(); + let thread_view = thread_view.clone(); + // N.B. We must defer because the scroll handler is called while the + // ListState's RefCell is mutably borrowed. Reading logical_scroll_top() + // directly would panic from a double borrow. + cx.defer(move |cx| { + let scroll_top = list_state.logical_scroll_top(); + let _ = thread_view.update(cx, |this, cx| { + if let Some(thread) = this.as_native_thread(cx) { + thread.update(cx, |thread, _cx| { + thread.set_ui_scroll_position(Some(scroll_top)); + }); + } + this.schedule_save(cx); + }); + }); + }); + if should_auto_submit { this.send(window, cx); } this } + /// Schedule a throttled save of the thread state (draft prompt, scroll position, etc.). + /// Multiple calls within `SERIALIZATION_THROTTLE_TIME` are coalesced into a single save. + fn schedule_save(&mut self, cx: &mut Context) { + self._save_task = Some(cx.spawn(async move |this, cx| { + cx.background_executor() + .timer(SERIALIZATION_THROTTLE_TIME) + .await; + this.update(cx, |this, cx| { + if let Some(thread) = this.as_native_thread(cx) { + thread.update(cx, |_thread, cx| cx.notify()); + } + }) + .ok(); + })); + } + pub fn handle_message_editor_event( &mut self, _editor: &Entity, @@ -6736,6 +6768,31 @@ impl ThreadView { .read(cx) .pending_tool_call(thread.read(cx).session_id(), cx); + let session_id = thread.read(cx).session_id().clone(); + + let fullscreen_toggle = h_flex() + .id(entry_ix) + .py_1() + .w_full() + .justify_center() + .border_t_1() + .when(is_failed, |this| this.border_dashed()) + .border_color(self.tool_card_border_color(cx)) + .hover(|s| s.bg(cx.theme().colors().element_hover)) + .child( + Icon::new(IconName::Maximize) + .color(Color::Muted) + .size(IconSize::Small), + ) + .tooltip(Tooltip::text("Make Subagent Full Screen")) + .on_click(cx.listener(move |this, _event, window, cx| { + this.server_view + .update(cx, |this, cx| { + this.navigate_to_session(session_id.clone(), window, cx); + }) + .ok(); + })); + if is_running && let Some((_, subagent_tool_call_id, _)) = pending_tool_call { if let Some((entry_ix, tool_call)) = thread.read(cx).tool_call(&subagent_tool_call_id) @@ -6750,11 +6807,11 @@ impl ThreadView { window, cx, )) + .child(fullscreen_toggle) } else { this } } else { - let session_id = thread.read(cx).session_id().clone(); this.when(is_expanded, |this| { this.child(self.render_subagent_expanded_content( thread_view, @@ -6771,34 +6828,7 @@ impl ThreadView { .title(message), ) }) - .child( - h_flex() - .id(entry_ix) - .py_1() - .w_full() - .justify_center() - .border_t_1() - .when(is_failed, |this| this.border_dashed()) - .border_color(self.tool_card_border_color(cx)) - .hover(|s| s.bg(cx.theme().colors().element_hover)) - .child( - Icon::new(IconName::Maximize) - .color(Color::Muted) - .size(IconSize::Small), - ) - .tooltip(Tooltip::text("Make Subagent Full Screen")) - .on_click(cx.listener(move |this, _event, window, cx| { - this.server_view - .update(cx, |this, cx| { - this.navigate_to_session( - session_id.clone(), - window, - cx, - ); - }) - .ok(); - })), - ) + .child(fullscreen_toggle) }) } }) diff --git a/crates/auto_update_helper/Cargo.toml b/crates/auto_update_helper/Cargo.toml index 73c38d80dd12e9c42daa42b7e6f2c9d6975cf47b..aa5bf6ac40b0e1ab20cbde510be5d7f389c7ade8 100644 --- a/crates/auto_update_helper/Cargo.toml +++ b/crates/auto_update_helper/Cargo.toml @@ -19,6 +19,7 @@ log.workspace = true simplelog.workspace = true [target.'cfg(target_os = "windows")'.dependencies] +scopeguard = "1.2" windows.workspace = true [target.'cfg(target_os = "windows")'.dev-dependencies] diff --git a/crates/auto_update_helper/src/updater.rs b/crates/auto_update_helper/src/updater.rs index 076e11fb4eef1e5c53e2bdc290be7117330c3e61..7821c908c40873637c4ac3993c320416e2a4b978 100644 --- a/crates/auto_update_helper/src/updater.rs +++ b/crates/auto_update_helper/src/updater.rs @@ -1,13 +1,22 @@ use std::{ + ffi::OsStr, + os::windows::ffi::OsStrExt, path::Path, sync::LazyLock, time::{Duration, Instant}, }; use anyhow::{Context as _, Result}; -use windows::Win32::{ - Foundation::{HWND, LPARAM, WPARAM}, - UI::WindowsAndMessaging::PostMessageW, +use windows::{ + Win32::{ + Foundation::{HWND, LPARAM, WPARAM}, + System::RestartManager::{ + CCH_RM_SESSION_KEY, RmEndSession, RmGetList, RmRegisterResources, RmShutdown, + RmStartSession, + }, + UI::WindowsAndMessaging::PostMessageW, + }, + core::{PCWSTR, PWSTR}, }; use crate::windows_impl::WM_JOB_UPDATED; @@ -262,9 +271,106 @@ pub(crate) static JOBS: LazyLock<[Job; 9]> = LazyLock::new(|| { ] }); +/// Attempts to use Windows Restart Manager to release file handles held by other processes +/// (e.g., Explorer.exe) on the files we need to move during the update. +/// +/// This is a best-effort operation - if it fails, we'll still try the update and rely on +/// the retry logic. +fn release_file_handles(app_dir: &Path) -> Result<()> { + // Files that commonly get locked by Explorer or other processes + let files_to_release = [ + app_dir.join("Zed.exe"), + app_dir.join("bin\\Zed.exe"), + app_dir.join("bin\\zed"), + app_dir.join("conpty.dll"), + ]; + + log::info!("Attempting to release file handles using Restart Manager..."); + + let mut session: u32 = 0; + let mut session_key = [0u16; CCH_RM_SESSION_KEY as usize + 1]; + + // Start a Restart Manager session + let err = unsafe { + RmStartSession( + &mut session, + Some(0), + PWSTR::from_raw(session_key.as_mut_ptr()), + ) + }; + if err.is_err() { + anyhow::bail!("RmStartSession failed: {err:?}"); + } + + // Ensure we end the session when done + let _session_guard = scopeguard::guard(session, |s| { + let _ = unsafe { RmEndSession(s) }; + }); + + // Convert paths to wide strings for Windows API + let wide_paths: Vec> = files_to_release + .iter() + .filter(|p| p.exists()) + .map(|p| { + OsStr::new(p) + .encode_wide() + .chain(std::iter::once(0)) + .collect() + }) + .collect(); + + if wide_paths.is_empty() { + log::info!("No files to release handles for"); + return Ok(()); + } + + let pcwstr_paths: Vec = wide_paths + .iter() + .map(|p| PCWSTR::from_raw(p.as_ptr())) + .collect(); + + // Register the files we want to modify + let err = unsafe { RmRegisterResources(session, Some(&pcwstr_paths), None, None) }; + if err.is_err() { + anyhow::bail!("RmRegisterResources failed: {err:?}"); + } + + // Check if any processes are using these files + let mut needed: u32 = 0; + let mut count: u32 = 0; + let mut reboot_reasons: u32 = 0; + let _ = unsafe { RmGetList(session, &mut needed, &mut count, None, &mut reboot_reasons) }; + + if needed == 0 { + log::info!("No processes are holding handles to the files"); + return Ok(()); + } + + log::info!( + "{} process(es) are holding handles to the files, requesting release...", + needed + ); + + // Request processes to release their handles + // RmShutdown with flags=0 asks applications to release handles gracefully + // For Explorer, this typically releases icon cache handles without closing Explorer + let err = unsafe { RmShutdown(session, 0, None) }; + if err.is_err() { + anyhow::bail!("RmShutdown failed: {:?}", err); + } + + log::info!("Successfully requested handle release"); + Ok(()) +} + pub(crate) fn perform_update(app_dir: &Path, hwnd: Option, launch: bool) -> Result<()> { let hwnd = hwnd.map(|ptr| HWND(ptr as _)); + // Try to release file handles before starting the update + if let Err(e) = release_file_handles(app_dir) { + log::warn!("Restart Manager failed (will continue anyway): {}", e); + } + let mut last_successful_job = None; 'outer: for (i, job) in JOBS.iter().enumerate() { let start = Instant::now(); @@ -279,19 +385,22 @@ pub(crate) fn perform_update(app_dir: &Path, hwnd: Option, launch: bool) unsafe { PostMessageW(hwnd, WM_JOB_UPDATED, WPARAM(0), LPARAM(0))? }; break; } - Err(err) => { - // Check if it's a "not found" error - let io_err = err.downcast_ref::().unwrap(); - if io_err.kind() == std::io::ErrorKind::NotFound { - log::warn!("File or folder not found."); - last_successful_job = Some(i); - unsafe { PostMessageW(hwnd, WM_JOB_UPDATED, WPARAM(0), LPARAM(0))? }; - break; + Err(err) => match err.downcast_ref::() { + Some(io_err) => match io_err.kind() { + std::io::ErrorKind::NotFound => { + log::error!("Operation failed with file not found, aborting: {}", err); + break 'outer; + } + _ => { + log::error!("Operation failed (retrying): {}", err); + std::thread::sleep(Duration::from_millis(50)); + } + }, + None => { + log::error!("Operation failed with unexpected error, aborting: {}", err); + break 'outer; } - - log::error!("Operation failed: {} ({:?})", err, io_err.kind()); - std::thread::sleep(Duration::from_millis(50)); - } + }, } } } diff --git a/crates/cloud_api_client/src/cloud_api_client.rs b/crates/cloud_api_client/src/cloud_api_client.rs index f485e2d20c619715ea342fccd2a5cec0ecaa6f4e..13d67838b216f4990f15ec22c1701aa7aef9dbf2 100644 --- a/crates/cloud_api_client/src/cloud_api_client.rs +++ b/crates/cloud_api_client/src/cloud_api_client.rs @@ -9,7 +9,9 @@ use futures::AsyncReadExt as _; use gpui::{App, Task}; use gpui_tokio::Tokio; use http_client::http::request; -use http_client::{AsyncBody, HttpClientWithUrl, HttpRequestExt, Method, Request, StatusCode}; +use http_client::{ + AsyncBody, HttpClientWithUrl, HttpRequestExt, Json, Method, Request, StatusCode, +}; use parking_lot::RwLock; use thiserror::Error; use yawc::WebSocket; @@ -141,6 +143,7 @@ impl CloudApiClient { pub async fn create_llm_token( &self, system_id: Option, + organization_id: Option, ) -> Result { let request_builder = Request::builder() .method(Method::POST) @@ -153,7 +156,10 @@ impl CloudApiClient { builder.header(ZED_SYSTEM_ID_HEADER_NAME, system_id) }); - let request = self.build_request(request_builder, AsyncBody::default())?; + let request = self.build_request( + request_builder, + Json(CreateLlmTokenBody { organization_id }), + )?; let mut response = self.http_client.send(request).await?; diff --git a/crates/cloud_api_types/src/cloud_api_types.rs b/crates/cloud_api_types/src/cloud_api_types.rs index 2d457fc6630d5b32f049e67a6a460047e925973a..42d3442bfc016f5cb1a39ba421ccdfe386bcbc65 100644 --- a/crates/cloud_api_types/src/cloud_api_types.rs +++ b/crates/cloud_api_types/src/cloud_api_types.rs @@ -52,6 +52,12 @@ pub struct AcceptTermsOfServiceResponse { #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] pub struct LlmToken(pub String); +#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)] +pub struct CreateLlmTokenBody { + #[serde(default)] + pub organization_id: Option, +} + #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] pub struct CreateLlmTokenResponse { pub token: LlmToken, diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 087dbe2a0ba23851689e75401c62b64775cf2282..b521f6b083ae311d98ec46c900ce821fd8042e4a 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -437,6 +437,8 @@ impl Server { .add_request_handler(forward_mutating_project_request::) .add_request_handler(forward_mutating_project_request::) .add_request_handler(forward_mutating_project_request::) + .add_request_handler(forward_read_only_project_request::) + .add_request_handler(forward_mutating_project_request::) .add_request_handler(forward_mutating_project_request::) .add_message_handler(broadcast_project_message_from_host::) .add_message_handler(update_context) diff --git a/crates/collab/tests/integration/git_tests.rs b/crates/collab/tests/integration/git_tests.rs index f3abb5bc3f3e1a12e7ecb56c985f2cff46582cee..6e50e41bade5f5dfdf124f5a6d659e81fc2ce0f6 100644 --- a/crates/collab/tests/integration/git_tests.rs +++ b/crates/collab/tests/integration/git_tests.rs @@ -1,15 +1,14 @@ -use std::path::Path; +use std::path::{Path, PathBuf}; use call::ActiveCall; use git::status::{FileStatus, StatusCode, TrackedStatus}; use git_ui::project_diff::ProjectDiff; -use gpui::{AppContext as _, TestAppContext, VisualTestContext}; +use gpui::{AppContext as _, BackgroundExecutor, TestAppContext, VisualTestContext}; use project::ProjectPath; use serde_json::json; use util::{path, rel_path::rel_path}; use workspace::{MultiWorkspace, Workspace}; -// use crate::TestServer; #[gpui::test] @@ -141,3 +140,142 @@ async fn test_project_diff(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) ); }); } + +#[gpui::test] +async fn test_remote_git_worktrees( + executor: BackgroundExecutor, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + let mut server = TestServer::start(executor.clone()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + + client_a + .fs() + .insert_tree( + path!("/project"), + json!({ ".git": {}, "file.txt": "content" }), + ) + .await; + + let (project_a, _) = client_a.build_local_project(path!("/project"), cx_a).await; + + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + let project_b = client_b.join_remote_project(project_id, cx_b).await; + + executor.run_until_parked(); + + let repo_b = cx_b.update(|cx| project_b.read(cx).active_repository(cx).unwrap()); + + // Initially only the main worktree (the repo itself) should be present + let worktrees = cx_b + .update(|cx| repo_b.update(cx, |repository, _| repository.worktrees())) + .await + .unwrap() + .unwrap(); + assert_eq!(worktrees.len(), 1); + assert_eq!(worktrees[0].path, PathBuf::from(path!("/project"))); + + // Client B creates a git worktree via the remote project + let worktree_directory = PathBuf::from(path!("/project")); + cx_b.update(|cx| { + repo_b.update(cx, |repository, _| { + repository.create_worktree( + "feature-branch".to_string(), + worktree_directory.clone(), + Some("abc123".to_string()), + ) + }) + }) + .await + .unwrap() + .unwrap(); + + executor.run_until_parked(); + + // Client B lists worktrees — should see main + the one just created + let worktrees = cx_b + .update(|cx| repo_b.update(cx, |repository, _| repository.worktrees())) + .await + .unwrap() + .unwrap(); + assert_eq!(worktrees.len(), 2); + assert_eq!(worktrees[0].path, PathBuf::from(path!("/project"))); + assert_eq!(worktrees[1].path, worktree_directory.join("feature-branch")); + assert_eq!(worktrees[1].ref_name.as_ref(), "refs/heads/feature-branch"); + assert_eq!(worktrees[1].sha.as_ref(), "abc123"); + + // Verify from the host side that the worktree was actually created + let host_worktrees = { + let repo_a = cx_a.update(|cx| { + project_a + .read(cx) + .repositories(cx) + .values() + .next() + .unwrap() + .clone() + }); + cx_a.update(|cx| repo_a.update(cx, |repository, _| repository.worktrees())) + .await + .unwrap() + .unwrap() + }; + assert_eq!(host_worktrees.len(), 2); + assert_eq!(host_worktrees[0].path, PathBuf::from(path!("/project"))); + assert_eq!( + host_worktrees[1].path, + worktree_directory.join("feature-branch") + ); + + // Client B creates a second git worktree without an explicit commit + cx_b.update(|cx| { + repo_b.update(cx, |repository, _| { + repository.create_worktree( + "bugfix-branch".to_string(), + worktree_directory.clone(), + None, + ) + }) + }) + .await + .unwrap() + .unwrap(); + + executor.run_until_parked(); + + // Client B lists worktrees — should now have main + two created + let worktrees = cx_b + .update(|cx| repo_b.update(cx, |repository, _| repository.worktrees())) + .await + .unwrap() + .unwrap(); + assert_eq!(worktrees.len(), 3); + + let feature_worktree = worktrees + .iter() + .find(|worktree| worktree.ref_name.as_ref() == "refs/heads/feature-branch") + .expect("should find feature-branch worktree"); + assert_eq!( + feature_worktree.path, + worktree_directory.join("feature-branch") + ); + + let bugfix_worktree = worktrees + .iter() + .find(|worktree| worktree.ref_name.as_ref() == "refs/heads/bugfix-branch") + .expect("should find bugfix-branch worktree"); + assert_eq!( + bugfix_worktree.path, + worktree_directory.join("bugfix-branch") + ); + assert_eq!(bugfix_worktree.sha.as_ref(), "fake-sha"); +} diff --git a/crates/collab/tests/integration/integration_tests.rs b/crates/collab/tests/integration/integration_tests.rs index c26f20c1e294326f275dbfda1d2d41603719cd3e..3bad9c82c26392a935f67efc578b5d293b2cab3d 100644 --- a/crates/collab/tests/integration/integration_tests.rs +++ b/crates/collab/tests/integration/integration_tests.rs @@ -7205,3 +7205,89 @@ async fn test_remote_git_branches( assert_eq!(host_branch.name(), "totally-new-branch"); } + +#[gpui::test] +async fn test_guest_can_rejoin_shared_project_after_leaving_call( + executor: BackgroundExecutor, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, + cx_c: &mut TestAppContext, +) { + let mut server = TestServer::start(executor.clone()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + let client_c = server.create_client(cx_c, "user_c").await; + + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) + .await; + + client_a + .fs() + .insert_tree( + path!("/project"), + json!({ + "file.txt": "hello\n", + }), + ) + .await; + + let (project_a, _worktree_id) = client_a.build_local_project(path!("/project"), cx_a).await; + let active_call_a = cx_a.read(ActiveCall::global); + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + + let _project_b = client_b.join_remote_project(project_id, cx_b).await; + executor.run_until_parked(); + + // third client joins call to prevent room from being torn down + let _project_c = client_c.join_remote_project(project_id, cx_c).await; + executor.run_until_parked(); + + let active_call_b = cx_b.read(ActiveCall::global); + active_call_b + .update(cx_b, |call, cx| call.hang_up(cx)) + .await + .unwrap(); + executor.run_until_parked(); + + let user_id_b = client_b.current_user_id(cx_b).to_proto(); + let active_call_a = cx_a.read(ActiveCall::global); + active_call_a + .update(cx_a, |call, cx| call.invite(user_id_b, None, cx)) + .await + .unwrap(); + executor.run_until_parked(); + let active_call_b = cx_b.read(ActiveCall::global); + active_call_b + .update(cx_b, |call, cx| call.accept_incoming(cx)) + .await + .unwrap(); + executor.run_until_parked(); + + let _project_b2 = client_b.join_remote_project(project_id, cx_b).await; + executor.run_until_parked(); + + project_a.read_with(cx_a, |project, _| { + let guest_count = project + .collaborators() + .values() + .filter(|c| !c.is_host) + .count(); + + assert_eq!( + guest_count, 2, + "host should have exactly one guest collaborator after rejoin" + ); + }); + + _project_b.read_with(cx_b, |project, _| { + assert_eq!( + project.client_subscriptions().len(), + 0, + "We should clear all host subscriptions after leaving the project" + ); + }) +} diff --git a/crates/collab/tests/integration/remote_editing_collaboration_tests.rs b/crates/collab/tests/integration/remote_editing_collaboration_tests.rs index 4556c740ec74f6fb1bc8a2c760812376dae6b4a8..6825c468e783ee8d3a2a6107a031accfc108abd0 100644 --- a/crates/collab/tests/integration/remote_editing_collaboration_tests.rs +++ b/crates/collab/tests/integration/remote_editing_collaboration_tests.rs @@ -33,7 +33,7 @@ use settings::{ SettingsStore, }; use std::{ - path::Path, + path::{Path, PathBuf}, sync::{ Arc, atomic::{AtomicUsize, Ordering}, @@ -396,6 +396,130 @@ async fn test_ssh_collaboration_git_branches( }); } +#[gpui::test] +async fn test_ssh_collaboration_git_worktrees( + executor: BackgroundExecutor, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, + server_cx: &mut TestAppContext, +) { + cx_a.set_name("a"); + cx_b.set_name("b"); + server_cx.set_name("server"); + + cx_a.update(|cx| { + release_channel::init(semver::Version::new(0, 0, 0), cx); + }); + server_cx.update(|cx| { + release_channel::init(semver::Version::new(0, 0, 0), cx); + }); + + let mut server = TestServer::start(executor.clone()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + + let (opts, server_ssh, _) = RemoteClient::fake_server(cx_a, server_cx); + let remote_fs = FakeFs::new(server_cx.executor()); + remote_fs + .insert_tree("/project", json!({ ".git": {}, "file.txt": "content" })) + .await; + + server_cx.update(HeadlessProject::init); + let languages = Arc::new(LanguageRegistry::new(server_cx.executor())); + let headless_project = server_cx.new(|cx| { + HeadlessProject::new( + HeadlessAppState { + session: server_ssh, + fs: remote_fs.clone(), + http_client: Arc::new(BlockedHttpClient), + node_runtime: NodeRuntime::unavailable(), + languages, + extension_host_proxy: Arc::new(ExtensionHostProxy::new()), + startup_time: std::time::Instant::now(), + }, + false, + cx, + ) + }); + + let client_ssh = RemoteClient::connect_mock(opts, cx_a).await; + let (project_a, _) = client_a + .build_ssh_project("/project", client_ssh, false, cx_a) + .await; + + let active_call_a = cx_a.read(ActiveCall::global); + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + let project_b = client_b.join_remote_project(project_id, cx_b).await; + + executor.run_until_parked(); + + let repo_b = cx_b.update(|cx| project_b.read(cx).active_repository(cx).unwrap()); + + let worktrees = cx_b + .update(|cx| repo_b.update(cx, |repo, _| repo.worktrees())) + .await + .unwrap() + .unwrap(); + assert_eq!(worktrees.len(), 1); + + let worktree_directory = PathBuf::from("/project"); + cx_b.update(|cx| { + repo_b.update(cx, |repo, _| { + repo.create_worktree( + "feature-branch".to_string(), + worktree_directory.clone(), + Some("abc123".to_string()), + ) + }) + }) + .await + .unwrap() + .unwrap(); + + executor.run_until_parked(); + + let worktrees = cx_b + .update(|cx| repo_b.update(cx, |repo, _| repo.worktrees())) + .await + .unwrap() + .unwrap(); + assert_eq!(worktrees.len(), 2); + assert_eq!(worktrees[1].path, worktree_directory.join("feature-branch")); + assert_eq!(worktrees[1].ref_name.as_ref(), "refs/heads/feature-branch"); + assert_eq!(worktrees[1].sha.as_ref(), "abc123"); + + let server_worktrees = { + let server_repo = server_cx.update(|cx| { + headless_project.update(cx, |headless_project, cx| { + headless_project + .git_store + .read(cx) + .repositories() + .values() + .next() + .unwrap() + .clone() + }) + }); + server_cx + .update(|cx| server_repo.update(cx, |repo, _| repo.worktrees())) + .await + .unwrap() + .unwrap() + }; + assert_eq!(server_worktrees.len(), 2); + assert_eq!( + server_worktrees[1].path, + worktree_directory.join("feature-branch") + ); +} + #[gpui::test] async fn test_ssh_collaboration_formatting_with_prettier( executor: BackgroundExecutor, diff --git a/crates/docs_preprocessor/src/main.rs b/crates/docs_preprocessor/src/main.rs index 6ef599542a5b2f511915d7435af192162a5dbd3b..43efbeea0b0310cf70cd9bdb560b1b0d2b0c14ef 100644 --- a/crates/docs_preprocessor/src/main.rs +++ b/crates/docs_preprocessor/src/main.rs @@ -578,6 +578,7 @@ fn handle_postprocessing() -> Result<()> { .expect("Default title not a string") .to_string(); let amplitude_key = std::env::var("DOCS_AMPLITUDE_API_KEY").unwrap_or_default(); + let consent_io_instance = std::env::var("DOCS_CONSENT_IO_INSTANCE").unwrap_or_default(); output.insert("html".to_string(), zed_html); mdbook::Renderer::render(&mdbook::renderer::HtmlHandlebars::new(), &ctx)?; @@ -647,6 +648,7 @@ fn handle_postprocessing() -> Result<()> { zlog::trace!(logger => "Updating {:?}", pretty_path(&file, &root_dir)); let contents = contents.replace("#description#", meta_description); let contents = contents.replace("#amplitude_key#", &litude_key); + let contents = contents.replace("#consent_io_instance#", &consent_io_instance); let contents = title_regex() .replace(&contents, |_: ®ex::Captures| { format!("{}", meta_title) diff --git a/crates/edit_prediction/src/edit_prediction.rs b/crates/edit_prediction/src/edit_prediction.rs index e6e3a9abdf83deb785cd56d358b065973682b8cc..5c7ce045121739f341b84dd87d827878550f4048 100644 --- a/crates/edit_prediction/src/edit_prediction.rs +++ b/crates/edit_prediction/src/edit_prediction.rs @@ -1,7 +1,7 @@ use anyhow::Result; use arrayvec::ArrayVec; use client::{Client, EditPredictionUsage, UserStore}; -use cloud_api_types::SubmitEditPredictionFeedbackBody; +use cloud_api_types::{OrganizationId, SubmitEditPredictionFeedbackBody}; use cloud_llm_client::predict_edits_v3::{ PredictEditsV3Request, PredictEditsV3Response, RawCompletionRequest, RawCompletionResponse, }; @@ -69,6 +69,7 @@ pub mod sweep_ai; pub mod udiff; mod capture_example; +pub mod open_ai_compatible; mod zed_edit_prediction_delegate; pub mod zeta; @@ -107,13 +108,8 @@ const EDIT_PREDICTION_SETTLED_EVENT: &str = "Edit Prediction Settled"; const EDIT_PREDICTION_SETTLED_TTL: Duration = Duration::from_secs(60 * 5); const EDIT_PREDICTION_SETTLED_QUIESCENCE: Duration = Duration::from_secs(10); -pub struct Zeta2FeatureFlag; pub struct EditPredictionJumpsFeatureFlag; -impl FeatureFlag for Zeta2FeatureFlag { - const NAME: &'static str = "zeta2"; -} - impl FeatureFlag for EditPredictionJumpsFeatureFlag { const NAME: &'static str = "edit_prediction_jumps"; } @@ -129,6 +125,7 @@ impl Global for EditPredictionStoreGlobal {} #[derive(Clone)] pub struct Zeta2RawConfig { pub model_id: Option, + pub environment: Option, pub format: ZetaFormat, } @@ -147,7 +144,7 @@ pub struct EditPredictionStore { pub sweep_ai: SweepAi, pub mercury: Mercury, data_collection_choice: DataCollectionChoice, - reject_predictions_tx: mpsc::UnboundedSender, + reject_predictions_tx: mpsc::UnboundedSender, settled_predictions_tx: mpsc::UnboundedSender, shown_predictions: VecDeque, rated_predictions: HashSet, @@ -155,6 +152,11 @@ pub struct EditPredictionStore { settled_event_callback: Option>, } +pub(crate) struct EditPredictionRejectionPayload { + rejection: EditPredictionRejection, + organization_id: Option, +} + #[derive(Copy, Clone, PartialEq, Eq)] pub enum EditPredictionModel { Zeta, @@ -723,8 +725,13 @@ impl EditPredictionStore { |this, _listener, _event, cx| { let client = this.client.clone(); let llm_token = this.llm_token.clone(); + let organization_id = this + .user_store + .read(cx) + .current_organization() + .map(|organization| organization.id.clone()); cx.spawn(async move |_this, _cx| { - llm_token.refresh(&client).await?; + llm_token.refresh(&client, organization_id).await?; anyhow::Ok(()) }) .detach_and_log_err(cx); @@ -754,7 +761,12 @@ impl EditPredictionStore { let version_str = env::var("ZED_ZETA_FORMAT").ok()?; let format = ZetaFormat::parse(&version_str).ok()?; let model_id = env::var("ZED_ZETA_MODEL").ok(); - Some(Zeta2RawConfig { model_id, format }) + let environment = env::var("ZED_ZETA_ENVIRONMENT").ok(); + Some(Zeta2RawConfig { + model_id, + environment, + format, + }) } pub fn set_edit_prediction_model(&mut self, model: EditPredictionModel) { @@ -785,11 +797,17 @@ impl EditPredictionStore { let client = self.client.clone(); let llm_token = self.llm_token.clone(); let app_version = AppVersion::global(cx); + let organization_id = self + .user_store + .read(cx) + .current_organization() + .map(|organization| organization.id.clone()); + cx.spawn(async move |this, cx| { let experiments = cx .background_spawn(async move { let http_client = client.http_client(); - let token = llm_token.acquire(&client).await?; + let token = llm_token.acquire(&client, organization_id).await?; let url = http_client.build_zed_llm_url("/edit_prediction_experiments", &[])?; let request = http_client::Request::builder() .method(Method::GET) @@ -1428,7 +1446,7 @@ impl EditPredictionStore { } async fn handle_rejected_predictions( - rx: UnboundedReceiver, + rx: UnboundedReceiver, client: Arc, llm_token: LlmApiToken, app_version: Version, @@ -1437,7 +1455,11 @@ impl EditPredictionStore { let mut rx = std::pin::pin!(rx.peekable()); let mut batched = Vec::new(); - while let Some(rejection) = rx.next().await { + while let Some(EditPredictionRejectionPayload { + rejection, + organization_id, + }) = rx.next().await + { batched.push(rejection); if batched.len() < MAX_EDIT_PREDICTION_REJECTIONS_PER_REQUEST / 2 { @@ -1475,6 +1497,7 @@ impl EditPredictionStore { }, client.clone(), llm_token.clone(), + organization_id, app_version.clone(), true, ) @@ -1680,13 +1703,23 @@ impl EditPredictionStore { all_language_settings(None, cx).edit_predictions.provider, EditPredictionProvider::Ollama | EditPredictionProvider::OpenAiCompatibleApi ); + if is_cloud { + let organization_id = self + .user_store + .read(cx) + .current_organization() + .map(|organization| organization.id.clone()); + self.reject_predictions_tx - .unbounded_send(EditPredictionRejection { - request_id: prediction_id.to_string(), - reason, - was_shown, - model_version, + .unbounded_send(EditPredictionRejectionPayload { + rejection: EditPredictionRejection { + request_id: prediction_id.to_string(), + reason, + was_shown, + model_version, + }, + organization_id, }) .log_err(); } @@ -2108,7 +2141,7 @@ impl EditPredictionStore { active_buffer.clone(), position, trigger, - cx.has_flag::(), + cx.has_flag::(), cx, ) } @@ -2341,6 +2374,7 @@ impl EditPredictionStore { client: Arc, custom_url: Option>, llm_token: LlmApiToken, + organization_id: Option, app_version: Version, ) -> Result<(RawCompletionResponse, Option)> { let url = if let Some(custom_url) = custom_url { @@ -2360,6 +2394,7 @@ impl EditPredictionStore { }, client, llm_token, + organization_id, app_version, true, ) @@ -2370,6 +2405,7 @@ impl EditPredictionStore { input: ZetaPromptInput, client: Arc, llm_token: LlmApiToken, + organization_id: Option, app_version: Version, trigger: PredictEditsRequestTrigger, ) -> Result<(PredictEditsV3Response, Option)> { @@ -2392,6 +2428,7 @@ impl EditPredictionStore { }, client, llm_token, + organization_id, app_version, true, ) @@ -2445,6 +2482,7 @@ impl EditPredictionStore { build: impl Fn(http_client::http::request::Builder) -> Result>, client: Arc, llm_token: LlmApiToken, + organization_id: Option, app_version: Version, require_auth: bool, ) -> Result<(Res, Option)> @@ -2454,9 +2492,12 @@ impl EditPredictionStore { let http_client = client.http_client(); let mut token = if require_auth { - Some(llm_token.acquire(&client).await?) + Some(llm_token.acquire(&client, organization_id.clone()).await?) } else { - llm_token.acquire(&client).await.ok() + llm_token + .acquire(&client, organization_id.clone()) + .await + .ok() }; let mut did_retry = false; @@ -2498,7 +2539,7 @@ impl EditPredictionStore { return Ok((serde_json::from_slice(&body)?, usage)); } else if !did_retry && token.is_some() && response.needs_llm_token_refresh() { did_retry = true; - token = Some(llm_token.refresh(&client).await?); + token = Some(llm_token.refresh(&client, organization_id.clone()).await?); } else { let mut body = String::new(); response.body_mut().read_to_string(&mut body).await?; diff --git a/crates/edit_prediction/src/fim.rs b/crates/edit_prediction/src/fim.rs index 66f2e58a3b01b4fbf49b11864db4daec6b4dc1c2..d3e18f73acc665eec28d725530d11297cf4d69ea 100644 --- a/crates/edit_prediction/src/fim.rs +++ b/crates/edit_prediction/src/fim.rs @@ -1,6 +1,7 @@ use crate::{ - EditPredictionId, EditPredictionModelInput, cursor_excerpt, prediction::EditPredictionResult, - zeta, + EditPredictionId, EditPredictionModelInput, cursor_excerpt, + open_ai_compatible::{self, load_open_ai_compatible_api_key_if_needed}, + prediction::EditPredictionResult, }; use anyhow::{Context as _, Result, anyhow}; use gpui::{App, AppContext as _, Entity, Task}; @@ -58,6 +59,8 @@ pub fn request_prediction( return Task::ready(Err(anyhow!("Unsupported edit prediction provider for FIM"))); }; + let api_key = load_open_ai_compatible_api_key_if_needed(provider, cx); + let result = cx.background_spawn(async move { let (excerpt_range, _) = cursor_excerpt::editable_and_context_ranges_for_cursor_position( cursor_point, @@ -90,12 +93,14 @@ pub fn request_prediction( let stop_tokens = get_fim_stop_tokens(); let max_tokens = settings.max_output_tokens; - let (response_text, request_id) = zeta::send_custom_server_request( + + let (response_text, request_id) = open_ai_compatible::send_custom_server_request( provider, &settings, prompt, max_tokens, stop_tokens, + api_key, &http_client, ) .await?; diff --git a/crates/edit_prediction/src/open_ai_compatible.rs b/crates/edit_prediction/src/open_ai_compatible.rs new file mode 100644 index 0000000000000000000000000000000000000000..ca378ba1fd0bc9bdbb3e85c7610e1b94c1be388f --- /dev/null +++ b/crates/edit_prediction/src/open_ai_compatible.rs @@ -0,0 +1,133 @@ +use anyhow::{Context as _, Result}; +use cloud_llm_client::predict_edits_v3::{RawCompletionRequest, RawCompletionResponse}; +use futures::AsyncReadExt as _; +use gpui::{App, AppContext as _, Entity, Global, SharedString, Task, http_client}; +use language::language_settings::{OpenAiCompatibleEditPredictionSettings, all_language_settings}; +use language_model::{ApiKeyState, EnvVar, env_var}; +use std::sync::Arc; + +pub fn open_ai_compatible_api_url(cx: &App) -> SharedString { + all_language_settings(None, cx) + .edit_predictions + .open_ai_compatible_api + .as_ref() + .map(|settings| settings.api_url.clone()) + .unwrap_or_default() + .into() +} + +pub const OPEN_AI_COMPATIBLE_CREDENTIALS_USERNAME: &str = "openai-compatible-api-token"; +pub static OPEN_AI_COMPATIBLE_TOKEN_ENV_VAR: std::sync::LazyLock = + env_var!("ZED_OPEN_AI_COMPATIBLE_EDIT_PREDICTION_API_KEY"); + +struct GlobalOpenAiCompatibleApiKey(Entity); + +impl Global for GlobalOpenAiCompatibleApiKey {} + +pub fn open_ai_compatible_api_token(cx: &mut App) -> Entity { + if let Some(global) = cx.try_global::() { + return global.0.clone(); + } + + let entity = cx.new(|cx| { + ApiKeyState::new( + open_ai_compatible_api_url(cx), + OPEN_AI_COMPATIBLE_TOKEN_ENV_VAR.clone(), + ) + }); + cx.set_global(GlobalOpenAiCompatibleApiKey(entity.clone())); + entity +} + +pub fn load_open_ai_compatible_api_token( + cx: &mut App, +) -> Task> { + let api_url = open_ai_compatible_api_url(cx); + open_ai_compatible_api_token(cx).update(cx, |key_state, cx| { + key_state.load_if_needed(api_url, |s| s, cx) + }) +} + +pub fn load_open_ai_compatible_api_key_if_needed( + provider: settings::EditPredictionProvider, + cx: &mut App, +) -> Option> { + if provider != settings::EditPredictionProvider::OpenAiCompatibleApi { + return None; + } + _ = load_open_ai_compatible_api_token(cx); + let url = open_ai_compatible_api_url(cx); + return open_ai_compatible_api_token(cx).read(cx).key(&url); +} + +pub(crate) async fn send_custom_server_request( + provider: settings::EditPredictionProvider, + settings: &OpenAiCompatibleEditPredictionSettings, + prompt: String, + max_tokens: u32, + stop_tokens: Vec, + api_key: Option>, + http_client: &Arc, +) -> Result<(String, String)> { + match provider { + settings::EditPredictionProvider::Ollama => { + let response = crate::ollama::make_request( + settings.clone(), + prompt, + stop_tokens, + http_client.clone(), + ) + .await?; + Ok((response.response, response.created_at)) + } + _ => { + let request = RawCompletionRequest { + model: settings.model.clone(), + prompt, + max_tokens: Some(max_tokens), + temperature: None, + stop: stop_tokens + .into_iter() + .map(std::borrow::Cow::Owned) + .collect(), + environment: None, + }; + + let request_body = serde_json::to_string(&request)?; + let mut http_request_builder = http_client::Request::builder() + .method(http_client::Method::POST) + .uri(settings.api_url.as_ref()) + .header("Content-Type", "application/json"); + + if let Some(api_key) = api_key { + http_request_builder = + http_request_builder.header("Authorization", format!("Bearer {}", api_key)); + } + + let http_request = + http_request_builder.body(http_client::AsyncBody::from(request_body))?; + + let mut response = http_client.send(http_request).await?; + let status = response.status(); + + if !status.is_success() { + let mut body = String::new(); + response.body_mut().read_to_string(&mut body).await?; + anyhow::bail!("custom server error: {} - {}", status, body); + } + + let mut body = String::new(); + response.body_mut().read_to_string(&mut body).await?; + + let parsed: RawCompletionResponse = + serde_json::from_str(&body).context("Failed to parse completion response")?; + let text = parsed + .choices + .into_iter() + .next() + .map(|choice| choice.text) + .unwrap_or_default(); + Ok((text, parsed.id)) + } + } +} diff --git a/crates/edit_prediction/src/zeta.rs b/crates/edit_prediction/src/zeta.rs index f6a786572736908556535b9131c1cf7814a6126f..ccb058e1193eaf2919c286c6e675a907e4af159f 100644 --- a/crates/edit_prediction/src/zeta.rs +++ b/crates/edit_prediction/src/zeta.rs @@ -2,29 +2,30 @@ use crate::cursor_excerpt::compute_excerpt_ranges; use crate::prediction::EditPredictionResult; use crate::{ CurrentEditPrediction, DebugEvent, EditPredictionFinishedDebugEvent, EditPredictionId, - EditPredictionModelInput, EditPredictionStartedDebugEvent, EditPredictionStore, ollama, + EditPredictionModelInput, EditPredictionStartedDebugEvent, EditPredictionStore, }; -use anyhow::{Context as _, Result}; -use cloud_llm_client::predict_edits_v3::{RawCompletionRequest, RawCompletionResponse}; +use anyhow::Result; +use cloud_llm_client::predict_edits_v3::RawCompletionRequest; use cloud_llm_client::{AcceptEditPredictionBody, EditPredictionRejectReason}; use edit_prediction_types::PredictedCursorPosition; -use futures::AsyncReadExt as _; -use gpui::{App, AppContext as _, Task, http_client, prelude::*}; -use language::language_settings::{OpenAiCompatibleEditPredictionSettings, all_language_settings}; +use gpui::{App, AppContext as _, Task, prelude::*}; +use language::language_settings::all_language_settings; use language::{BufferSnapshot, ToOffset as _, ToPoint, text_diff}; use release_channel::AppVersion; use settings::EditPredictionPromptFormat; use text::{Anchor, Bias}; -use std::env; -use std::ops::Range; -use std::{path::Path, sync::Arc, time::Instant}; +use std::{env, ops::Range, path::Path, sync::Arc, time::Instant}; use zeta_prompt::{ CURSOR_MARKER, ZetaFormat, clean_zeta2_model_output, format_zeta_prompt, get_prefill, - prompt_input_contains_special_tokens, + output_with_context_for_format, prompt_input_contains_special_tokens, zeta1::{self, EDITABLE_REGION_END_MARKER}, }; +use crate::open_ai_compatible::{ + load_open_ai_compatible_api_key_if_needed, send_custom_server_request, +}; + pub fn request_prediction_with_zeta( store: &mut EditPredictionStore, EditPredictionModelInput { @@ -56,6 +57,7 @@ pub fn request_prediction_with_zeta( let buffer_snapshotted_at = Instant::now(); let raw_config = store.zeta2_raw_config().cloned(); let preferred_experiment = store.preferred_experiment().map(|s| s.to_owned()); + let open_ai_compatible_api_key = load_open_ai_compatible_api_key_if_needed(provider, cx); let excerpt_path: Arc = snapshot .file() @@ -64,6 +66,11 @@ pub fn request_prediction_with_zeta( let client = store.client.clone(); let llm_token = store.llm_token.clone(); + let organization_id = store + .user_store + .read(cx) + .current_organization() + .map(|organization| organization.id.clone()); let app_version = AppVersion::global(cx); let request_task = cx.background_spawn({ @@ -131,6 +138,7 @@ pub fn request_prediction_with_zeta( prompt, max_tokens, stop_tokens, + open_ai_compatible_api_key.clone(), &http_client, ) .await?; @@ -157,6 +165,7 @@ pub fn request_prediction_with_zeta( prompt, max_tokens, vec![], + open_ai_compatible_api_key.clone(), &http_client, ) .await?; @@ -177,13 +186,17 @@ pub fn request_prediction_with_zeta( let prompt = format_zeta_prompt(&prompt_input, config.format); let prefill = get_prefill(&prompt_input, config.format); let prompt = format!("{prompt}{prefill}"); + let environment = config + .environment + .clone() + .or_else(|| Some(config.format.to_string().to_lowercase())); let request = RawCompletionRequest { model: config.model_id.clone().unwrap_or_default(), prompt, temperature: None, stop: vec![], max_tokens: Some(2048), - environment: Some(config.format.to_string().to_lowercase()), + environment, }; editable_range_in_excerpt = zeta_prompt::excerpt_range_for_format( @@ -197,6 +210,7 @@ pub fn request_prediction_with_zeta( client, None, llm_token, + organization_id, app_version, ) .await?; @@ -215,6 +229,7 @@ pub fn request_prediction_with_zeta( prompt_input.clone(), client, llm_token, + organization_id, app_version, trigger, ) @@ -240,6 +255,25 @@ pub fn request_prediction_with_zeta( return Ok((Some((request_id, None, model_version)), usage)); }; + let editable_range_in_buffer = editable_range_in_excerpt.start + + full_context_offset_range.start + ..editable_range_in_excerpt.end + full_context_offset_range.start; + + let mut old_text = snapshot + .text_for_range(editable_range_in_buffer.clone()) + .collect::(); + + // For the hashline format, the model may return <|set|>/<|insert|> + // edit commands instead of a full replacement. Apply them against + // the original editable region to produce the full replacement text. + // This must happen before cursor marker stripping because the cursor + // marker is embedded inside edit command content. + if let Some(rewritten_output) = + output_with_context_for_format(zeta_version, &old_text, &output_text)? + { + output_text = rewritten_output; + } + // Client-side cursor marker processing (applies to both raw and v3 responses) let cursor_offset_in_output = output_text.find(CURSOR_MARKER); if let Some(offset) = cursor_offset_in_output { @@ -259,14 +293,6 @@ pub fn request_prediction_with_zeta( .ok(); } - let editable_range_in_buffer = editable_range_in_excerpt.start - + full_context_offset_range.start - ..editable_range_in_excerpt.end + full_context_offset_range.start; - - let mut old_text = snapshot - .text_for_range(editable_range_in_buffer.clone()) - .collect::(); - if !output_text.is_empty() && !output_text.ends_with('\n') { output_text.push('\n'); } @@ -400,66 +426,6 @@ pub fn zeta2_prompt_input( (full_context_offset_range, prompt_input) } -pub(crate) async fn send_custom_server_request( - provider: settings::EditPredictionProvider, - settings: &OpenAiCompatibleEditPredictionSettings, - prompt: String, - max_tokens: u32, - stop_tokens: Vec, - http_client: &Arc, -) -> Result<(String, String)> { - match provider { - settings::EditPredictionProvider::Ollama => { - let response = - ollama::make_request(settings.clone(), prompt, stop_tokens, http_client.clone()) - .await?; - Ok((response.response, response.created_at)) - } - _ => { - let request = RawCompletionRequest { - model: settings.model.clone(), - prompt, - max_tokens: Some(max_tokens), - temperature: None, - stop: stop_tokens - .into_iter() - .map(std::borrow::Cow::Owned) - .collect(), - environment: None, - }; - - let request_body = serde_json::to_string(&request)?; - let http_request = http_client::Request::builder() - .method(http_client::Method::POST) - .uri(settings.api_url.as_ref()) - .header("Content-Type", "application/json") - .body(http_client::AsyncBody::from(request_body))?; - - let mut response = http_client.send(http_request).await?; - let status = response.status(); - - if !status.is_success() { - let mut body = String::new(); - response.body_mut().read_to_string(&mut body).await?; - anyhow::bail!("custom server error: {} - {}", status, body); - } - - let mut body = String::new(); - response.body_mut().read_to_string(&mut body).await?; - - let parsed: RawCompletionResponse = - serde_json::from_str(&body).context("Failed to parse completion response")?; - let text = parsed - .choices - .into_iter() - .next() - .map(|choice| choice.text) - .unwrap_or_default(); - Ok((text, parsed.id)) - } - } -} - pub(crate) fn edit_prediction_accepted( store: &EditPredictionStore, current_prediction: CurrentEditPrediction, @@ -475,6 +441,11 @@ pub(crate) fn edit_prediction_accepted( let require_auth = custom_accept_url.is_none(); let client = store.client.clone(); let llm_token = store.llm_token.clone(); + let organization_id = store + .user_store + .read(cx) + .current_organization() + .map(|organization| organization.id.clone()); let app_version = AppVersion::global(cx); cx.background_spawn(async move { @@ -499,6 +470,7 @@ pub(crate) fn edit_prediction_accepted( }, client, llm_token, + organization_id, app_version, require_auth, ) diff --git a/crates/edit_prediction_cli/src/format_prompt.rs b/crates/edit_prediction_cli/src/format_prompt.rs index ecacd963023d7d113ea5ad77b61fd1d88306fc95..f36eaf2799166d6fbd2b7b212003a1a0644b82c4 100644 --- a/crates/edit_prediction_cli/src/format_prompt.rs +++ b/crates/edit_prediction_cli/src/format_prompt.rs @@ -12,7 +12,8 @@ use similar::DiffableStr; use std::ops::Range; use std::sync::Arc; use zeta_prompt::{ - ZetaFormat, excerpt_range_for_format, format_zeta_prompt, resolve_cursor_region, + ZetaFormat, encode_patch_as_output_for_format, excerpt_range_for_format, format_zeta_prompt, + output_end_marker_for_format, resolve_cursor_region, }; pub async fn run_format_prompt( @@ -53,18 +54,22 @@ pub async fn run_format_prompt( let prompt = format_zeta_prompt(prompt_inputs, zeta_format); let prefill = zeta_prompt::get_prefill(prompt_inputs, zeta_format); - let (expected_patch, expected_cursor_offset) = example + let expected_output = example .spec .expected_patches_with_cursor_positions() .into_iter() .next() - .context("expected patches is empty")?; - let expected_output = zeta2_output_for_patch( - prompt_inputs, - &expected_patch, - expected_cursor_offset, - zeta_format, - )?; + .and_then(|(expected_patch, expected_cursor_offset)| { + zeta2_output_for_patch( + prompt_inputs, + &expected_patch, + expected_cursor_offset, + zeta_format, + ) + .ok() + }) + .unwrap_or_default(); + let rejected_output = example.spec.rejected_patch.as_ref().and_then(|patch| { zeta2_output_for_patch(prompt_inputs, patch, None, zeta_format).ok() }); @@ -97,6 +102,12 @@ pub fn zeta2_output_for_patch( old_editable_region.push('\n'); } + if let Some(encoded_output) = + encode_patch_as_output_for_format(version, &old_editable_region, patch, cursor_offset)? + { + return Ok(encoded_output); + } + let (mut result, first_hunk_offset) = udiff::apply_diff_to_string_with_hunk_offset(patch, &old_editable_region).with_context( || { @@ -116,16 +127,11 @@ pub fn zeta2_output_for_patch( result.insert_str(offset, zeta_prompt::CURSOR_MARKER); } - match version { - ZetaFormat::V0120GitMergeMarkers - | ZetaFormat::V0131GitMergeMarkersPrefix - | ZetaFormat::V0211SeedCoder => { - if !result.ends_with('\n') { - result.push('\n'); - } - result.push_str(zeta_prompt::v0120_git_merge_markers::END_MARKER); + if let Some(end_marker) = output_end_marker_for_format(version) { + if !result.ends_with('\n') { + result.push('\n'); } - _ => (), + result.push_str(end_marker); } Ok(result) diff --git a/crates/edit_prediction_cli/src/main.rs b/crates/edit_prediction_cli/src/main.rs index 207a69328fb07277c39463c0c6a460862c95fe42..8bb4b2a8e2f50d448fc314a70e2fc94cfa2c3d71 100644 --- a/crates/edit_prediction_cli/src/main.rs +++ b/crates/edit_prediction_cli/src/main.rs @@ -358,6 +358,7 @@ enum PredictionProvider { Mercury, Zeta1, Zeta2(ZetaFormat), + Baseten(ZetaFormat), Teacher(TeacherBackend), TeacherNonBatching(TeacherBackend), Repair, @@ -376,6 +377,7 @@ impl std::fmt::Display for PredictionProvider { PredictionProvider::Mercury => write!(f, "mercury"), PredictionProvider::Zeta1 => write!(f, "zeta1"), PredictionProvider::Zeta2(format) => write!(f, "zeta2:{format}"), + PredictionProvider::Baseten(format) => write!(f, "baseten:{format}"), PredictionProvider::Teacher(backend) => write!(f, "teacher:{backend}"), PredictionProvider::TeacherNonBatching(backend) => { write!(f, "teacher-non-batching:{backend}") @@ -415,6 +417,13 @@ impl std::str::FromStr for PredictionProvider { Ok(PredictionProvider::TeacherNonBatching(backend)) } "repair" => Ok(PredictionProvider::Repair), + "baseten" => { + let format = arg + .map(ZetaFormat::parse) + .transpose()? + .unwrap_or(ZetaFormat::default()); + Ok(PredictionProvider::Baseten(format)) + } _ => { anyhow::bail!( "unknown provider `{provider}`. Valid options: sweep, mercury, zeta1, zeta2, zeta2:, teacher, teacher:, teacher-non-batching, repair\n\ diff --git a/crates/edit_prediction_cli/src/parse_output.rs b/crates/edit_prediction_cli/src/parse_output.rs index 4b8af44785c1781de772f569c012ee64eee48aad..2c066b8b32b3eaab54ad6e3b3bcb0796ff27f950 100644 --- a/crates/edit_prediction_cli/src/parse_output.rs +++ b/crates/edit_prediction_cli/src/parse_output.rs @@ -6,7 +6,11 @@ use crate::{ }; use anyhow::{Context as _, Result}; use edit_prediction::example_spec::encode_cursor_in_patch; -use zeta_prompt::{CURSOR_MARKER, ZetaFormat}; +use zeta_prompt::{ + CURSOR_MARKER, ZetaFormat, clean_extracted_region_for_format, + current_region_markers_for_format, output_end_marker_for_format, + output_with_context_for_format, +}; pub fn run_parse_output(example: &mut Example) -> Result<()> { example @@ -51,22 +55,7 @@ pub fn parse_prediction_output( } fn extract_zeta2_current_region(prompt: &str, format: ZetaFormat) -> Result { - let (current_marker, end_marker) = match format { - ZetaFormat::V0112MiddleAtEnd => ("<|fim_middle|>current\n", "<|fim_middle|>updated"), - ZetaFormat::V0113Ordered | ZetaFormat::V0114180EditableRegion => { - ("<|fim_middle|>current\n", "<|fim_suffix|>") - } - ZetaFormat::V0120GitMergeMarkers - | ZetaFormat::V0131GitMergeMarkersPrefix - | ZetaFormat::V0211Prefill => ( - zeta_prompt::v0120_git_merge_markers::START_MARKER, - zeta_prompt::v0120_git_merge_markers::SEPARATOR, - ), - ZetaFormat::V0211SeedCoder => ( - zeta_prompt::seed_coder::START_MARKER, - zeta_prompt::seed_coder::SEPARATOR, - ), - }; + let (current_marker, end_marker) = current_region_markers_for_format(format); let start = prompt.find(current_marker).with_context(|| { format!( @@ -82,8 +71,7 @@ fn extract_zeta2_current_region(prompt: &str, format: ZetaFormat) -> Result { - zeta_prompt::v0131_git_merge_markers_prefix::END_MARKER - } - ZetaFormat::V0120GitMergeMarkers => zeta_prompt::v0120_git_merge_markers::END_MARKER, - ZetaFormat::V0112MiddleAtEnd - | ZetaFormat::V0113Ordered - | ZetaFormat::V0114180EditableRegion => "", - ZetaFormat::V0211SeedCoder => zeta_prompt::seed_coder::END_MARKER, - }; - if !suffix.is_empty() { + if let Some(marker) = output_end_marker_for_format(format) { new_text = new_text - .strip_suffix(suffix) + .strip_suffix(marker) .unwrap_or(&new_text) .to_string(); } diff --git a/crates/edit_prediction_cli/src/predict.rs b/crates/edit_prediction_cli/src/predict.rs index 02ba24b8a4f2627b9542254e3d118981737f8318..bd89d54ab37521ecb9661b6f1bb0156f30ba1acb 100644 --- a/crates/edit_prediction_cli/src/predict.rs +++ b/crates/edit_prediction_cli/src/predict.rs @@ -6,14 +6,18 @@ use crate::{ headless::EpAppState, load_project::run_load_project, openai_client::OpenAiClient, + parse_output::parse_prediction_output, paths::{LATEST_EXAMPLE_RUN_DIR, RUN_DIR}, - progress::{ExampleProgress, InfoStyle, Step}, + progress::{ExampleProgress, InfoStyle, Step, StepProgress}, retrieve_context::run_context_retrieval, }; use anyhow::Context as _; +use cloud_llm_client::predict_edits_v3::{RawCompletionRequest, RawCompletionResponse}; use edit_prediction::{DebugEvent, EditPredictionStore, Zeta2RawConfig}; -use futures::{FutureExt as _, StreamExt as _, future::Shared}; +use futures::{AsyncReadExt as _, FutureExt as _, StreamExt as _, future::Shared}; use gpui::{AppContext as _, AsyncApp, Task}; +use http_client::{AsyncBody, HttpClient, Method}; +use reqwest_client::ReqwestClient; use std::{ fs, sync::{ @@ -79,6 +83,22 @@ pub async fn run_prediction( .await; } + if let PredictionProvider::Baseten(format) = provider { + run_format_prompt( + example, + &FormatPromptArgs { + provider: PredictionProvider::Zeta2(format), + }, + app_state.clone(), + example_progress, + cx, + ) + .await?; + + let step_progress = example_progress.start(Step::Predict); + return predict_baseten(example, format, &step_progress).await; + } + run_load_project(example, app_state.clone(), example_progress, cx.clone()).await?; run_context_retrieval(example, app_state.clone(), example_progress, cx.clone()).await?; @@ -116,7 +136,8 @@ pub async fn run_prediction( PredictionProvider::Mercury => edit_prediction::EditPredictionModel::Mercury, PredictionProvider::Teacher(..) | PredictionProvider::TeacherNonBatching(..) - | PredictionProvider::Repair => { + | PredictionProvider::Repair + | PredictionProvider::Baseten(_) => { unreachable!() } }; @@ -127,7 +148,12 @@ pub async fn run_prediction( if let PredictionProvider::Zeta2(format) = provider { if format != ZetaFormat::default() { let model_id = std::env::var("ZED_ZETA_MODEL").ok(); - store.set_zeta2_raw_config(Zeta2RawConfig { model_id, format }); + let environment = std::env::var("ZED_ZETA_ENVIRONMENT").ok(); + store.set_zeta2_raw_config(Zeta2RawConfig { + model_id, + environment, + format, + }); } } }); @@ -480,6 +506,89 @@ async fn predict_openai( Ok(()) } +pub async fn predict_baseten( + example: &mut Example, + format: ZetaFormat, + step_progress: &StepProgress, +) -> anyhow::Result<()> { + let model_id = + std::env::var("ZED_ZETA_MODEL").context("ZED_ZETA_MODEL environment variable required")?; + + let api_key = + std::env::var("BASETEN_API_KEY").context("BASETEN_API_KEY environment variable not set")?; + + let prompt = example.prompt.as_ref().context("Prompt is required")?; + let prompt_text = prompt.input.clone(); + let prefill = prompt.prefill.clone().unwrap_or_default(); + + step_progress.set_substatus("running prediction via baseten"); + + let environment: String = <&'static str>::from(&format).to_lowercase(); + let url = format!( + "https://model-{model_id}.api.baseten.co/environments/{environment}/sync/v1/completions" + ); + + let request_body = RawCompletionRequest { + model: model_id, + prompt: prompt_text.clone(), + max_tokens: Some(2048), + temperature: Some(0.), + stop: vec![], + environment: None, + }; + + let body_bytes = + serde_json::to_vec(&request_body).context("Failed to serialize request body")?; + + let http_client: Arc = Arc::new(ReqwestClient::new()); + let request = http_client::Request::builder() + .method(Method::POST) + .uri(&url) + .header("Content-Type", "application/json") + .header("Authorization", format!("Api-Key {api_key}")) + .body(AsyncBody::from(body_bytes))?; + + let mut response = http_client.send(request).await?; + let status = response.status(); + + let mut body = String::new(); + response + .body_mut() + .read_to_string(&mut body) + .await + .context("Failed to read Baseten response body")?; + + if !status.is_success() { + anyhow::bail!("Baseten API returned {status}: {body}"); + } + + let completion: RawCompletionResponse = + serde_json::from_str(&body).context("Failed to parse Baseten response")?; + + let actual_output = completion + .choices + .into_iter() + .next() + .map(|choice| choice.text) + .unwrap_or_default(); + + let actual_output = format!("{prefill}{actual_output}"); + + let (actual_patch, actual_cursor) = + parse_prediction_output(example, &actual_output, PredictionProvider::Zeta2(format))?; + + let prediction = ExamplePrediction { + actual_patch: Some(actual_patch), + actual_output, + actual_cursor, + error: None, + provider: PredictionProvider::Baseten(format), + }; + + example.predictions.push(prediction); + Ok(()) +} + pub async fn sync_batches(provider: Option<&PredictionProvider>) -> anyhow::Result<()> { match provider { Some(PredictionProvider::Teacher(backend)) => match backend { diff --git a/crates/edit_prediction_cli/src/pull_examples.rs b/crates/edit_prediction_cli/src/pull_examples.rs index 2f371675b29015795beef550ce5e3956c63751f9..cccd351dcdeda0dbf059d851a44b02bc1e558654 100644 --- a/crates/edit_prediction_cli/src/pull_examples.rs +++ b/crates/edit_prediction_cli/src/pull_examples.rs @@ -34,7 +34,7 @@ pub struct MinCaptureVersion { pub patch: u32, } -const DEFAULT_STATEMENT_TIMEOUT_SECONDS: u64 = 120; +const DEFAULT_STATEMENT_TIMEOUT_SECONDS: u64 = 240; const SETTLED_STATEMENT_TIMEOUT_SECONDS: u64 = 240; pub(crate) const POLL_INTERVAL: Duration = Duration::from_secs(2); pub(crate) const MAX_POLL_ATTEMPTS: usize = 120; @@ -715,7 +715,7 @@ pub async fn fetch_rated_examples_after( AND rated.event_properties:inputs IS NOT NULL AND rated.event_properties:inputs:cursor_excerpt IS NOT NULL AND rated.event_properties:output IS NOT NULL - AND rated.event_properties:can_collect_data = true + AND rated.event_properties:inputs:can_collect_data = true ORDER BY rated.time ASC LIMIT ? OFFSET ? @@ -823,11 +823,11 @@ fn rated_examples_from_response<'a>( let environment = get_string("environment"); let zed_version = get_string("zed_version"); - match (inputs, output.clone(), rating.clone(), device_id.clone(), time.clone()) { - (Some(inputs), Some(output), Some(rating), Some(device_id), Some(time)) => { + match (inputs, output.clone(), rating.clone(), time.clone()) { + (Some(inputs), Some(output), Some(rating), Some(time)) => { Some(build_rated_example( request_id, - device_id, + device_id.unwrap_or_default(), time, inputs, output, @@ -840,11 +840,10 @@ fn rated_examples_from_response<'a>( } _ => { log::warn!( - "skipping row {row_index}: missing fields - inputs={:?} output={:?} rating={:?} device_id={:?} time={:?}", + "skipping row {row_index}: missing fields - inputs={:?} output={:?} rating={:?} time={:?}", inputs_json.is_some(), output.is_some(), rating.is_some(), - device_id.is_some(), time.is_some(), ); None diff --git a/crates/edit_prediction_ui/src/edit_prediction_button.rs b/crates/edit_prediction_ui/src/edit_prediction_button.rs index 6339c7d6cd9fa1cc40101cc1bf14650a6904b3c7..b00a229164d480d38312ca97cac31a23010f8b69 100644 --- a/crates/edit_prediction_ui/src/edit_prediction_button.rs +++ b/crates/edit_prediction_ui/src/edit_prediction_button.rs @@ -3,7 +3,7 @@ use client::{Client, UserStore, zed_urls}; use cloud_llm_client::UsageLimit; use codestral::{self, CodestralEditPredictionDelegate}; use copilot::Status; -use edit_prediction::{EditPredictionStore, Zeta2FeatureFlag}; +use edit_prediction::EditPredictionStore; use edit_prediction_types::EditPredictionDelegateHandle; use editor::{ Editor, MultiBufferOffset, SelectionEffects, actions::ShowEditPrediction, scroll::Autoscroll, @@ -22,9 +22,7 @@ use language::{ }; use project::{DisableAiSettings, Project}; use regex::Regex; -use settings::{ - EXPERIMENTAL_ZETA2_EDIT_PREDICTION_PROVIDER_NAME, Settings, SettingsStore, update_settings_file, -}; +use settings::{Settings, SettingsStore, update_settings_file}; use std::{ rc::Rc, sync::{Arc, LazyLock}, @@ -539,9 +537,15 @@ impl EditPredictionButton { edit_prediction::ollama::ensure_authenticated(cx); let sweep_api_token_task = edit_prediction::sweep_ai::load_sweep_api_token(cx); let mercury_api_token_task = edit_prediction::mercury::load_mercury_api_token(cx); + let open_ai_compatible_api_token_task = + edit_prediction::open_ai_compatible::load_open_ai_compatible_api_token(cx); cx.spawn(async move |this, cx| { - _ = futures::join!(sweep_api_token_task, mercury_api_token_task); + _ = futures::join!( + sweep_api_token_task, + mercury_api_token_task, + open_ai_compatible_api_token_task + ); this.update(cx, |_, cx| { cx.notify(); }) @@ -770,13 +774,7 @@ impl EditPredictionButton { menu = menu.separator().header("Privacy"); - if matches!( - provider, - EditPredictionProvider::Zed - | EditPredictionProvider::Experimental( - EXPERIMENTAL_ZETA2_EDIT_PREDICTION_PROVIDER_NAME, - ) - ) { + if matches!(provider, EditPredictionProvider::Zed) { if let Some(provider) = &self.edit_prediction_provider { let data_collection = provider.data_collection_state(cx); @@ -1399,12 +1397,6 @@ pub fn get_available_providers(cx: &mut App) -> Vec { providers.push(EditPredictionProvider::Zed); - if cx.has_flag::() { - providers.push(EditPredictionProvider::Experimental( - EXPERIMENTAL_ZETA2_EDIT_PREDICTION_PROVIDER_NAME, - )); - } - if let Some(app_state) = workspace::AppState::global(cx).upgrade() && copilot::GlobalCopilotAuth::try_get_or_init(app_state, cx) .is_some_and(|copilot| copilot.0.read(cx).is_authenticated()) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index b666557b90a3c1181404d8f09b1d50ff9f8402a9..00a48a9ab3d249850b9749d64267d8274e7eaa79 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -1006,11 +1006,6 @@ impl DisplayMap { &self.block_map.folded_buffers } - #[instrument(skip_all)] - pub(super) fn clear_folded_buffer(&mut self, buffer_id: language::BufferId) { - self.block_map.folded_buffers.remove(&buffer_id); - } - #[instrument(skip_all)] pub fn insert_creases( &mut self, @@ -1924,6 +1919,9 @@ impl DisplaySnapshot { color } }), + underline: chunk_highlight + .underline + .filter(|_| editor_style.show_underlines), ..chunk_highlight } }); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 5504305f86eb95dee000cec4099e366bbf86ffef..0d1238da21695738e4f6cedc54e172ad456c9bd6 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -24147,9 +24147,13 @@ impl Editor { self.display_map.update(cx, |display_map, cx| { display_map.invalidate_semantic_highlights(*buffer_id); display_map.clear_lsp_folding_ranges(*buffer_id, cx); - display_map.clear_folded_buffer(*buffer_id); }); } + + self.display_map.update(cx, |display_map, cx| { + display_map.unfold_buffers(removed_buffer_ids.iter().copied(), cx); + }); + jsx_tag_auto_close::refresh_enabled_in_any_buffer(self, multibuffer, cx); cx.emit(EditorEvent::ExcerptsRemoved { ids: ids.clone(), diff --git a/crates/fs/Cargo.toml b/crates/fs/Cargo.toml index 6355524e4f328df0ca7fcf24c1df0557676ba6a6..04cae2dd2ad18f85a7c2ed663c1c3482febb22d3 100644 --- a/crates/fs/Cargo.toml +++ b/crates/fs/Cargo.toml @@ -58,4 +58,4 @@ gpui = { workspace = true, features = ["test-support"] } git = { workspace = true, features = ["test-support"] } [features] -test-support = ["gpui/test-support", "git/test-support"] +test-support = ["gpui/test-support", "git/test-support", "util/test-support"] diff --git a/crates/fs/src/fake_git_repo.rs b/crates/fs/src/fake_git_repo.rs index 12cd67cdae1a250d07468047617c8cc7a52737fa..06ebea9157f97a0323297cd3ae142c4b306fe4ef 100644 --- a/crates/fs/src/fake_git_repo.rs +++ b/crates/fs/src/fake_git_repo.rs @@ -20,7 +20,7 @@ use ignore::gitignore::GitignoreBuilder; use parking_lot::Mutex; use rope::Rope; use smol::{channel::Sender, future::FutureExt as _}; -use std::{path::PathBuf, sync::Arc}; +use std::{path::PathBuf, sync::Arc, sync::atomic::AtomicBool}; use text::LineEnding; use util::{paths::PathStyle, rel_path::RelPath}; @@ -32,6 +32,7 @@ pub struct FakeGitRepository { pub(crate) dot_git_path: PathBuf, pub(crate) repository_dir_path: PathBuf, pub(crate) common_dir_path: PathBuf, + pub(crate) is_trusted: Arc, } #[derive(Debug, Clone)] @@ -406,7 +407,31 @@ impl GitRepository for FakeGitRepository { } fn worktrees(&self) -> BoxFuture<'_, Result>> { - self.with_state_async(false, |state| Ok(state.worktrees.clone())) + let dot_git_path = self.dot_git_path.clone(); + self.with_state_async(false, move |state| { + let work_dir = dot_git_path + .parent() + .map(PathBuf::from) + .unwrap_or(dot_git_path); + let head_sha = state + .refs + .get("HEAD") + .cloned() + .unwrap_or_else(|| "0000000".to_string()); + let branch_ref = state + .current_branch_name + .as_ref() + .map(|name| format!("refs/heads/{name}")) + .unwrap_or_else(|| "refs/heads/main".to_string()); + let main_worktree = Worktree { + path: work_dir, + ref_name: branch_ref.into(), + sha: head_sha.into(), + }; + let mut all = vec![main_worktree]; + all.extend(state.worktrees.iter().cloned()); + Ok(all) + }) } fn create_worktree( @@ -1011,146 +1036,13 @@ impl GitRepository for FakeGitRepository { fn commit_data_reader(&self) -> Result { anyhow::bail!("commit_data_reader not supported for FakeGitRepository") } -} -#[cfg(test)] -mod tests { - use super::*; - use crate::{FakeFs, Fs}; - use gpui::TestAppContext; - use serde_json::json; - use std::path::Path; - - #[gpui::test] - async fn test_fake_worktree_lifecycle(cx: &mut TestAppContext) { - let worktree_dir_settings = &["../worktrees", ".git/zed-worktrees", "my-worktrees/"]; - - for worktree_dir_setting in worktree_dir_settings { - let fs = FakeFs::new(cx.executor()); - fs.insert_tree("/project", json!({".git": {}, "file.txt": "content"})) - .await; - let repo = fs - .open_repo(Path::new("/project/.git"), None) - .expect("should open fake repo"); - - // Initially no worktrees - let worktrees = repo.worktrees().await.unwrap(); - assert!(worktrees.is_empty()); - - let expected_dir = git::repository::resolve_worktree_directory( - Path::new("/project"), - worktree_dir_setting, - ); - - // Create a worktree - repo.create_worktree( - "feature-branch".to_string(), - expected_dir.clone(), - Some("abc123".to_string()), - ) - .await - .unwrap(); - - // List worktrees — should have one - let worktrees = repo.worktrees().await.unwrap(); - assert_eq!(worktrees.len(), 1); - assert_eq!( - worktrees[0].path, - expected_dir.join("feature-branch"), - "failed for worktree_directory setting: {worktree_dir_setting:?}" - ); - assert_eq!(worktrees[0].ref_name.as_ref(), "refs/heads/feature-branch"); - assert_eq!(worktrees[0].sha.as_ref(), "abc123"); - - // Directory should exist in FakeFs after create - assert!( - fs.is_dir(&expected_dir.join("feature-branch")).await, - "worktree directory should be created in FakeFs for setting {worktree_dir_setting:?}" - ); - - // Create a second worktree (without explicit commit) - repo.create_worktree("bugfix-branch".to_string(), expected_dir.clone(), None) - .await - .unwrap(); - - let worktrees = repo.worktrees().await.unwrap(); - assert_eq!(worktrees.len(), 2); - assert!( - fs.is_dir(&expected_dir.join("bugfix-branch")).await, - "second worktree directory should be created in FakeFs for setting {worktree_dir_setting:?}" - ); - - // Rename the first worktree - repo.rename_worktree( - expected_dir.join("feature-branch"), - expected_dir.join("renamed-branch"), - ) - .await - .unwrap(); + fn set_trusted(&self, trusted: bool) { + self.is_trusted + .store(trusted, std::sync::atomic::Ordering::Release); + } - let worktrees = repo.worktrees().await.unwrap(); - assert_eq!(worktrees.len(), 2); - assert!( - worktrees - .iter() - .any(|w| w.path == expected_dir.join("renamed-branch")), - "renamed worktree should exist at new path for setting {worktree_dir_setting:?}" - ); - assert!( - worktrees - .iter() - .all(|w| w.path != expected_dir.join("feature-branch")), - "old path should no longer exist for setting {worktree_dir_setting:?}" - ); - - // Directory should be moved in FakeFs after rename - assert!( - !fs.is_dir(&expected_dir.join("feature-branch")).await, - "old worktree directory should not exist after rename for setting {worktree_dir_setting:?}" - ); - assert!( - fs.is_dir(&expected_dir.join("renamed-branch")).await, - "new worktree directory should exist after rename for setting {worktree_dir_setting:?}" - ); - - // Rename a nonexistent worktree should fail - let result = repo - .rename_worktree(PathBuf::from("/nonexistent"), PathBuf::from("/somewhere")) - .await; - assert!(result.is_err()); - - // Remove a worktree - repo.remove_worktree(expected_dir.join("renamed-branch"), false) - .await - .unwrap(); - - let worktrees = repo.worktrees().await.unwrap(); - assert_eq!(worktrees.len(), 1); - assert_eq!(worktrees[0].path, expected_dir.join("bugfix-branch")); - - // Directory should be removed from FakeFs after remove - assert!( - !fs.is_dir(&expected_dir.join("renamed-branch")).await, - "worktree directory should be removed from FakeFs for setting {worktree_dir_setting:?}" - ); - - // Remove a nonexistent worktree should fail - let result = repo - .remove_worktree(PathBuf::from("/nonexistent"), false) - .await; - assert!(result.is_err()); - - // Remove the last worktree - repo.remove_worktree(expected_dir.join("bugfix-branch"), false) - .await - .unwrap(); - - let worktrees = repo.worktrees().await.unwrap(); - assert!(worktrees.is_empty()); - assert!( - !fs.is_dir(&expected_dir.join("bugfix-branch")).await, - "last worktree directory should be removed from FakeFs for setting {worktree_dir_setting:?}" - ); - } + fn is_trusted(&self) -> bool { + self.is_trusted.load(std::sync::atomic::Ordering::Acquire) } } diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 2db9e48a2e77bdb3e49fce0b16ea9b67ffaacbc0..0fde444171042eda859edcac7915c456ab91e265 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -2776,6 +2776,7 @@ impl Fs for FakeFs { repository_dir_path: repository_dir_path.to_owned(), common_dir_path: common_dir_path.to_owned(), checkpoints: Arc::default(), + is_trusted: Arc::default(), }) as _ }, ) diff --git a/crates/fs/tests/integration/fake_git_repo.rs b/crates/fs/tests/integration/fake_git_repo.rs index 36dfcaf168b4f0190c5c49bf4798fac7bc9bd37b..bae7f2fc94dd5161793f85f64cc0a1448a187134 100644 --- a/crates/fs/tests/integration/fake_git_repo.rs +++ b/crates/fs/tests/integration/fake_git_repo.rs @@ -1,9 +1,146 @@ use fs::{FakeFs, Fs}; -use gpui::BackgroundExecutor; +use gpui::{BackgroundExecutor, TestAppContext}; use serde_json::json; -use std::path::Path; +use std::path::{Path, PathBuf}; use util::path; +#[gpui::test] +async fn test_fake_worktree_lifecycle(cx: &mut TestAppContext) { + let worktree_dir_settings = &["../worktrees", ".git/zed-worktrees", "my-worktrees/"]; + + for worktree_dir_setting in worktree_dir_settings { + let fs = FakeFs::new(cx.executor()); + fs.insert_tree("/project", json!({".git": {}, "file.txt": "content"})) + .await; + let repo = fs + .open_repo(Path::new("/project/.git"), None) + .expect("should open fake repo"); + + // Initially only the main worktree exists + let worktrees = repo.worktrees().await.unwrap(); + assert_eq!(worktrees.len(), 1); + assert_eq!(worktrees[0].path, PathBuf::from("/project")); + + let expected_dir = git::repository::resolve_worktree_directory( + Path::new("/project"), + worktree_dir_setting, + ); + + // Create a worktree + repo.create_worktree( + "feature-branch".to_string(), + expected_dir.clone(), + Some("abc123".to_string()), + ) + .await + .unwrap(); + + // List worktrees — should have main + one created + let worktrees = repo.worktrees().await.unwrap(); + assert_eq!(worktrees.len(), 2); + assert_eq!(worktrees[0].path, PathBuf::from("/project")); + assert_eq!( + worktrees[1].path, + expected_dir.join("feature-branch"), + "failed for worktree_directory setting: {worktree_dir_setting:?}" + ); + assert_eq!(worktrees[1].ref_name.as_ref(), "refs/heads/feature-branch"); + assert_eq!(worktrees[1].sha.as_ref(), "abc123"); + + // Directory should exist in FakeFs after create + assert!( + fs.is_dir(&expected_dir.join("feature-branch")).await, + "worktree directory should be created in FakeFs for setting {worktree_dir_setting:?}" + ); + + // Create a second worktree (without explicit commit) + repo.create_worktree("bugfix-branch".to_string(), expected_dir.clone(), None) + .await + .unwrap(); + + let worktrees = repo.worktrees().await.unwrap(); + assert_eq!(worktrees.len(), 3); + assert!( + fs.is_dir(&expected_dir.join("bugfix-branch")).await, + "second worktree directory should be created in FakeFs for setting {worktree_dir_setting:?}" + ); + + // Rename the first worktree + repo.rename_worktree( + expected_dir.join("feature-branch"), + expected_dir.join("renamed-branch"), + ) + .await + .unwrap(); + + let worktrees = repo.worktrees().await.unwrap(); + assert_eq!(worktrees.len(), 3); + assert!( + worktrees + .iter() + .any(|w| w.path == expected_dir.join("renamed-branch")), + "renamed worktree should exist at new path for setting {worktree_dir_setting:?}" + ); + assert!( + worktrees + .iter() + .all(|w| w.path != expected_dir.join("feature-branch")), + "old path should no longer exist for setting {worktree_dir_setting:?}" + ); + + // Directory should be moved in FakeFs after rename + assert!( + !fs.is_dir(&expected_dir.join("feature-branch")).await, + "old worktree directory should not exist after rename for setting {worktree_dir_setting:?}" + ); + assert!( + fs.is_dir(&expected_dir.join("renamed-branch")).await, + "new worktree directory should exist after rename for setting {worktree_dir_setting:?}" + ); + + // Rename a nonexistent worktree should fail + let result = repo + .rename_worktree(PathBuf::from("/nonexistent"), PathBuf::from("/somewhere")) + .await; + assert!(result.is_err()); + + // Remove a worktree + repo.remove_worktree(expected_dir.join("renamed-branch"), false) + .await + .unwrap(); + + let worktrees = repo.worktrees().await.unwrap(); + assert_eq!(worktrees.len(), 2); + assert_eq!(worktrees[0].path, PathBuf::from("/project")); + assert_eq!(worktrees[1].path, expected_dir.join("bugfix-branch")); + + // Directory should be removed from FakeFs after remove + assert!( + !fs.is_dir(&expected_dir.join("renamed-branch")).await, + "worktree directory should be removed from FakeFs for setting {worktree_dir_setting:?}" + ); + + // Remove a nonexistent worktree should fail + let result = repo + .remove_worktree(PathBuf::from("/nonexistent"), false) + .await; + assert!(result.is_err()); + + // Remove the last worktree + repo.remove_worktree(expected_dir.join("bugfix-branch"), false) + .await + .unwrap(); + + let worktrees = repo.worktrees().await.unwrap(); + assert_eq!(worktrees.len(), 1); + assert_eq!(worktrees[0].path, PathBuf::from("/project")); + assert!( + !fs.is_dir(&expected_dir.join("bugfix-branch")).await, + "last worktree directory should be removed from FakeFs for setting {worktree_dir_setting:?}" + ); + } +} + #[gpui::test] async fn test_checkpoints(executor: BackgroundExecutor) { let fs = FakeFs::new(executor); diff --git a/crates/git/clippy.toml b/crates/git/clippy.toml new file mode 100644 index 0000000000000000000000000000000000000000..fb3926840493fd5981c1861e7cea96bd54b9647f --- /dev/null +++ b/crates/git/clippy.toml @@ -0,0 +1,28 @@ +allow-private-module-inception = true +avoid-breaking-exported-api = false +ignore-interior-mutability = [ + # Suppresses clippy::mutable_key_type, which is a false positive as the Eq + # and Hash impls do not use fields with interior mutability. + "agent_ui::context::AgentContextKey" +] +disallowed-methods = [ + { path = "std::process::Command::spawn", reason = "Spawning `std::process::Command` can block the current thread for an unknown duration", replacement = "smol::process::Command::spawn" }, + { path = "std::process::Command::output", reason = "Spawning `std::process::Command` can block the current thread for an unknown duration", replacement = "smol::process::Command::output" }, + { path = "std::process::Command::status", reason = "Spawning `std::process::Command` can block the current thread for an unknown duration", replacement = "smol::process::Command::status" }, + { path = "std::process::Command::stdin", reason = "`smol::process::Command::from()` does not preserve stdio configuration", replacement = "smol::process::Command::stdin" }, + { path = "std::process::Command::stdout", reason = "`smol::process::Command::from()` does not preserve stdio configuration", replacement = "smol::process::Command::stdout" }, + { path = "std::process::Command::stderr", reason = "`smol::process::Command::from()` does not preserve stdio configuration", replacement = "smol::process::Command::stderr" }, + { path = "smol::Timer::after", reason = "smol::Timer introduces non-determinism in tests", replacement = "gpui::BackgroundExecutor::timer" }, + { path = "serde_json::from_reader", reason = "Parsing from a buffer is much slower than first reading the buffer into a Vec/String, see https://github.com/serde-rs/json/issues/160#issuecomment-253446892. Use `serde_json::from_slice` instead." }, + { path = "serde_json_lenient::from_reader", reason = "Parsing from a buffer is much slower than first reading the buffer into a Vec/String, see https://github.com/serde-rs/json/issues/160#issuecomment-253446892, Use `serde_json_lenient::from_slice` instead." }, + { path = "cocoa::foundation::NSString::alloc", reason = "NSString must be autoreleased to avoid memory leaks. Use `ns_string()` helper instead." }, + { path = "smol::process::Command::new", reason = "Git commands must go through `GitBinary::build_command` to ensure security flags like `-c core.fsmonitor=false` are always applied.", replacement = "GitBinary::build_command" }, + { path = "util::command::new_command", reason = "Git commands must go through `GitBinary::build_command` to ensure security flags like `-c core.fsmonitor=false` are always applied.", replacement = "GitBinary::build_command" }, + { path = "util::command::Command::new", reason = "Git commands must go through `GitBinary::build_command` to ensure security flags like `-c core.fsmonitor=false` are always applied.", replacement = "GitBinary::build_command" }, +] +disallowed-types = [ + # { path = "std::collections::HashMap", replacement = "collections::HashMap" }, + # { path = "std::collections::HashSet", replacement = "collections::HashSet" }, + # { path = "indexmap::IndexSet", replacement = "collections::IndexSet" }, + # { path = "indexmap::IndexMap", replacement = "collections::IndexMap" }, +] \ No newline at end of file diff --git a/crates/git/src/blame.rs b/crates/git/src/blame.rs index 9dc184bf2ac253c8bc24f6203f13d6654ac2b64b..c44aea74051bb7c190a091703d6c60807fc4e27e 100644 --- a/crates/git/src/blame.rs +++ b/crates/git/src/blame.rs @@ -1,11 +1,11 @@ use crate::Oid; use crate::commit::get_messages; -use crate::repository::RepoPath; +use crate::repository::{GitBinary, RepoPath}; use anyhow::{Context as _, Result}; use collections::{HashMap, HashSet}; use futures::AsyncWriteExt; use serde::{Deserialize, Serialize}; -use std::{ops::Range, path::Path}; +use std::ops::Range; use text::{LineEnding, Rope}; use time::OffsetDateTime; use time::UtcOffset; @@ -21,15 +21,13 @@ pub struct Blame { } impl Blame { - pub async fn for_path( - git_binary: &Path, - working_directory: &Path, + pub(crate) async fn for_path( + git: &GitBinary, path: &RepoPath, content: &Rope, line_ending: LineEnding, ) -> Result { - let output = - run_git_blame(git_binary, working_directory, path, content, line_ending).await?; + let output = run_git_blame(git, path, content, line_ending).await?; let mut entries = parse_git_blame(&output)?; entries.sort_unstable_by(|a, b| a.range.start.cmp(&b.range.start)); @@ -40,7 +38,7 @@ impl Blame { } let shas = unique_shas.into_iter().collect::>(); - let messages = get_messages(working_directory, &shas) + let messages = get_messages(git, &shas) .await .context("failed to get commit messages")?; @@ -52,8 +50,7 @@ const GIT_BLAME_NO_COMMIT_ERROR: &str = "fatal: no such ref: HEAD"; const GIT_BLAME_NO_PATH: &str = "fatal: no such path"; async fn run_git_blame( - git_binary: &Path, - working_directory: &Path, + git: &GitBinary, path: &RepoPath, contents: &Rope, line_ending: LineEnding, @@ -61,12 +58,7 @@ async fn run_git_blame( let mut child = { let span = ztracing::debug_span!("spawning git-blame command", path = path.as_unix_str()); let _enter = span.enter(); - util::command::new_command(git_binary) - .current_dir(working_directory) - .arg("blame") - .arg("--incremental") - .arg("--contents") - .arg("-") + git.build_command(["blame", "--incremental", "--contents", "-"]) .arg(path.as_unix_str()) .stdin(Stdio::piped()) .stdout(Stdio::piped()) diff --git a/crates/git/src/commit.rs b/crates/git/src/commit.rs index 3f3526afc4ba8fa146592684a6d3acfc44ce7e73..46e050ce155fc049a670fdfa26101eb729b34352 100644 --- a/crates/git/src/commit.rs +++ b/crates/git/src/commit.rs @@ -1,11 +1,11 @@ use crate::{ BuildCommitPermalinkParams, GitHostingProviderRegistry, GitRemote, Oid, parse_git_remote_url, - status::StatusCode, + repository::GitBinary, status::StatusCode, }; use anyhow::{Context as _, Result}; use collections::HashMap; use gpui::SharedString; -use std::{path::Path, sync::Arc}; +use std::sync::Arc; #[derive(Clone, Debug, Default)] pub struct ParsedCommitMessage { @@ -48,7 +48,7 @@ impl ParsedCommitMessage { } } -pub async fn get_messages(working_directory: &Path, shas: &[Oid]) -> Result> { +pub(crate) async fn get_messages(git: &GitBinary, shas: &[Oid]) -> Result> { if shas.is_empty() { return Ok(HashMap::default()); } @@ -63,12 +63,12 @@ pub async fn get_messages(working_directory: &Path, shas: &[Oid]) -> Result Result>()) } -async fn get_messages_impl(working_directory: &Path, shas: &[Oid]) -> Result> { +async fn get_messages_impl(git: &GitBinary, shas: &[Oid]) -> Result> { const MARKER: &str = ""; - let output = util::command::new_command("git") - .current_dir(working_directory) - .arg("show") + let output = git + .build_command(["show"]) .arg("-s") .arg(format!("--format=%B{}", MARKER)) .args(shas.iter().map(ToString::to_string)) diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index bd07555d05b759a33080b9ae9f166145c3d26d14..f5a856325cc80071f2c8ef500e7b07aa24035f59 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -21,6 +21,7 @@ use text::LineEnding; use std::collections::HashSet; use std::ffi::{OsStr, OsString}; +use std::sync::atomic::AtomicBool; use std::process::ExitStatus; use std::str::FromStr; @@ -303,6 +304,7 @@ impl Branch { pub struct Worktree { pub path: PathBuf, pub ref_name: SharedString, + // todo(git_worktree) This type should be a Oid pub sha: SharedString, } @@ -340,6 +342,8 @@ pub fn parse_worktrees_from_str>(raw_worktrees: T) -> Vec BoxFuture<'_, Result<()>>; fn commit_data_reader(&self) -> Result; + + fn set_trusted(&self, trusted: bool); + fn is_trusted(&self) -> bool; } pub enum DiffType { @@ -984,6 +991,7 @@ pub struct RealGitRepository { pub any_git_binary_path: PathBuf, any_git_binary_help_output: Arc>>, executor: BackgroundExecutor, + is_trusted: Arc, } impl RealGitRepository { @@ -1002,6 +1010,7 @@ impl RealGitRepository { any_git_binary_path, executor, any_git_binary_help_output: Arc::new(Mutex::new(None)), + is_trusted: Arc::new(AtomicBool::new(false)), }) } @@ -1013,20 +1022,24 @@ impl RealGitRepository { .map(Path::to_path_buf) } + fn git_binary(&self) -> Result { + Ok(GitBinary::new( + self.any_git_binary_path.clone(), + self.working_directory() + .with_context(|| "Can't run git commands without a working directory")?, + self.executor.clone(), + self.is_trusted(), + )) + } + async fn any_git_binary_help_output(&self) -> SharedString { if let Some(output) = self.any_git_binary_help_output.lock().clone() { return output; } - let git_binary_path = self.any_git_binary_path.clone(); - let executor = self.executor.clone(); - let working_directory = self.working_directory(); + let git_binary = self.git_binary(); let output: SharedString = self .executor - .spawn(async move { - GitBinary::new(git_binary_path, working_directory?, executor) - .run(["help", "-a"]) - .await - }) + .spawn(async move { git_binary?.run(["help", "-a"]).await }) .await .unwrap_or_default() .into(); @@ -1069,6 +1082,7 @@ pub async fn get_git_committer(cx: &AsyncApp) -> GitCommitter { git_binary_path.unwrap_or(PathBuf::from("git")), paths::home_dir().clone(), cx.background_executor().clone(), + true, ); cx.background_spawn(async move { @@ -1100,14 +1114,12 @@ impl GitRepository for RealGitRepository { } fn show(&self, commit: String) -> BoxFuture<'_, Result> { - let git_binary_path = self.any_git_binary_path.clone(); - let working_directory = self.working_directory(); + let git_binary = self.git_binary(); self.executor .spawn(async move { - let working_directory = working_directory?; - let output = new_command(git_binary_path) - .current_dir(&working_directory) - .args([ + let git = git_binary?; + let output = git + .build_command([ "--no-optional-locks", "show", "--no-patch", @@ -1138,15 +1150,14 @@ impl GitRepository for RealGitRepository { } fn load_commit(&self, commit: String, cx: AsyncApp) -> BoxFuture<'_, Result> { - let Some(working_directory) = self.repository.lock().workdir().map(ToOwned::to_owned) - else { + if self.repository.lock().workdir().is_none() { return future::ready(Err(anyhow!("no working directory"))).boxed(); - }; - let git_binary_path = self.any_git_binary_path.clone(); + } + let git_binary = self.git_binary(); cx.background_spawn(async move { - let show_output = util::command::new_command(&git_binary_path) - .current_dir(&working_directory) - .args([ + let git = git_binary?; + let show_output = git + .build_command([ "--no-optional-locks", "show", "--format=", @@ -1167,9 +1178,8 @@ impl GitRepository for RealGitRepository { let changes = parse_git_diff_name_status(&show_stdout); let parent_sha = format!("{}^", commit); - let mut cat_file_process = util::command::new_command(&git_binary_path) - .current_dir(&working_directory) - .args(["--no-optional-locks", "cat-file", "--batch=%(objectsize)"]) + let mut cat_file_process = git + .build_command(["--no-optional-locks", "cat-file", "--batch=%(objectsize)"]) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) @@ -1276,18 +1286,17 @@ impl GitRepository for RealGitRepository { mode: ResetMode, env: Arc>, ) -> BoxFuture<'_, Result<()>> { + let git_binary = self.git_binary(); async move { - let working_directory = self.working_directory(); - let mode_flag = match mode { ResetMode::Mixed => "--mixed", ResetMode::Soft => "--soft", }; - let output = new_command(&self.any_git_binary_path) + let git = git_binary?; + let output = git + .build_command(["reset", mode_flag, &commit]) .envs(env.iter()) - .current_dir(&working_directory?) - .args(["reset", mode_flag, &commit]) .output() .await?; anyhow::ensure!( @@ -1306,17 +1315,16 @@ impl GitRepository for RealGitRepository { paths: Vec, env: Arc>, ) -> BoxFuture<'_, Result<()>> { - let working_directory = self.working_directory(); - let git_binary_path = self.any_git_binary_path.clone(); + let git_binary = self.git_binary(); async move { if paths.is_empty() { return Ok(()); } - let output = new_command(&git_binary_path) - .current_dir(&working_directory?) + let git = git_binary?; + let output = git + .build_command(["checkout", &commit, "--"]) .envs(env.iter()) - .args(["checkout", &commit, "--"]) .args(paths.iter().map(|path| path.as_unix_str())) .output() .await?; @@ -1411,18 +1419,16 @@ impl GitRepository for RealGitRepository { env: Arc>, is_executable: bool, ) -> BoxFuture<'_, anyhow::Result<()>> { - let working_directory = self.working_directory(); - let git_binary_path = self.any_git_binary_path.clone(); + let git_binary = self.git_binary(); self.executor .spawn(async move { - let working_directory = working_directory?; + let git = git_binary?; let mode = if is_executable { "100755" } else { "100644" }; if let Some(content) = content { - let mut child = new_command(&git_binary_path) - .current_dir(&working_directory) + let mut child = git + .build_command(["hash-object", "-w", "--stdin"]) .envs(env.iter()) - .args(["hash-object", "-w", "--stdin"]) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .spawn()?; @@ -1435,10 +1441,9 @@ impl GitRepository for RealGitRepository { log::debug!("indexing SHA: {sha}, path {path:?}"); - let output = new_command(&git_binary_path) - .current_dir(&working_directory) + let output = git + .build_command(["update-index", "--add", "--cacheinfo", mode, sha]) .envs(env.iter()) - .args(["update-index", "--add", "--cacheinfo", mode, sha]) .arg(path.as_unix_str()) .output() .await?; @@ -1450,10 +1455,9 @@ impl GitRepository for RealGitRepository { ); } else { log::debug!("removing path {path:?} from the index"); - let output = new_command(&git_binary_path) - .current_dir(&working_directory) + let output = git + .build_command(["update-index", "--force-remove"]) .envs(env.iter()) - .args(["update-index", "--force-remove"]) .arg(path.as_unix_str()) .output() .await?; @@ -1482,14 +1486,12 @@ impl GitRepository for RealGitRepository { } fn revparse_batch(&self, revs: Vec) -> BoxFuture<'_, Result>>> { - let working_directory = self.working_directory(); - let git_binary_path = self.any_git_binary_path.clone(); + let git_binary = self.git_binary(); self.executor .spawn(async move { - let working_directory = working_directory?; - let mut process = new_command(&git_binary_path) - .current_dir(&working_directory) - .args([ + let git = git_binary?; + let mut process = git + .build_command([ "--no-optional-locks", "cat-file", "--batch-check=%(objectname)", @@ -1542,19 +1544,14 @@ impl GitRepository for RealGitRepository { } fn status(&self, path_prefixes: &[RepoPath]) -> Task> { - let git_binary_path = self.any_git_binary_path.clone(); - let working_directory = match self.working_directory() { - Ok(working_directory) => working_directory, + let git = match self.git_binary() { + Ok(git) => git, Err(e) => return Task::ready(Err(e)), }; let args = git_status_args(path_prefixes); log::debug!("Checking for git status in {path_prefixes:?}"); self.executor.spawn(async move { - let output = new_command(&git_binary_path) - .current_dir(working_directory) - .args(args) - .output() - .await?; + let output = git.build_command(args).output().await?; if output.status.success() { let stdout = String::from_utf8_lossy(&output.stdout); stdout.parse() @@ -1566,9 +1563,8 @@ impl GitRepository for RealGitRepository { } fn diff_tree(&self, request: DiffTreeType) -> BoxFuture<'_, Result> { - let git_binary_path = self.any_git_binary_path.clone(); - let working_directory = match self.working_directory() { - Ok(working_directory) => working_directory, + let git = match self.git_binary() { + Ok(git) => git, Err(e) => return Task::ready(Err(e)).boxed(), }; @@ -1593,11 +1589,7 @@ impl GitRepository for RealGitRepository { self.executor .spawn(async move { - let output = new_command(&git_binary_path) - .current_dir(working_directory) - .args(args) - .output() - .await?; + let output = git.build_command(args).output().await?; if output.status.success() { let stdout = String::from_utf8_lossy(&output.stdout); stdout.parse() @@ -1610,13 +1602,12 @@ impl GitRepository for RealGitRepository { } fn stash_entries(&self) -> BoxFuture<'_, Result> { - let git_binary_path = self.any_git_binary_path.clone(); - let working_directory = self.working_directory(); + let git_binary = self.git_binary(); self.executor .spawn(async move { - let output = new_command(&git_binary_path) - .current_dir(working_directory?) - .args(&["stash", "list", "--pretty=format:%gd%x00%H%x00%ct%x00%s"]) + let git = git_binary?; + let output = git + .build_command(&["stash", "list", "--pretty=format:%gd%x00%H%x00%ct%x00%s"]) .output() .await?; if output.status.success() { @@ -1631,8 +1622,7 @@ impl GitRepository for RealGitRepository { } fn branches(&self) -> BoxFuture<'_, Result>> { - let working_directory = self.working_directory(); - let git_binary_path = self.any_git_binary_path.clone(); + let git_binary = self.git_binary(); self.executor .spawn(async move { let fields = [ @@ -1654,12 +1644,8 @@ impl GitRepository for RealGitRepository { "--format", &fields, ]; - let working_directory = working_directory?; - let output = new_command(&git_binary_path) - .current_dir(&working_directory) - .args(args) - .output() - .await?; + let git = git_binary?; + let output = git.build_command(args).output().await?; anyhow::ensure!( output.status.success(), @@ -1673,11 +1659,7 @@ impl GitRepository for RealGitRepository { if branches.is_empty() { let args = vec!["symbolic-ref", "--quiet", "HEAD"]; - let output = new_command(&git_binary_path) - .current_dir(&working_directory) - .args(args) - .output() - .await?; + let output = git.build_command(args).output().await?; // git symbolic-ref returns a non-0 exit code if HEAD points // to something other than a branch @@ -1699,13 +1681,12 @@ impl GitRepository for RealGitRepository { } fn worktrees(&self) -> BoxFuture<'_, Result>> { - let git_binary_path = self.any_git_binary_path.clone(); - let working_directory = self.working_directory(); + let git_binary = self.git_binary(); self.executor .spawn(async move { - let output = new_command(&git_binary_path) - .current_dir(working_directory?) - .args(&["--no-optional-locks", "worktree", "list", "--porcelain"]) + let git = git_binary?; + let output = git + .build_command(&["--no-optional-locks", "worktree", "list", "--porcelain"]) .output() .await?; if output.status.success() { @@ -1725,8 +1706,7 @@ impl GitRepository for RealGitRepository { directory: PathBuf, from_commit: Option, ) -> BoxFuture<'_, Result<()>> { - let git_binary_path = self.any_git_binary_path.clone(); - let working_directory = self.working_directory(); + let git_binary = self.git_binary(); let final_path = directory.join(&name); let mut args = vec![ OsString::from("--no-optional-locks"), @@ -1746,11 +1726,8 @@ impl GitRepository for RealGitRepository { self.executor .spawn(async move { std::fs::create_dir_all(final_path.parent().unwrap_or(&final_path))?; - let output = new_command(&git_binary_path) - .current_dir(working_directory?) - .args(args) - .output() - .await?; + let git = git_binary?; + let output = git.build_command(args).output().await?; if output.status.success() { Ok(()) } else { @@ -1762,9 +1739,7 @@ impl GitRepository for RealGitRepository { } fn remove_worktree(&self, path: PathBuf, force: bool) -> BoxFuture<'_, Result<()>> { - let git_binary_path = self.any_git_binary_path.clone(); - let working_directory = self.working_directory(); - let executor = self.executor.clone(); + let git_binary = self.git_binary(); self.executor .spawn(async move { @@ -1778,18 +1753,14 @@ impl GitRepository for RealGitRepository { } args.push("--".into()); args.push(path.as_os_str().into()); - GitBinary::new(git_binary_path, working_directory?, executor) - .run(args) - .await?; + git_binary?.run(args).await?; anyhow::Ok(()) }) .boxed() } fn rename_worktree(&self, old_path: PathBuf, new_path: PathBuf) -> BoxFuture<'_, Result<()>> { - let git_binary_path = self.any_git_binary_path.clone(); - let working_directory = self.working_directory(); - let executor = self.executor.clone(); + let git_binary = self.git_binary(); self.executor .spawn(async move { @@ -1801,9 +1772,7 @@ impl GitRepository for RealGitRepository { old_path.as_os_str().into(), new_path.as_os_str().into(), ]; - GitBinary::new(git_binary_path, working_directory?, executor) - .run(args) - .await?; + git_binary?.run(args).await?; anyhow::Ok(()) }) .boxed() @@ -1811,9 +1780,7 @@ impl GitRepository for RealGitRepository { fn change_branch(&self, name: String) -> BoxFuture<'_, Result<()>> { let repo = self.repository.clone(); - let working_directory = self.working_directory(); - let git_binary_path = self.any_git_binary_path.clone(); - let executor = self.executor.clone(); + let git_binary = self.git_binary(); let branch = self.executor.spawn(async move { let repo = repo.lock(); let branch = if let Ok(branch) = repo.find_branch(&name, BranchType::Local) { @@ -1848,9 +1815,7 @@ impl GitRepository for RealGitRepository { self.executor .spawn(async move { let branch = branch.await?; - GitBinary::new(git_binary_path, working_directory?, executor) - .run(&["checkout", &branch]) - .await?; + git_binary?.run(&["checkout", &branch]).await?; anyhow::Ok(()) }) .boxed() @@ -1861,9 +1826,7 @@ impl GitRepository for RealGitRepository { name: String, base_branch: Option, ) -> BoxFuture<'_, Result<()>> { - let git_binary_path = self.any_git_binary_path.clone(); - let working_directory = self.working_directory(); - let executor = self.executor.clone(); + let git_binary = self.git_binary(); self.executor .spawn(async move { @@ -1874,22 +1837,18 @@ impl GitRepository for RealGitRepository { args.push(&base_branch_str); } - GitBinary::new(git_binary_path, working_directory?, executor) - .run(&args) - .await?; + git_binary?.run(&args).await?; anyhow::Ok(()) }) .boxed() } fn rename_branch(&self, branch: String, new_name: String) -> BoxFuture<'_, Result<()>> { - let git_binary_path = self.any_git_binary_path.clone(); - let working_directory = self.working_directory(); - let executor = self.executor.clone(); + let git_binary = self.git_binary(); self.executor .spawn(async move { - GitBinary::new(git_binary_path, working_directory?, executor) + git_binary? .run(&["branch", "-m", &branch, &new_name]) .await?; anyhow::Ok(()) @@ -1898,15 +1857,11 @@ impl GitRepository for RealGitRepository { } fn delete_branch(&self, name: String) -> BoxFuture<'_, Result<()>> { - let git_binary_path = self.any_git_binary_path.clone(); - let working_directory = self.working_directory(); - let executor = self.executor.clone(); + let git_binary = self.git_binary(); self.executor .spawn(async move { - GitBinary::new(git_binary_path, working_directory?, executor) - .run(&["branch", "-d", &name]) - .await?; + git_binary?.run(&["branch", "-d", &name]).await?; anyhow::Ok(()) }) .boxed() @@ -1918,20 +1873,11 @@ impl GitRepository for RealGitRepository { content: Rope, line_ending: LineEnding, ) -> BoxFuture<'_, Result> { - let working_directory = self.working_directory(); - let git_binary_path = self.any_git_binary_path.clone(); - let executor = self.executor.clone(); + let git = self.git_binary(); - executor + self.executor .spawn(async move { - crate::blame::Blame::for_path( - &git_binary_path, - &working_directory?, - &path, - &content, - line_ending, - ) - .await + crate::blame::Blame::for_path(&git?, &path, &content, line_ending).await }) .boxed() } @@ -1946,11 +1892,10 @@ impl GitRepository for RealGitRepository { skip: usize, limit: Option, ) -> BoxFuture<'_, Result> { - let working_directory = self.working_directory(); - let git_binary_path = self.any_git_binary_path.clone(); + let git_binary = self.git_binary(); self.executor .spawn(async move { - let working_directory = working_directory?; + let git = git_binary?; // Use a unique delimiter with a hardcoded UUID to separate commits // This essentially eliminates any chance of encountering the delimiter in actual commit data let commit_delimiter = @@ -1978,9 +1923,8 @@ impl GitRepository for RealGitRepository { args.push("--"); - let output = new_command(&git_binary_path) - .current_dir(&working_directory) - .args(&args) + let output = git + .build_command(&args) .arg(path.as_unix_str()) .output() .await?; @@ -2025,30 +1969,17 @@ impl GitRepository for RealGitRepository { } fn diff(&self, diff: DiffType) -> BoxFuture<'_, Result> { - let working_directory = self.working_directory(); - let git_binary_path = self.any_git_binary_path.clone(); + let git_binary = self.git_binary(); self.executor .spawn(async move { - let working_directory = working_directory?; + let git = git_binary?; let output = match diff { DiffType::HeadToIndex => { - new_command(&git_binary_path) - .current_dir(&working_directory) - .args(["diff", "--staged"]) - .output() - .await? - } - DiffType::HeadToWorktree => { - new_command(&git_binary_path) - .current_dir(&working_directory) - .args(["diff"]) - .output() - .await? + git.build_command(["diff", "--staged"]).output().await? } + DiffType::HeadToWorktree => git.build_command(["diff"]).output().await?, DiffType::MergeBase { base_ref } => { - new_command(&git_binary_path) - .current_dir(&working_directory) - .args(["diff", "--merge-base", base_ref.as_ref()]) + git.build_command(["diff", "--merge-base", base_ref.as_ref()]) .output() .await? } @@ -2068,38 +1999,29 @@ impl GitRepository for RealGitRepository { &self, diff: DiffType, ) -> BoxFuture<'_, Result>> { - let working_directory = self.working_directory(); - let git_binary_path = self.any_git_binary_path.clone(); + let git_binary = self.git_binary(); self.executor .spawn(async move { - let working_directory = working_directory?; + let git = git_binary?; let output = match diff { DiffType::HeadToIndex => { - new_command(&git_binary_path) - .current_dir(&working_directory) - .args(["diff", "--numstat", "--staged"]) + git.build_command(["diff", "--numstat", "--staged"]) .output() .await? } DiffType::HeadToWorktree => { - new_command(&git_binary_path) - .current_dir(&working_directory) - .args(["diff", "--numstat"]) - .output() - .await? + git.build_command(["diff", "--numstat"]).output().await? } DiffType::MergeBase { base_ref } => { - new_command(&git_binary_path) - .current_dir(&working_directory) - .args([ - "diff", - "--numstat", - "--merge-base", - base_ref.as_ref(), - "HEAD", - ]) - .output() - .await? + git.build_command([ + "diff", + "--numstat", + "--merge-base", + base_ref.as_ref(), + "HEAD", + ]) + .output() + .await? } }; @@ -2120,15 +2042,14 @@ impl GitRepository for RealGitRepository { paths: Vec, env: Arc>, ) -> BoxFuture<'_, Result<()>> { - let working_directory = self.working_directory(); - let git_binary_path = self.any_git_binary_path.clone(); + let git_binary = self.git_binary(); self.executor .spawn(async move { if !paths.is_empty() { - let output = new_command(&git_binary_path) - .current_dir(&working_directory?) + let git = git_binary?; + let output = git + .build_command(["update-index", "--add", "--remove", "--"]) .envs(env.iter()) - .args(["update-index", "--add", "--remove", "--"]) .args(paths.iter().map(|p| p.as_unix_str())) .output() .await?; @@ -2148,16 +2069,15 @@ impl GitRepository for RealGitRepository { paths: Vec, env: Arc>, ) -> BoxFuture<'_, Result<()>> { - let working_directory = self.working_directory(); - let git_binary_path = self.any_git_binary_path.clone(); + let git_binary = self.git_binary(); self.executor .spawn(async move { if !paths.is_empty() { - let output = new_command(&git_binary_path) - .current_dir(&working_directory?) + let git = git_binary?; + let output = git + .build_command(["reset", "--quiet", "--"]) .envs(env.iter()) - .args(["reset", "--quiet", "--"]) .args(paths.iter().map(|p| p.as_std_path())) .output() .await?; @@ -2178,19 +2098,16 @@ impl GitRepository for RealGitRepository { paths: Vec, env: Arc>, ) -> BoxFuture<'_, Result<()>> { - let working_directory = self.working_directory(); - let git_binary_path = self.any_git_binary_path.clone(); + let git_binary = self.git_binary(); self.executor .spawn(async move { - let mut cmd = new_command(&git_binary_path); - cmd.current_dir(&working_directory?) + let git = git_binary?; + let output = git + .build_command(["stash", "push", "--quiet", "--include-untracked"]) .envs(env.iter()) - .args(["stash", "push", "--quiet"]) - .arg("--include-untracked"); - - cmd.args(paths.iter().map(|p| p.as_unix_str())); - - let output = cmd.output().await?; + .args(paths.iter().map(|p| p.as_unix_str())) + .output() + .await?; anyhow::ensure!( output.status.success(), @@ -2207,20 +2124,15 @@ impl GitRepository for RealGitRepository { index: Option, env: Arc>, ) -> BoxFuture<'_, Result<()>> { - let working_directory = self.working_directory(); - let git_binary_path = self.any_git_binary_path.clone(); + let git_binary = self.git_binary(); self.executor .spawn(async move { - let mut cmd = new_command(git_binary_path); + let git = git_binary?; let mut args = vec!["stash".to_string(), "pop".to_string()]; if let Some(index) = index { args.push(format!("stash@{{{}}}", index)); } - cmd.current_dir(&working_directory?) - .envs(env.iter()) - .args(args); - - let output = cmd.output().await?; + let output = git.build_command(&args).envs(env.iter()).output().await?; anyhow::ensure!( output.status.success(), @@ -2237,20 +2149,15 @@ impl GitRepository for RealGitRepository { index: Option, env: Arc>, ) -> BoxFuture<'_, Result<()>> { - let working_directory = self.working_directory(); - let git_binary_path = self.any_git_binary_path.clone(); + let git_binary = self.git_binary(); self.executor .spawn(async move { - let mut cmd = new_command(git_binary_path); + let git = git_binary?; let mut args = vec!["stash".to_string(), "apply".to_string()]; if let Some(index) = index { args.push(format!("stash@{{{}}}", index)); } - cmd.current_dir(&working_directory?) - .envs(env.iter()) - .args(args); - - let output = cmd.output().await?; + let output = git.build_command(&args).envs(env.iter()).output().await?; anyhow::ensure!( output.status.success(), @@ -2267,20 +2174,15 @@ impl GitRepository for RealGitRepository { index: Option, env: Arc>, ) -> BoxFuture<'_, Result<()>> { - let working_directory = self.working_directory(); - let git_binary_path = self.any_git_binary_path.clone(); + let git_binary = self.git_binary(); self.executor .spawn(async move { - let mut cmd = new_command(git_binary_path); + let git = git_binary?; let mut args = vec!["stash".to_string(), "drop".to_string()]; if let Some(index) = index { args.push(format!("stash@{{{}}}", index)); } - cmd.current_dir(&working_directory?) - .envs(env.iter()) - .args(args); - - let output = cmd.output().await?; + let output = git.build_command(&args).envs(env.iter()).output().await?; anyhow::ensure!( output.status.success(), @@ -2300,16 +2202,14 @@ impl GitRepository for RealGitRepository { ask_pass: AskPassDelegate, env: Arc>, ) -> BoxFuture<'_, Result<()>> { - let working_directory = self.working_directory(); - let git_binary_path = self.any_git_binary_path.clone(); + let git_binary = self.git_binary(); let executor = self.executor.clone(); // Note: Do not spawn this command on the background thread, it might pop open the credential helper // which we want to block on. async move { - let mut cmd = new_command(git_binary_path); - cmd.current_dir(&working_directory?) - .envs(env.iter()) - .args(["commit", "--quiet", "-m"]) + let git = git_binary?; + let mut cmd = git.build_command(["commit", "--quiet", "-m"]); + cmd.envs(env.iter()) .arg(&message.to_string()) .arg("--cleanup=strip") .arg("--no-verify") @@ -2348,16 +2248,21 @@ impl GitRepository for RealGitRepository { let working_directory = self.working_directory(); let executor = cx.background_executor().clone(); let git_binary_path = self.system_git_binary_path.clone(); + let is_trusted = self.is_trusted(); // Note: Do not spawn this command on the background thread, it might pop open the credential helper // which we want to block on. async move { let git_binary_path = git_binary_path.context("git not found on $PATH, can't push")?; let working_directory = working_directory?; - let mut command = new_command(git_binary_path); + let git = GitBinary::new( + git_binary_path, + working_directory, + executor.clone(), + is_trusted, + ); + let mut command = git.build_command(["push"]); command .envs(env.iter()) - .current_dir(&working_directory) - .args(["push"]) .args(options.map(|option| match option { PushOptions::SetUpstream => "--set-upstream", PushOptions::Force => "--force-with-lease", @@ -2385,15 +2290,20 @@ impl GitRepository for RealGitRepository { let working_directory = self.working_directory(); let executor = cx.background_executor().clone(); let git_binary_path = self.system_git_binary_path.clone(); + let is_trusted = self.is_trusted(); // Note: Do not spawn this command on the background thread, it might pop open the credential helper // which we want to block on. async move { let git_binary_path = git_binary_path.context("git not found on $PATH, can't pull")?; - let mut command = new_command(git_binary_path); - command - .envs(env.iter()) - .current_dir(&working_directory?) - .arg("pull"); + let working_directory = working_directory?; + let git = GitBinary::new( + git_binary_path, + working_directory, + executor.clone(), + is_trusted, + ); + let mut command = git.build_command(["pull"]); + command.envs(env.iter()); if rebase { command.arg("--rebase"); @@ -2421,15 +2331,21 @@ impl GitRepository for RealGitRepository { let remote_name = format!("{}", fetch_options); let git_binary_path = self.system_git_binary_path.clone(); let executor = cx.background_executor().clone(); + let is_trusted = self.is_trusted(); // Note: Do not spawn this command on the background thread, it might pop open the credential helper // which we want to block on. async move { let git_binary_path = git_binary_path.context("git not found on $PATH, can't fetch")?; - let mut command = new_command(git_binary_path); + let working_directory = working_directory?; + let git = GitBinary::new( + git_binary_path, + working_directory, + executor.clone(), + is_trusted, + ); + let mut command = git.build_command(["fetch", &remote_name]); command .envs(env.iter()) - .current_dir(&working_directory?) - .args(["fetch", &remote_name]) .stdout(Stdio::piped()) .stderr(Stdio::piped()); @@ -2439,14 +2355,12 @@ impl GitRepository for RealGitRepository { } fn get_push_remote(&self, branch: String) -> BoxFuture<'_, Result>> { - let working_directory = self.working_directory(); - let git_binary_path = self.any_git_binary_path.clone(); + let git_binary = self.git_binary(); self.executor .spawn(async move { - let working_directory = working_directory?; - let output = new_command(&git_binary_path) - .current_dir(&working_directory) - .args(["rev-parse", "--abbrev-ref"]) + let git = git_binary?; + let output = git + .build_command(["rev-parse", "--abbrev-ref"]) .arg(format!("{branch}@{{push}}")) .output() .await?; @@ -2466,14 +2380,12 @@ impl GitRepository for RealGitRepository { } fn get_branch_remote(&self, branch: String) -> BoxFuture<'_, Result>> { - let working_directory = self.working_directory(); - let git_binary_path = self.any_git_binary_path.clone(); + let git_binary = self.git_binary(); self.executor .spawn(async move { - let working_directory = working_directory?; - let output = new_command(&git_binary_path) - .current_dir(&working_directory) - .args(["config", "--get"]) + let git = git_binary?; + let output = git + .build_command(["config", "--get"]) .arg(format!("branch.{branch}.remote")) .output() .await?; @@ -2490,16 +2402,11 @@ impl GitRepository for RealGitRepository { } fn get_all_remotes(&self) -> BoxFuture<'_, Result>> { - let working_directory = self.working_directory(); - let git_binary_path = self.any_git_binary_path.clone(); + let git_binary = self.git_binary(); self.executor .spawn(async move { - let working_directory = working_directory?; - let output = new_command(&git_binary_path) - .current_dir(&working_directory) - .args(["remote", "-v"]) - .output() - .await?; + let git = git_binary?; + let output = git.build_command(["remote", "-v"]).output().await?; anyhow::ensure!( output.status.success(), @@ -2548,17 +2455,12 @@ impl GitRepository for RealGitRepository { } fn check_for_pushed_commit(&self) -> BoxFuture<'_, Result>> { - let working_directory = self.working_directory(); - let git_binary_path = self.any_git_binary_path.clone(); + let git_binary = self.git_binary(); self.executor .spawn(async move { - let working_directory = working_directory?; + let git = git_binary?; let git_cmd = async |args: &[&str]| -> Result { - let output = new_command(&git_binary_path) - .current_dir(&working_directory) - .args(args) - .output() - .await?; + let output = git.build_command(args).output().await?; anyhow::ensure!( output.status.success(), String::from_utf8_lossy(&output.stderr).to_string() @@ -2607,14 +2509,10 @@ impl GitRepository for RealGitRepository { } fn checkpoint(&self) -> BoxFuture<'static, Result> { - let working_directory = self.working_directory(); - let git_binary_path = self.any_git_binary_path.clone(); - let executor = self.executor.clone(); + let git_binary = self.git_binary(); self.executor .spawn(async move { - let working_directory = working_directory?; - let mut git = GitBinary::new(git_binary_path, working_directory.clone(), executor) - .envs(checkpoint_author_envs()); + let mut git = git_binary?.envs(checkpoint_author_envs()); git.with_temp_index(async |git| { let head_sha = git.run(&["rev-parse", "HEAD"]).await.ok(); let mut excludes = exclude_files(git).await?; @@ -2640,15 +2538,10 @@ impl GitRepository for RealGitRepository { } fn restore_checkpoint(&self, checkpoint: GitRepositoryCheckpoint) -> BoxFuture<'_, Result<()>> { - let working_directory = self.working_directory(); - let git_binary_path = self.any_git_binary_path.clone(); - - let executor = self.executor.clone(); + let git_binary = self.git_binary(); self.executor .spawn(async move { - let working_directory = working_directory?; - - let git = GitBinary::new(git_binary_path, working_directory, executor); + let git = git_binary?; git.run(&[ "restore", "--source", @@ -2679,14 +2572,10 @@ impl GitRepository for RealGitRepository { left: GitRepositoryCheckpoint, right: GitRepositoryCheckpoint, ) -> BoxFuture<'_, Result> { - let working_directory = self.working_directory(); - let git_binary_path = self.any_git_binary_path.clone(); - - let executor = self.executor.clone(); + let git_binary = self.git_binary(); self.executor .spawn(async move { - let working_directory = working_directory?; - let git = GitBinary::new(git_binary_path, working_directory, executor); + let git = git_binary?; let result = git .run(&[ "diff-tree", @@ -2717,14 +2606,10 @@ impl GitRepository for RealGitRepository { base_checkpoint: GitRepositoryCheckpoint, target_checkpoint: GitRepositoryCheckpoint, ) -> BoxFuture<'_, Result> { - let working_directory = self.working_directory(); - let git_binary_path = self.any_git_binary_path.clone(); - - let executor = self.executor.clone(); + let git_binary = self.git_binary(); self.executor .spawn(async move { - let working_directory = working_directory?; - let git = GitBinary::new(git_binary_path, working_directory, executor); + let git = git_binary?; git.run(&[ "diff", "--find-renames", @@ -2741,14 +2626,10 @@ impl GitRepository for RealGitRepository { &self, include_remote_name: bool, ) -> BoxFuture<'_, Result>> { - let working_directory = self.working_directory(); - let git_binary_path = self.any_git_binary_path.clone(); - - let executor = self.executor.clone(); + let git_binary = self.git_binary(); self.executor .spawn(async move { - let working_directory = working_directory?; - let git = GitBinary::new(git_binary_path, working_directory, executor); + let git = git_binary?; let strip_prefix = if include_remote_name { "refs/remotes/" @@ -2798,15 +2679,19 @@ impl GitRepository for RealGitRepository { hook: RunHook, env: Arc>, ) -> BoxFuture<'_, Result<()>> { - let working_directory = self.working_directory(); + let git_binary = self.git_binary(); let repository = self.repository.clone(); - let git_binary_path = self.any_git_binary_path.clone(); - let executor = self.executor.clone(); let help_output = self.any_git_binary_help_output(); // Note: Do not spawn these commands on the background thread, as this causes some git hooks to hang. async move { - let working_directory = working_directory?; + let git = git_binary?; + + if !git.is_trusted { + bail!("Can't run git commit hooks in restrictive workspace"); + } + + let working_directory = git.working_directory.clone(); if !help_output .await .lines() @@ -2814,6 +2699,7 @@ impl GitRepository for RealGitRepository { { let hook_abs_path = repository.lock().path().join("hooks").join(hook.as_str()); if hook_abs_path.is_file() { + #[allow(clippy::disallowed_methods)] let output = new_command(&hook_abs_path) .envs(env.iter()) .current_dir(&working_directory) @@ -2833,8 +2719,7 @@ impl GitRepository for RealGitRepository { return Ok(()); } - let git = GitBinary::new(git_binary_path, working_directory, executor) - .envs(HashMap::clone(&env)); + let git = git.envs(HashMap::clone(&env)); git.run(&["hook", "run", "--ignore-missing", hook.as_str()]) .await?; Ok(()) @@ -2848,13 +2733,10 @@ impl GitRepository for RealGitRepository { log_order: LogOrder, request_tx: Sender>>, ) -> BoxFuture<'_, Result<()>> { - let git_binary_path = self.any_git_binary_path.clone(); - let working_directory = self.working_directory(); - let executor = self.executor.clone(); + let git_binary = self.git_binary(); async move { - let working_directory = working_directory?; - let git = GitBinary::new(git_binary_path, working_directory, executor); + let git = git_binary?; let mut command = git.build_command([ "log", @@ -2908,19 +2790,12 @@ impl GitRepository for RealGitRepository { } fn commit_data_reader(&self) -> Result { - let git_binary_path = self.any_git_binary_path.clone(); - let working_directory = self - .working_directory() - .map_err(|_| anyhow!("no working directory"))?; - let executor = self.executor.clone(); + let git_binary = self.git_binary()?; let (request_tx, request_rx) = smol::channel::bounded::(64); let task = self.executor.spawn(async move { - if let Err(error) = - run_commit_data_reader(git_binary_path, working_directory, executor, request_rx) - .await - { + if let Err(error) = run_commit_data_reader(git_binary, request_rx).await { log::error!("commit data reader failed: {error:?}"); } }); @@ -2930,15 +2805,21 @@ impl GitRepository for RealGitRepository { _task: task, }) } + + fn set_trusted(&self, trusted: bool) { + self.is_trusted + .store(trusted, std::sync::atomic::Ordering::Release); + } + + fn is_trusted(&self) -> bool { + self.is_trusted.load(std::sync::atomic::Ordering::Acquire) + } } async fn run_commit_data_reader( - git_binary_path: PathBuf, - working_directory: PathBuf, - executor: BackgroundExecutor, + git: GitBinary, request_rx: smol::channel::Receiver, ) -> Result<()> { - let git = GitBinary::new(git_binary_path, working_directory, executor); let mut process = git .build_command(["--no-optional-locks", "cat-file", "--batch"]) .stdin(Stdio::piped()) @@ -3114,19 +2995,21 @@ async fn exclude_files(git: &GitBinary) -> Result { Ok(excludes) } -struct GitBinary { +pub(crate) struct GitBinary { git_binary_path: PathBuf, working_directory: PathBuf, executor: BackgroundExecutor, index_file_path: Option, envs: HashMap, + is_trusted: bool, } impl GitBinary { - fn new( + pub(crate) fn new( git_binary_path: PathBuf, working_directory: PathBuf, executor: BackgroundExecutor, + is_trusted: bool, ) -> Self { Self { git_binary_path, @@ -3134,6 +3017,7 @@ impl GitBinary { executor, index_file_path: None, envs: HashMap::default(), + is_trusted, } } @@ -3238,12 +3122,20 @@ impl GitBinary { Ok(String::from_utf8(output.stdout)?) } - fn build_command(&self, args: impl IntoIterator) -> util::command::Command + #[allow(clippy::disallowed_methods)] + pub(crate) fn build_command( + &self, + args: impl IntoIterator, + ) -> util::command::Command where S: AsRef, { let mut command = new_command(&self.git_binary_path); command.current_dir(&self.working_directory); + command.args(["-c", "core.fsmonitor=false"]); + if !self.is_trusted { + command.args(["-c", "core.hooksPath=/dev/null"]); + } command.args(args); if let Some(index_file_path) = self.index_file_path.as_ref() { command.env("GIT_INDEX_FILE", index_file_path); @@ -3503,6 +3395,102 @@ mod tests { } } + #[gpui::test] + async fn test_build_command_untrusted_includes_both_safety_args(cx: &mut TestAppContext) { + cx.executor().allow_parking(); + let dir = tempfile::tempdir().unwrap(); + let git = GitBinary::new( + PathBuf::from("git"), + dir.path().to_path_buf(), + cx.executor(), + false, + ); + let output = git + .build_command(["version"]) + .output() + .await + .expect("git version should succeed"); + assert!(output.status.success()); + + let git = GitBinary::new( + PathBuf::from("git"), + dir.path().to_path_buf(), + cx.executor(), + false, + ); + let output = git + .build_command(["config", "--get", "core.fsmonitor"]) + .output() + .await + .expect("git config should run"); + let stdout = String::from_utf8_lossy(&output.stdout); + assert_eq!( + stdout.trim(), + "false", + "fsmonitor should be disabled for untrusted repos" + ); + + git2::Repository::init(dir.path()).unwrap(); + let git = GitBinary::new( + PathBuf::from("git"), + dir.path().to_path_buf(), + cx.executor(), + false, + ); + let output = git + .build_command(["config", "--get", "core.hooksPath"]) + .output() + .await + .expect("git config should run"); + let stdout = String::from_utf8_lossy(&output.stdout); + assert_eq!( + stdout.trim(), + "/dev/null", + "hooksPath should be /dev/null for untrusted repos" + ); + } + + #[gpui::test] + async fn test_build_command_trusted_only_disables_fsmonitor(cx: &mut TestAppContext) { + cx.executor().allow_parking(); + let dir = tempfile::tempdir().unwrap(); + git2::Repository::init(dir.path()).unwrap(); + + let git = GitBinary::new( + PathBuf::from("git"), + dir.path().to_path_buf(), + cx.executor(), + true, + ); + let output = git + .build_command(["config", "--get", "core.fsmonitor"]) + .output() + .await + .expect("git config should run"); + let stdout = String::from_utf8_lossy(&output.stdout); + assert_eq!( + stdout.trim(), + "false", + "fsmonitor should be disabled even for trusted repos" + ); + + let git = GitBinary::new( + PathBuf::from("git"), + dir.path().to_path_buf(), + cx.executor(), + true, + ); + let output = git + .build_command(["config", "--get", "core.hooksPath"]) + .output() + .await + .expect("git config should run"); + assert!( + !output.status.success(), + "hooksPath should NOT be overridden for trusted repos" + ); + } + #[gpui::test] async fn test_checkpoint_basic(cx: &mut TestAppContext) { disable_git_global_config(); @@ -4395,7 +4383,7 @@ mod tests { .spawn(async move { let git_binary_path = git_binary_path.clone(); let working_directory = working_directory?; - let git = GitBinary::new(git_binary_path, working_directory, executor); + let git = GitBinary::new(git_binary_path, working_directory, executor, true); git.run(&["gc", "--prune"]).await?; Ok(()) }) diff --git a/crates/gpui_wgpu/src/wgpu_renderer.rs b/crates/gpui_wgpu/src/wgpu_renderer.rs index 5beeef6ad1238f25db7c50f739053e138b2e1295..2fd83b7b065e7ce4fe0ba9ec017f39264a33bee3 100644 --- a/crates/gpui_wgpu/src/wgpu_renderer.rs +++ b/crates/gpui_wgpu/src/wgpu_renderer.rs @@ -98,7 +98,6 @@ pub struct WgpuRenderer { queue: Arc, surface: wgpu::Surface<'static>, surface_config: wgpu::SurfaceConfiguration, - surface_configured: bool, pipelines: WgpuPipelines, bind_group_layouts: WgpuBindGroupLayouts, atlas: Arc, @@ -381,7 +380,6 @@ impl WgpuRenderer { queue, surface, surface_config, - surface_configured: true, pipelines, bind_group_layouts, atlas, @@ -875,9 +873,7 @@ impl WgpuRenderer { self.surface_config.width = clamped_width.max(1); self.surface_config.height = clamped_height.max(1); - if self.surface_configured { - self.surface.configure(&self.device, &self.surface_config); - } + self.surface.configure(&self.device, &self.surface_config); // Invalidate intermediate textures - they will be lazily recreated // in draw() after we confirm the surface is healthy. This avoids @@ -928,9 +924,7 @@ impl WgpuRenderer { if new_alpha_mode != self.surface_config.alpha_mode { self.surface_config.alpha_mode = new_alpha_mode; - if self.surface_configured { - self.surface.configure(&self.device, &self.surface_config); - } + self.surface.configure(&self.device, &self.surface_config); self.pipelines = Self::create_pipelines( &self.device, &self.bind_group_layouts, @@ -991,7 +985,7 @@ impl WgpuRenderer { let frame = match self.surface.get_current_texture() { Ok(frame) => frame, Err(wgpu::SurfaceError::Lost | wgpu::SurfaceError::Outdated) => { - self.surface_configured = false; + self.surface.configure(&self.device, &self.surface_config); return; } Err(e) => { diff --git a/crates/http_client/src/async_body.rs b/crates/http_client/src/async_body.rs index 8fb49f218568ea36078d772a7225229f31a916c4..a59a7339db1e4449b875e2c539e98c86b4279365 100644 --- a/crates/http_client/src/async_body.rs +++ b/crates/http_client/src/async_body.rs @@ -7,6 +7,7 @@ use std::{ use bytes::Bytes; use futures::AsyncRead; use http_body::{Body, Frame}; +use serde::Serialize; /// Based on the implementation of AsyncBody in /// . @@ -88,6 +89,19 @@ impl From<&'static str> for AsyncBody { } } +/// Newtype wrapper that serializes a value as JSON into an `AsyncBody`. +pub struct Json(pub T); + +impl From> for AsyncBody { + fn from(json: Json) -> Self { + Self::from_bytes( + serde_json::to_vec(&json.0) + .expect("failed to serialize JSON") + .into(), + ) + } +} + impl> From> for AsyncBody { fn from(body: Option) -> Self { match body { diff --git a/crates/http_client/src/http_client.rs b/crates/http_client/src/http_client.rs index 5cf25a8277872ba3c6d502565e8057623b267d42..bbbe3b1a832332bd6bee693b4c0b916b4f4c182a 100644 --- a/crates/http_client/src/http_client.rs +++ b/crates/http_client/src/http_client.rs @@ -5,7 +5,7 @@ pub mod github; pub mod github_download; pub use anyhow::{Result, anyhow}; -pub use async_body::{AsyncBody, Inner}; +pub use async_body::{AsyncBody, Inner, Json}; use derive_more::Deref; use http::HeaderValue; pub use http::{self, Method, Request, Response, StatusCode, Uri, request::Builder}; diff --git a/crates/language_model/src/model/cloud_model.rs b/crates/language_model/src/model/cloud_model.rs index 18e099b4d6fc62867bf35fbd1d4573093af44744..b2af80a3c295cab1cf40a330eb8d84f94a137eb7 100644 --- a/crates/language_model/src/model/cloud_model.rs +++ b/crates/language_model/src/model/cloud_model.rs @@ -4,6 +4,7 @@ use std::sync::Arc; use anyhow::{Context as _, Result}; use client::Client; use cloud_api_client::ClientApiError; +use cloud_api_types::OrganizationId; use cloud_api_types::websocket_protocol::MessageToClient; use cloud_llm_client::{EXPIRED_LLM_TOKEN_HEADER_NAME, OUTDATED_LLM_TOKEN_HEADER_NAME}; use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Global, ReadGlobal as _}; @@ -26,29 +27,46 @@ impl fmt::Display for PaymentRequiredError { pub struct LlmApiToken(Arc>>); impl LlmApiToken { - pub async fn acquire(&self, client: &Arc) -> Result { + pub async fn acquire( + &self, + client: &Arc, + organization_id: Option, + ) -> Result { let lock = self.0.upgradable_read().await; if let Some(token) = lock.as_ref() { Ok(token.to_string()) } else { - Self::fetch(RwLockUpgradableReadGuard::upgrade(lock).await, client).await + Self::fetch( + RwLockUpgradableReadGuard::upgrade(lock).await, + client, + organization_id, + ) + .await } } - pub async fn refresh(&self, client: &Arc) -> Result { - Self::fetch(self.0.write().await, client).await + pub async fn refresh( + &self, + client: &Arc, + organization_id: Option, + ) -> Result { + Self::fetch(self.0.write().await, client, organization_id).await } async fn fetch( mut lock: RwLockWriteGuard<'_, Option>, client: &Arc, + organization_id: Option, ) -> Result { let system_id = client .telemetry() .system_id() .map(|system_id| system_id.to_string()); - let result = client.cloud_client().create_llm_token(system_id).await; + let result = client + .cloud_client() + .create_llm_token(system_id, organization_id) + .await; match result { Ok(response) => { *lock = Some(response.token.0.clone()); diff --git a/crates/language_models/src/provider/cloud.rs b/crates/language_models/src/provider/cloud.rs index 19009013bf84ad9751e9ed0de2d3338b279a258e..b84b19b038905ba9f3d9a0637c770acc95687976 100644 --- a/crates/language_models/src/provider/cloud.rs +++ b/crates/language_models/src/provider/cloud.rs @@ -3,7 +3,7 @@ use anthropic::AnthropicModelMode; use anyhow::{Context as _, Result, anyhow}; use chrono::{DateTime, Utc}; use client::{Client, UserStore, zed_urls}; -use cloud_api_types::Plan; +use cloud_api_types::{OrganizationId, Plan}; use cloud_llm_client::{ CLIENT_SUPPORTS_STATUS_MESSAGES_HEADER_NAME, CLIENT_SUPPORTS_STATUS_STREAM_ENDED_HEADER_NAME, CLIENT_SUPPORTS_X_AI_HEADER_NAME, CompletionBody, CompletionEvent, CompletionRequestStatus, @@ -122,15 +122,25 @@ impl State { recommended_models: Vec::new(), _fetch_models_task: cx.spawn(async move |this, cx| { maybe!(async move { - let (client, llm_api_token) = this - .read_with(cx, |this, _cx| (client.clone(), this.llm_api_token.clone()))?; + let (client, llm_api_token, organization_id) = + this.read_with(cx, |this, cx| { + ( + client.clone(), + this.llm_api_token.clone(), + this.user_store + .read(cx) + .current_organization() + .map(|o| o.id.clone()), + ) + })?; while current_user.borrow().is_none() { current_user.next().await; } let response = - Self::fetch_models(client.clone(), llm_api_token.clone()).await?; + Self::fetch_models(client.clone(), llm_api_token.clone(), organization_id) + .await?; this.update(cx, |this, cx| this.update_models(response, cx))?; anyhow::Ok(()) }) @@ -146,9 +156,17 @@ impl State { move |this, _listener, _event, cx| { let client = this.client.clone(); let llm_api_token = this.llm_api_token.clone(); + let organization_id = this + .user_store + .read(cx) + .current_organization() + .map(|o| o.id.clone()); cx.spawn(async move |this, cx| { - llm_api_token.refresh(&client).await?; - let response = Self::fetch_models(client, llm_api_token).await?; + llm_api_token + .refresh(&client, organization_id.clone()) + .await?; + let response = + Self::fetch_models(client, llm_api_token, organization_id).await?; this.update(cx, |this, cx| { this.update_models(response, cx); }) @@ -209,9 +227,10 @@ impl State { async fn fetch_models( client: Arc, llm_api_token: LlmApiToken, + organization_id: Option, ) -> Result { let http_client = &client.http_client(); - let token = llm_api_token.acquire(&client).await?; + let token = llm_api_token.acquire(&client, organization_id).await?; let request = http_client::Request::builder() .method(Method::GET) @@ -273,11 +292,13 @@ impl CloudLanguageModelProvider { &self, model: Arc, llm_api_token: LlmApiToken, + user_store: Entity, ) -> Arc { Arc::new(CloudLanguageModel { id: LanguageModelId(SharedString::from(model.id.0.clone())), model, llm_api_token, + user_store, client: self.client.clone(), request_limiter: RateLimiter::new(4), }) @@ -306,36 +327,46 @@ impl LanguageModelProvider for CloudLanguageModelProvider { } fn default_model(&self, cx: &App) -> Option> { - let default_model = self.state.read(cx).default_model.clone()?; - let llm_api_token = self.state.read(cx).llm_api_token.clone(); - Some(self.create_language_model(default_model, llm_api_token)) + let state = self.state.read(cx); + let default_model = state.default_model.clone()?; + let llm_api_token = state.llm_api_token.clone(); + let user_store = state.user_store.clone(); + Some(self.create_language_model(default_model, llm_api_token, user_store)) } fn default_fast_model(&self, cx: &App) -> Option> { - let default_fast_model = self.state.read(cx).default_fast_model.clone()?; - let llm_api_token = self.state.read(cx).llm_api_token.clone(); - Some(self.create_language_model(default_fast_model, llm_api_token)) + let state = self.state.read(cx); + let default_fast_model = state.default_fast_model.clone()?; + let llm_api_token = state.llm_api_token.clone(); + let user_store = state.user_store.clone(); + Some(self.create_language_model(default_fast_model, llm_api_token, user_store)) } fn recommended_models(&self, cx: &App) -> Vec> { - let llm_api_token = self.state.read(cx).llm_api_token.clone(); - self.state - .read(cx) + let state = self.state.read(cx); + let llm_api_token = state.llm_api_token.clone(); + let user_store = state.user_store.clone(); + state .recommended_models .iter() .cloned() - .map(|model| self.create_language_model(model, llm_api_token.clone())) + .map(|model| { + self.create_language_model(model, llm_api_token.clone(), user_store.clone()) + }) .collect() } fn provided_models(&self, cx: &App) -> Vec> { - let llm_api_token = self.state.read(cx).llm_api_token.clone(); - self.state - .read(cx) + let state = self.state.read(cx); + let llm_api_token = state.llm_api_token.clone(); + let user_store = state.user_store.clone(); + state .models .iter() .cloned() - .map(|model| self.create_language_model(model, llm_api_token.clone())) + .map(|model| { + self.create_language_model(model, llm_api_token.clone(), user_store.clone()) + }) .collect() } @@ -367,6 +398,7 @@ pub struct CloudLanguageModel { id: LanguageModelId, model: Arc, llm_api_token: LlmApiToken, + user_store: Entity, client: Arc, request_limiter: RateLimiter, } @@ -380,12 +412,15 @@ impl CloudLanguageModel { async fn perform_llm_completion( client: Arc, llm_api_token: LlmApiToken, + organization_id: Option, app_version: Option, body: CompletionBody, ) -> Result { let http_client = &client.http_client(); - let mut token = llm_api_token.acquire(&client).await?; + let mut token = llm_api_token + .acquire(&client, organization_id.clone()) + .await?; let mut refreshed_token = false; loop { @@ -416,7 +451,9 @@ impl CloudLanguageModel { } if !refreshed_token && response.needs_llm_token_refresh() { - token = llm_api_token.refresh(&client).await?; + token = llm_api_token + .refresh(&client, organization_id.clone()) + .await?; refreshed_token = true; continue; } @@ -670,12 +707,17 @@ impl LanguageModel for CloudLanguageModel { cloud_llm_client::LanguageModelProvider::Google => { let client = self.client.clone(); let llm_api_token = self.llm_api_token.clone(); + let organization_id = self + .user_store + .read(cx) + .current_organization() + .map(|o| o.id.clone()); let model_id = self.model.id.to_string(); let generate_content_request = into_google(request, model_id.clone(), GoogleModelMode::Default); async move { let http_client = &client.http_client(); - let token = llm_api_token.acquire(&client).await?; + let token = llm_api_token.acquire(&client, organization_id).await?; let request_body = CountTokensBody { provider: cloud_llm_client::LanguageModelProvider::Google, @@ -736,6 +778,13 @@ impl LanguageModel for CloudLanguageModel { let prompt_id = request.prompt_id.clone(); let intent = request.intent; let app_version = Some(cx.update(|cx| AppVersion::global(cx))); + let user_store = self.user_store.clone(); + let organization_id = cx.update(|cx| { + user_store + .read(cx) + .current_organization() + .map(|o| o.id.clone()) + }); let thinking_allowed = request.thinking_allowed; let enable_thinking = thinking_allowed && self.model.supports_thinking; let provider_name = provider_name(&self.model.provider); @@ -767,6 +816,7 @@ impl LanguageModel for CloudLanguageModel { let client = self.client.clone(); let llm_api_token = self.llm_api_token.clone(); + let organization_id = organization_id.clone(); let future = self.request_limiter.stream(async move { let PerformLlmCompletionResponse { response, @@ -774,6 +824,7 @@ impl LanguageModel for CloudLanguageModel { } = Self::perform_llm_completion( client.clone(), llm_api_token, + organization_id, app_version, CompletionBody { thread_id, @@ -803,6 +854,7 @@ impl LanguageModel for CloudLanguageModel { cloud_llm_client::LanguageModelProvider::OpenAi => { let client = self.client.clone(); let llm_api_token = self.llm_api_token.clone(); + let organization_id = organization_id.clone(); let effort = request .thinking_effort .as_ref() @@ -828,6 +880,7 @@ impl LanguageModel for CloudLanguageModel { } = Self::perform_llm_completion( client.clone(), llm_api_token, + organization_id, app_version, CompletionBody { thread_id, @@ -861,6 +914,7 @@ impl LanguageModel for CloudLanguageModel { None, ); let llm_api_token = self.llm_api_token.clone(); + let organization_id = organization_id.clone(); let future = self.request_limiter.stream(async move { let PerformLlmCompletionResponse { response, @@ -868,6 +922,7 @@ impl LanguageModel for CloudLanguageModel { } = Self::perform_llm_completion( client.clone(), llm_api_token, + organization_id, app_version, CompletionBody { thread_id, @@ -902,6 +957,7 @@ impl LanguageModel for CloudLanguageModel { } = Self::perform_llm_completion( client.clone(), llm_api_token, + organization_id, app_version, CompletionBody { thread_id, diff --git a/crates/languages/src/cpp/config.toml b/crates/languages/src/cpp/config.toml index 10c36a6ded1e1f3a1204d1e15af47fee78b8e049..e2608a8ce5f17cb648e4f86dc27da60ed8bdd2ae 100644 --- a/crates/languages/src/cpp/config.toml +++ b/crates/languages/src/cpp/config.toml @@ -1,6 +1,6 @@ name = "C++" grammar = "cpp" -path_suffixes = ["cc", "hh", "cpp", "h", "hpp", "cxx", "hxx", "c++", "h++", "ipp", "inl", "ino", "ixx", "cu", "cuh", "C", "H"] +path_suffixes = ["cc", "hh", "cpp", "cppm", "h", "hpp", "cxx", "hxx", "c++", "h++", "ipp", "inl", "ino", "ixx", "cu", "cuh", "C", "H"] line_comments = ["// ", "/// ", "//! "] first_line_pattern = '^//.*-\*-\s*C\+\+\s*-\*-' decrease_indent_patterns = [ diff --git a/crates/languages/src/lib.rs b/crates/languages/src/lib.rs index c31911f372261db47f689d29de9c60c0f9cad56e..4c291b86982a8cb1aa153aa0c036b3d169621339 100644 --- a/crates/languages/src/lib.rs +++ b/crates/languages/src/lib.rs @@ -179,7 +179,13 @@ pub fn init(languages: Arc, fs: Arc, node: NodeRuntime }, LanguageInfo { name: "python", - adapters: vec![basedpyright_lsp_adapter, ruff_lsp_adapter], + adapters: vec![ + basedpyright_lsp_adapter, + ruff_lsp_adapter, + ty_lsp_adapter, + py_lsp_adapter, + python_lsp_adapter, + ], context: Some(python_context_provider), toolchain: Some(python_toolchain_provider), manifest_name: Some(SharedString::new_static("pyproject.toml").into()), @@ -281,9 +287,6 @@ pub fn init(languages: Arc, fs: Arc, node: NodeRuntime typescript_lsp_adapter, ); - languages.register_available_lsp_adapter(python_lsp_adapter.name(), python_lsp_adapter); - languages.register_available_lsp_adapter(py_lsp_adapter.name(), py_lsp_adapter); - languages.register_available_lsp_adapter(ty_lsp_adapter.name(), ty_lsp_adapter); // Register Tailwind for the existing languages that should have it by default. // // This can be driven by the `language_servers` setting once we have a way for diff --git a/crates/miniprofiler_ui/src/miniprofiler_ui.rs b/crates/miniprofiler_ui/src/miniprofiler_ui.rs index 1f95dc3d230e7c50b4960560a96c9007fd77aab8..12b2bce77b5866e885483a847d40647f525207e6 100644 --- a/crates/miniprofiler_ui/src/miniprofiler_ui.rs +++ b/crates/miniprofiler_ui/src/miniprofiler_ui.rs @@ -544,7 +544,7 @@ impl Render for ProfilerWindow { let path = cx.prompt_for_new_path( &active_path, - Some("performance_profile.miniprof"), + Some("performance_profile.miniprof.json"), ); cx.background_spawn(async move { diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index 487e7f5f9699382ce4930141f7a0c7c50a1d23b8..b03c7d69ab05daf94254a9d47cb2ae23da3043d1 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -6,6 +6,9 @@ pub mod pending_op; use crate::{ ProjectEnvironment, ProjectItem, ProjectPath, buffer_store::{BufferStore, BufferStoreEvent}, + trusted_worktrees::{ + PathTrust, TrustedWorktrees, TrustedWorktreesEvent, TrustedWorktreesStore, + }, worktree_store::{WorktreeStore, WorktreeStoreEvent}, }; use anyhow::{Context as _, Result, anyhow, bail}; @@ -354,6 +357,7 @@ impl LocalRepositoryState { dot_git_abs_path: Arc, project_environment: WeakEntity, fs: Arc, + is_trusted: bool, cx: &mut AsyncApp, ) -> anyhow::Result { let environment = project_environment @@ -381,6 +385,7 @@ impl LocalRepositoryState { } }) .await?; + backend.set_trusted(is_trusted); Ok(LocalRepositoryState { backend, environment: Arc::new(environment), @@ -495,11 +500,15 @@ impl GitStore { state: GitStoreState, cx: &mut Context, ) -> Self { - let _subscriptions = vec![ + let mut _subscriptions = vec![ cx.subscribe(&worktree_store, Self::on_worktree_store_event), cx.subscribe(&buffer_store, Self::on_buffer_store_event), ]; + if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) { + _subscriptions.push(cx.subscribe(&trusted_worktrees, Self::on_trusted_worktrees_event)); + } + GitStore { state, buffer_store, @@ -1517,6 +1526,13 @@ impl GitStore { let original_repo_abs_path: Arc = git::repository::original_repo_path_from_common_dir(common_dir_abs_path).into(); let id = RepositoryId(next_repository_id.fetch_add(1, atomic::Ordering::Release)); + let is_trusted = TrustedWorktrees::try_get_global(cx) + .map(|trusted_worktrees| { + trusted_worktrees.update(cx, |trusted_worktrees, cx| { + trusted_worktrees.can_trust(&self.worktree_store, worktree_id, cx) + }) + }) + .unwrap_or(false); let git_store = cx.weak_entity(); let repo = cx.new(|cx| { let mut repo = Repository::local( @@ -1526,6 +1542,7 @@ impl GitStore { dot_git_abs_path.clone(), project_environment.downgrade(), fs.clone(), + is_trusted, git_store, cx, ); @@ -1566,6 +1583,39 @@ impl GitStore { } } + fn on_trusted_worktrees_event( + &mut self, + _: Entity, + event: &TrustedWorktreesEvent, + cx: &mut Context, + ) { + if !matches!(self.state, GitStoreState::Local { .. }) { + return; + } + + let (is_trusted, event_paths) = match event { + TrustedWorktreesEvent::Trusted(_, trusted_paths) => (true, trusted_paths), + TrustedWorktreesEvent::Restricted(_, restricted_paths) => (false, restricted_paths), + }; + + for (repo_id, worktree_ids) in &self.worktree_ids { + if worktree_ids + .iter() + .any(|worktree_id| event_paths.contains(&PathTrust::Worktree(*worktree_id))) + { + if let Some(repo) = self.repositories.get(repo_id) { + let repository_state = repo.read(cx).repository_state.clone(); + cx.background_spawn(async move { + if let Ok(RepositoryState::Local(state)) = repository_state.await { + state.backend.set_trusted(is_trusted); + } + }) + .detach(); + } + } + } + } + fn on_buffer_store_event( &mut self, _: Entity, @@ -3763,6 +3813,13 @@ impl MergeDetails { } impl Repository { + pub fn is_trusted(&self) -> bool { + match self.repository_state.peek() { + Some(Ok(RepositoryState::Local(state))) => state.backend.is_trusted(), + _ => false, + } + } + pub fn snapshot(&self) -> RepositorySnapshot { self.snapshot.clone() } @@ -3788,6 +3845,7 @@ impl Repository { dot_git_abs_path: Arc, project_environment: WeakEntity, fs: Arc, + is_trusted: bool, git_store: WeakEntity, cx: &mut Context, ) -> Self { @@ -3804,6 +3862,7 @@ impl Repository { dot_git_abs_path, project_environment, fs, + is_trusted, cx, ) .await diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 9e37802213dfb8df5cf63af5648044ae8ec65ecb..756f095511a9688678df013458710e69d720c52e 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1942,6 +1942,11 @@ impl Project { } } + #[cfg(feature = "test-support")] + pub fn client_subscriptions(&self) -> &Vec { + &self.client_subscriptions + } + #[cfg(feature = "test-support")] pub async fn example( root_paths: impl IntoIterator, @@ -2741,6 +2746,7 @@ impl Project { } = &mut self.client_state { *sharing_has_stopped = true; + self.client_subscriptions.clear(); self.collaborators.clear(); self.worktree_store.update(cx, |store, cx| { store.disconnected_from_host(cx); diff --git a/crates/project/tests/integration/git_store.rs b/crates/project/tests/integration/git_store.rs index 802e0c072bf60466c32146d12cadd7c1e35c61ad..82e92bc4f1cfb606fb09d5efd5d341ed2951c067 100644 --- a/crates/project/tests/integration/git_store.rs +++ b/crates/project/tests/integration/git_store.rs @@ -1174,3 +1174,327 @@ mod git_traversal { pretty_assertions::assert_eq!(found_statuses, expected_statuses); } } + +mod git_worktrees { + use std::path::PathBuf; + + use fs::FakeFs; + use gpui::TestAppContext; + use serde_json::json; + use settings::SettingsStore; + use util::path; + + fn init_test(cx: &mut gpui::TestAppContext) { + zlog::init_test(); + + cx.update(|cx| { + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); + }); + } + + #[gpui::test] + async fn test_git_worktrees_list_and_create(cx: &mut TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + path!("/root"), + json!({ + ".git": {}, + "file.txt": "content", + }), + ) + .await; + + let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; + cx.executor().run_until_parked(); + + let repository = project.read_with(cx, |project, cx| { + project.repositories(cx).values().next().unwrap().clone() + }); + + let worktrees = cx + .update(|cx| repository.update(cx, |repository, _| repository.worktrees())) + .await + .unwrap() + .unwrap(); + assert_eq!(worktrees.len(), 1); + assert_eq!(worktrees[0].path, PathBuf::from(path!("/root"))); + + let worktree_directory = PathBuf::from(path!("/root")); + cx.update(|cx| { + repository.update(cx, |repository, _| { + repository.create_worktree( + "feature-branch".to_string(), + worktree_directory.clone(), + Some("abc123".to_string()), + ) + }) + }) + .await + .unwrap() + .unwrap(); + + cx.executor().run_until_parked(); + + let worktrees = cx + .update(|cx| repository.update(cx, |repository, _| repository.worktrees())) + .await + .unwrap() + .unwrap(); + assert_eq!(worktrees.len(), 2); + assert_eq!(worktrees[0].path, PathBuf::from(path!("/root"))); + assert_eq!(worktrees[1].path, worktree_directory.join("feature-branch")); + assert_eq!(worktrees[1].ref_name.as_ref(), "refs/heads/feature-branch"); + assert_eq!(worktrees[1].sha.as_ref(), "abc123"); + + cx.update(|cx| { + repository.update(cx, |repository, _| { + repository.create_worktree( + "bugfix-branch".to_string(), + worktree_directory.clone(), + None, + ) + }) + }) + .await + .unwrap() + .unwrap(); + + cx.executor().run_until_parked(); + + // List worktrees — should now have main + two created + let worktrees = cx + .update(|cx| repository.update(cx, |repository, _| repository.worktrees())) + .await + .unwrap() + .unwrap(); + assert_eq!(worktrees.len(), 3); + + let feature_worktree = worktrees + .iter() + .find(|worktree| worktree.ref_name.as_ref() == "refs/heads/feature-branch") + .expect("should find feature-branch worktree"); + assert_eq!( + feature_worktree.path, + worktree_directory.join("feature-branch") + ); + + let bugfix_worktree = worktrees + .iter() + .find(|worktree| worktree.ref_name.as_ref() == "refs/heads/bugfix-branch") + .expect("should find bugfix-branch worktree"); + assert_eq!( + bugfix_worktree.path, + worktree_directory.join("bugfix-branch") + ); + assert_eq!(bugfix_worktree.sha.as_ref(), "fake-sha"); + } + + use crate::Project; +} + +mod trust_tests { + use collections::HashSet; + use fs::FakeFs; + use gpui::TestAppContext; + use project::trusted_worktrees::*; + + use serde_json::json; + use settings::SettingsStore; + use util::path; + + use crate::Project; + + fn init_test(cx: &mut TestAppContext) { + zlog::init_test(); + + cx.update(|cx| { + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); + }); + } + + #[gpui::test] + async fn test_repository_defaults_to_untrusted_without_trust_system(cx: &mut TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + path!("/project"), + json!({ + ".git": {}, + "a.txt": "hello", + }), + ) + .await; + + // Create project without trust system — repos should default to untrusted. + let project = Project::test(fs, [path!("/project").as_ref()], cx).await; + cx.executor().run_until_parked(); + + let repository = project.read_with(cx, |project, cx| { + project.repositories(cx).values().next().unwrap().clone() + }); + + repository.read_with(cx, |repo, _| { + assert!( + !repo.is_trusted(), + "repository should default to untrusted when no trust system is initialized" + ); + }); + } + + #[gpui::test] + async fn test_multiple_repos_trust_with_single_worktree(cx: &mut TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + path!("/project"), + json!({ + ".git": {}, + "a.txt": "hello", + "sub": { + ".git": {}, + "b.txt": "world", + }, + }), + ) + .await; + + cx.update(|cx| { + init(DbTrustedPaths::default(), cx); + }); + + let project = + Project::test_with_worktree_trust(fs.clone(), [path!("/project").as_ref()], cx).await; + cx.executor().run_until_parked(); + + let worktree_store = project.read_with(cx, |project, _| project.worktree_store()); + let worktree_id = worktree_store.read_with(cx, |store, cx| { + store.worktrees().next().unwrap().read(cx).id() + }); + + let repos = project.read_with(cx, |project, cx| { + project + .repositories(cx) + .values() + .cloned() + .collect::>() + }); + assert_eq!(repos.len(), 2, "should have two repositories"); + for repo in &repos { + repo.read_with(cx, |repo, _| { + assert!( + !repo.is_trusted(), + "all repos should be untrusted initially" + ); + }); + } + + let trusted_worktrees = cx + .update(|cx| TrustedWorktrees::try_get_global(cx).expect("trust global should be set")); + trusted_worktrees.update(cx, |store, cx| { + store.trust( + &worktree_store, + HashSet::from_iter([PathTrust::Worktree(worktree_id)]), + cx, + ); + }); + cx.executor().run_until_parked(); + + for repo in &repos { + repo.read_with(cx, |repo, _| { + assert!( + repo.is_trusted(), + "all repos should be trusted after worktree is trusted" + ); + }); + } + } + + #[gpui::test] + async fn test_repository_trust_restrict_trust_cycle(cx: &mut TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + path!("/project"), + json!({ + ".git": {}, + "a.txt": "hello", + }), + ) + .await; + + cx.update(|cx| { + project::trusted_worktrees::init(DbTrustedPaths::default(), cx); + }); + + let project = + Project::test_with_worktree_trust(fs.clone(), [path!("/project").as_ref()], cx).await; + cx.executor().run_until_parked(); + + let worktree_store = project.read_with(cx, |project, _| project.worktree_store()); + let worktree_id = worktree_store.read_with(cx, |store, cx| { + store.worktrees().next().unwrap().read(cx).id() + }); + + let repository = project.read_with(cx, |project, cx| { + project.repositories(cx).values().next().unwrap().clone() + }); + + repository.read_with(cx, |repo, _| { + assert!(!repo.is_trusted(), "repository should start untrusted"); + }); + + let trusted_worktrees = cx + .update(|cx| TrustedWorktrees::try_get_global(cx).expect("trust global should be set")); + + trusted_worktrees.update(cx, |store, cx| { + store.trust( + &worktree_store, + HashSet::from_iter([PathTrust::Worktree(worktree_id)]), + cx, + ); + }); + cx.executor().run_until_parked(); + + repository.read_with(cx, |repo, _| { + assert!( + repo.is_trusted(), + "repository should be trusted after worktree is trusted" + ); + }); + + trusted_worktrees.update(cx, |store, cx| { + store.restrict( + worktree_store.downgrade(), + HashSet::from_iter([PathTrust::Worktree(worktree_id)]), + cx, + ); + }); + cx.executor().run_until_parked(); + + repository.read_with(cx, |repo, _| { + assert!( + !repo.is_trusted(), + "repository should be untrusted after worktree is restricted" + ); + }); + + trusted_worktrees.update(cx, |store, cx| { + store.trust( + &worktree_store, + HashSet::from_iter([PathTrust::Worktree(worktree_id)]), + cx, + ); + }); + cx.executor().run_until_parked(); + + repository.read_with(cx, |repo, _| { + assert!( + repo.is_trusted(), + "repository should be trusted again after second trust" + ); + }); + } +} diff --git a/crates/project/tests/integration/project_tests.rs b/crates/project/tests/integration/project_tests.rs index 6092836c19ef280aa2d13abcb32932f3b47703b6..d597377910a2a837e456ac4384b06c333887dfb3 100644 --- a/crates/project/tests/integration/project_tests.rs +++ b/crates/project/tests/integration/project_tests.rs @@ -5359,6 +5359,52 @@ async fn test_rescan_and_remote_updates(cx: &mut gpui::TestAppContext) { }); } +#[cfg(target_os = "linux")] +#[gpui::test(retries = 5)] +async fn test_recreated_directory_receives_child_events(cx: &mut gpui::TestAppContext) { + init_test(cx); + cx.executor().allow_parking(); + + let dir = TempTree::new(json!({})); + let project = Project::test(Arc::new(RealFs::new(None, cx.executor())), [dir.path()], cx).await; + let tree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap()); + + tree.flush_fs_events(cx).await; + + let repro_dir = dir.path().join("repro"); + std::fs::create_dir(&repro_dir).unwrap(); + tree.flush_fs_events(cx).await; + + cx.update(|cx| { + assert!(tree.read(cx).entry_for_path(rel_path("repro")).is_some()); + }); + + std::fs::remove_dir_all(&repro_dir).unwrap(); + tree.flush_fs_events(cx).await; + + cx.update(|cx| { + assert!(tree.read(cx).entry_for_path(rel_path("repro")).is_none()); + }); + + std::fs::create_dir(&repro_dir).unwrap(); + tree.flush_fs_events(cx).await; + + cx.update(|cx| { + assert!(tree.read(cx).entry_for_path(rel_path("repro")).is_some()); + }); + + std::fs::write(repro_dir.join("repro-marker"), "").unwrap(); + tree.flush_fs_events(cx).await; + + cx.update(|cx| { + assert!( + tree.read(cx) + .entry_for_path(rel_path("repro/repro-marker")) + .is_some() + ); + }); +} + #[gpui::test(iterations = 10)] async fn test_buffer_identity_across_renames(cx: &mut gpui::TestAppContext) { init_test(cx); diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 7f746a6ccd7efec2b73354992c593433b0b6f281..0dd19dddde7ab947cfe85a1fd9d96ad7b2d6f23d 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -6457,11 +6457,14 @@ impl Render for ProjectPanel { el.on_action(cx.listener(Self::trash)) }) }) - .when(project.is_local(), |el| { - el.on_action(cx.listener(Self::reveal_in_finder)) - .on_action(cx.listener(Self::open_system)) - .on_action(cx.listener(Self::open_in_terminal)) - }) + .when( + project.is_local() || project.is_via_wsl_with_host_interop(cx), + |el| { + el.on_action(cx.listener(Self::reveal_in_finder)) + .on_action(cx.listener(Self::open_system)) + .on_action(cx.listener(Self::open_in_terminal)) + }, + ) .when(project.is_via_remote_server(), |el| { el.on_action(cx.listener(Self::open_in_terminal)) .on_action(cx.listener(Self::download_from_remote)) diff --git a/crates/recent_projects/src/remote_servers.rs b/crates/recent_projects/src/remote_servers.rs index 6c0ce4b18854320fda8e72f291800049b07cec1a..a94f7b1d57eaef8657fb0d448480f84c97ce7e70 100644 --- a/crates/recent_projects/src/remote_servers.rs +++ b/crates/recent_projects/src/remote_servers.rs @@ -1161,12 +1161,11 @@ impl RemoteServerProjects { workspace.toggle_modal(window, cx, |window, cx| { RemoteConnectionModal::new(&connection_options, Vec::new(), window, cx) }); - let prompt = workspace - .active_modal::(cx) - .unwrap() - .read(cx) - .prompt - .clone(); + // can be None if another copy of this modal opened in the meantime + let Some(modal) = workspace.active_modal::(cx) else { + return; + }; + let prompt = modal.read(cx).prompt.clone(); let connect = connect( ConnectionIdentifier::setup(), diff --git a/crates/remote_server/src/remote_editing_tests.rs b/crates/remote_server/src/remote_editing_tests.rs index 778f7292d2a032df6995169852deeecee6fa76a7..9b9fe9948ace530d7e55d2843952ca5c9efb3749 100644 --- a/crates/remote_server/src/remote_editing_tests.rs +++ b/crates/remote_server/src/remote_editing_tests.rs @@ -2,15 +2,12 @@ /// The tests in this file assume that server_cx is running on Windows too. /// We neead to find a way to test Windows-Non-Windows interactions. use crate::headless_project::HeadlessProject; -use agent::{ - AgentTool, ReadFileTool, ReadFileToolInput, Templates, Thread, ToolCallEventStream, ToolInput, -}; +use agent::{AgentTool, ReadFileTool, ReadFileToolInput, ToolCallEventStream, ToolInput}; use client::{Client, UserStore}; use clock::FakeSystemClock; use collections::{HashMap, HashSet}; use git::repository::DiffType; -use language_model::{LanguageModelToolResultContent, fake_provider::FakeLanguageModel}; -use prompt_store::ProjectContext; +use language_model::LanguageModelToolResultContent; use extension::ExtensionHostProxy; use fs::{FakeFs, Fs}; @@ -2065,27 +2062,12 @@ async fn test_remote_agent_fs_tool_calls(cx: &mut TestAppContext, server_cx: &mu let action_log = cx.new(|_| action_log::ActionLog::new(project.clone())); - // Create a minimal thread for the ReadFileTool - let context_server_registry = - cx.new(|cx| agent::ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); - let model = Arc::new(FakeLanguageModel::default()); - let thread = cx.new(|cx| { - Thread::new( - project.clone(), - cx.new(|_cx| ProjectContext::default()), - context_server_registry, - Templates::new(), - Some(model), - cx, - ) - }); - let input = ReadFileToolInput { path: "project/b.txt".into(), start_line: None, end_line: None, }; - let read_tool = Arc::new(ReadFileTool::new(thread.downgrade(), project, action_log)); + let read_tool = Arc::new(ReadFileTool::new(project, action_log, true)); let (event_stream, _) = ToolCallEventStream::test(); let exists_result = cx.update(|cx| { diff --git a/crates/repl/src/notebook/cell.rs b/crates/repl/src/notebook/cell.rs index d66261698b722cfcd0f547e09d84cf83a0d2b1a6..200424742aff113d637fe9aca30999c0f95e79a5 100644 --- a/crates/repl/src/notebook/cell.rs +++ b/crates/repl/src/notebook/cell.rs @@ -1,13 +1,11 @@ -#![allow(unused, dead_code)] use std::sync::Arc; use std::time::{Duration, Instant}; use editor::{Editor, EditorMode, MultiBuffer, SizingBehavior}; use futures::future::Shared; use gpui::{ - App, Entity, EventEmitter, Focusable, Hsla, InteractiveElement, KeyContext, - RetainAllImageCache, StatefulInteractiveElement, Task, TextStyleRefinement, image_cache, - prelude::*, + App, Entity, EventEmitter, Focusable, Hsla, InteractiveElement, RetainAllImageCache, + StatefulInteractiveElement, Task, TextStyleRefinement, prelude::*, }; use language::{Buffer, Language, LanguageRegistry}; use markdown::{Markdown, MarkdownElement, MarkdownStyle}; @@ -236,7 +234,7 @@ pub trait RenderableCell: Render { fn source(&self) -> &String; fn selected(&self) -> bool; fn set_selected(&mut self, selected: bool) -> &mut Self; - fn selected_bg_color(&self, window: &mut Window, cx: &mut Context) -> Hsla { + fn selected_bg_color(&self, _window: &mut Window, cx: &mut Context) -> Hsla { if self.selected() { let mut color = cx.theme().colors().element_hover; color.fade_out(0.5); @@ -253,7 +251,7 @@ pub trait RenderableCell: Render { fn cell_position_spacer( &self, is_first: bool, - window: &mut Window, + _window: &mut Window, cx: &mut Context, ) -> Option { let cell_position = self.cell_position(); @@ -328,7 +326,6 @@ pub struct MarkdownCell { editing: bool, selected: bool, cell_position: Option, - languages: Arc, _editor_subscription: gpui::Subscription, } @@ -386,7 +383,6 @@ impl MarkdownCell { let markdown = cx.new(|cx| Markdown::new(source.clone().into(), None, None, cx)); - let cell_id = id.clone(); let editor_subscription = cx.subscribe(&editor, move |this, _editor, event, cx| match event { editor::EditorEvent::Blurred => { @@ -410,7 +406,6 @@ impl MarkdownCell { editing: start_editing, selected: false, cell_position: None, - languages, _editor_subscription: editor_subscription, } } @@ -461,8 +456,6 @@ impl MarkdownCell { .unwrap_or_default(); self.source = source.clone(); - let languages = self.languages.clone(); - self.markdown.update(cx, |markdown, cx| { markdown.reset(source.into(), cx); }); @@ -606,7 +599,7 @@ pub struct CodeCell { outputs: Vec, selected: bool, cell_position: Option, - language_task: Task<()>, + _language_task: Task<()>, execution_start_time: Option, execution_duration: Option, is_executing: bool, @@ -670,10 +663,10 @@ impl CodeCell { outputs: Vec::new(), selected: false, cell_position: None, - language_task, execution_start_time: None, execution_duration: None, is_executing: false, + _language_task: language_task, } } @@ -748,10 +741,10 @@ impl CodeCell { outputs, selected: false, cell_position: None, - language_task, execution_start_time: None, execution_duration: None, is_executing: false, + _language_task: language_task, } } @@ -879,15 +872,7 @@ impl CodeCell { cx.notify(); } - fn output_control(&self) -> Option { - if self.has_outputs() { - Some(CellControlType::ClearCell) - } else { - None - } - } - - pub fn gutter_output(&self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + pub fn gutter_output(&self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let is_selected = self.selected(); div() @@ -948,7 +933,7 @@ impl RenderableCell for CodeCell { &self.source } - fn control(&self, window: &mut Window, cx: &mut Context) -> Option { + fn control(&self, _window: &mut Window, cx: &mut Context) -> Option { let control_type = if self.has_outputs() { CellControlType::RerunCell } else { @@ -1038,8 +1023,7 @@ impl RenderableCell for CodeCell { } impl RunnableCell for CodeCell { - fn run(&mut self, window: &mut Window, cx: &mut Context) { - println!("Running code cell: {}", self.id); + fn run(&mut self, _window: &mut Window, cx: &mut Context) { cx.emit(CellEvent::Run(self.id.clone())); } @@ -1062,11 +1046,8 @@ impl Render for CodeCell { } else { None }; - let output_max_width = plain::max_width_for_columns( - ReplSettings::get_global(cx).output_max_width_columns, - window, - cx, - ); + let output_max_width = + plain::max_width_for_columns(ReplSettings::get_global(cx).max_columns, window, cx); // get the language from the editor's buffer let language_name = self .editor @@ -1198,41 +1179,23 @@ impl Render for CodeCell { }, ) // output at bottom - .child(div().w_full().children(self.outputs.iter().map( - |output| { - let content = match output { - Output::Plain { content, .. } => { - Some(content.clone().into_any_element()) - } - Output::Markdown { content, .. } => { - Some(content.clone().into_any_element()) - } - Output::Stream { content, .. } => { - Some(content.clone().into_any_element()) - } - Output::Image { content, .. } => { - Some(content.clone().into_any_element()) - } - Output::Message(message) => Some( - div() - .child(message.clone()) - .into_any_element(), - ), - Output::Table { content, .. } => { - Some(content.clone().into_any_element()) - } - Output::Json { content, .. } => { - Some(content.clone().into_any_element()) - } - Output::ErrorOutput(error_view) => { - error_view.render(window, cx) - } - Output::ClearOutputWaitMarker => None, - }; - - div().children(content) - }, - ))), + .child( + div() + .id(( + ElementId::from(self.id.to_string()), + "output-scroll", + )) + .w_full() + .when_some(output_max_width, |div, max_width| { + div.max_w(max_width).overflow_x_scroll() + }) + .when_some(output_max_height, |div, max_height| { + div.max_h(max_height).overflow_y_scroll() + }) + .children(self.outputs.iter().map(|output| { + div().children(output.content(window, cx)) + })), + ), ), ), ) diff --git a/crates/repl/src/outputs.rs b/crates/repl/src/outputs.rs index 8be8c57cceee84435a6d99ba5c611d24c563bec3..f6d2bc4d3173ce64700b7b5ac45301df0fe0ab53 100644 --- a/crates/repl/src/outputs.rs +++ b/crates/repl/src/outputs.rs @@ -253,18 +253,8 @@ impl Output { ) } - pub fn render( - &self, - workspace: WeakEntity, - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement + use<> { - let max_width = plain::max_width_for_columns( - ReplSettings::get_global(cx).output_max_width_columns, - window, - cx, - ); - let content = match self { + pub fn content(&self, window: &mut Window, cx: &mut App) -> Option { + match self { Self::Plain { content, .. } => Some(content.clone().into_any_element()), Self::Markdown { content, .. } => Some(content.clone().into_any_element()), Self::Stream { content, .. } => Some(content.clone().into_any_element()), @@ -274,21 +264,36 @@ impl Output { Self::Json { content, .. } => Some(content.clone().into_any_element()), Self::ErrorOutput(error_view) => error_view.render(window, cx), Self::ClearOutputWaitMarker => None, - }; + } + } - let needs_horizontal_scroll = matches!(self, Self::Table { .. } | Self::Image { .. }); + pub fn render( + &self, + workspace: WeakEntity, + window: &mut Window, + cx: &mut Context, + ) -> impl IntoElement + use<> { + let max_width = + plain::max_width_for_columns(ReplSettings::get_global(cx).max_columns, window, cx); + let content = self.content(window, cx); + + let needs_horizontal_scroll = matches!(self, Self::Table { .. }); h_flex() .id("output-content") .w_full() - .when_some(max_width, |this, max_w| this.max_w(max_w)) - .overflow_x_scroll() + .when_else( + needs_horizontal_scroll, + |this| this.overflow_x_scroll(), + |this| this.overflow_x_hidden(), + ) .items_start() .child( div() .when(!needs_horizontal_scroll, |el| { el.flex_1().w_full().overflow_x_hidden() }) + .when_some(max_width, |el, max_width| el.max_w(max_width)) .children(content), ) .children(match self { diff --git a/crates/repl/src/outputs/image.rs b/crates/repl/src/outputs/image.rs index 9d1ffa3d2065281cd69e67b2faf960c9aa690bcb..e5444be3d779c9541fcadd55b9255d3e25da0cba 100644 --- a/crates/repl/src/outputs/image.rs +++ b/crates/repl/src/outputs/image.rs @@ -3,10 +3,10 @@ use base64::{ Engine as _, alphabet, engine::{DecodePaddingMode, GeneralPurpose, GeneralPurposeConfig}, }; -use gpui::{App, ClipboardItem, Image, ImageFormat, RenderImage, Window, img}; +use gpui::{App, ClipboardItem, Image, ImageFormat, Pixels, RenderImage, Window, img}; use settings::Settings as _; use std::sync::Arc; -use ui::{IntoElement, Styled, div, prelude::*}; +use ui::{IntoElement, Styled, prelude::*}; use crate::outputs::{OutputContent, plain}; use crate::repl_settings::ReplSettings; @@ -113,7 +113,7 @@ impl Render for ImageView { let settings = ReplSettings::get_global(cx); let line_height = window.line_height(); - let max_width = plain::max_width_for_columns(settings.output_max_width_columns, window, cx); + let max_width = plain::max_width_for_columns(settings.max_columns, window, cx); let max_height = if settings.output_max_height_lines > 0 { Some(line_height * settings.output_max_height_lines as f32) @@ -125,7 +125,7 @@ impl Render for ImageView { let image = self.image.clone(); - div().h(height).w(width).child(img(image)) + img(image).w(width).h(height) } } diff --git a/crates/repl/src/outputs/plain.rs b/crates/repl/src/outputs/plain.rs index 0db2f811fb9ca3b82114db23826e37fe699bd3a0..71e2624f8ad7b0172a86793d5d81b38339b04f36 100644 --- a/crates/repl/src/outputs/plain.rs +++ b/crates/repl/src/outputs/plain.rs @@ -22,7 +22,7 @@ use alacritty_terminal::{ term::Config, vte::ansi::Processor, }; -use gpui::{Bounds, ClipboardItem, Entity, FontStyle, TextStyle, WhiteSpace, canvas, size}; +use gpui::{Bounds, ClipboardItem, Entity, FontStyle, Pixels, TextStyle, WhiteSpace, canvas, size}; use language::Buffer; use settings::Settings as _; use terminal::terminal_settings::TerminalSettings; diff --git a/crates/repl/src/repl_settings.rs b/crates/repl/src/repl_settings.rs index 302164a5b360157edceff1b1f2e18f6c6fd7a50b..5fd7623bb71e6446b8cacd6029108e481efc8680 100644 --- a/crates/repl/src/repl_settings.rs +++ b/crates/repl/src/repl_settings.rs @@ -27,11 +27,6 @@ pub struct ReplSettings { /// /// Default: 0 pub output_max_height_lines: usize, - /// Maximum number of columns of output to display before scaling images. - /// Set to 0 to disable output width limits. - /// - /// Default: 0 - pub output_max_width_columns: usize, } impl Settings for ReplSettings { @@ -44,7 +39,6 @@ impl Settings for ReplSettings { inline_output: repl.inline_output.unwrap_or(true), inline_output_max_length: repl.inline_output_max_length.unwrap_or(50), output_max_height_lines: repl.output_max_height_lines.unwrap_or(0), - output_max_width_columns: repl.output_max_width_columns.unwrap_or(0), } } } diff --git a/crates/settings_content/src/language.rs b/crates/settings_content/src/language.rs index d429f53824fd0f4f0a5810bce01b05badcfb9a51..a8d68fea99c024830ee45c66ec5d7d641aa4c250 100644 --- a/crates/settings_content/src/language.rs +++ b/crates/settings_content/src/language.rs @@ -90,7 +90,7 @@ pub enum EditPredictionProvider { Experimental(&'static str), } -pub const EXPERIMENTAL_ZETA2_EDIT_PREDICTION_PROVIDER_NAME: &str = "zeta2"; +const EXPERIMENTAL_ZETA2_EDIT_PREDICTION_PROVIDER_NAME: &str = "zeta2"; impl<'de> Deserialize<'de> for EditPredictionProvider { fn deserialize(deserializer: D) -> Result @@ -157,10 +157,7 @@ impl EditPredictionProvider { EditPredictionProvider::Codestral => Some("Codestral"), EditPredictionProvider::Sweep => Some("Sweep"), EditPredictionProvider::Mercury => Some("Mercury"), - EditPredictionProvider::Experimental( - EXPERIMENTAL_ZETA2_EDIT_PREDICTION_PROVIDER_NAME, - ) => Some("Zeta2"), - EditPredictionProvider::None | EditPredictionProvider::Experimental(_) => None, + EditPredictionProvider::Experimental(_) | EditPredictionProvider::None => None, EditPredictionProvider::Ollama => Some("Ollama"), EditPredictionProvider::OpenAiCompatibleApi => Some("OpenAI-Compatible API"), } diff --git a/crates/settings_content/src/settings_content.rs b/crates/settings_content/src/settings_content.rs index f94c6a0b98d7fa23686dc1c89012e3b1fe476c70..5a4e87c384d802f3de4c96c07f65cf163c3a6d1a 100644 --- a/crates/settings_content/src/settings_content.rs +++ b/crates/settings_content/src/settings_content.rs @@ -1148,11 +1148,6 @@ pub struct ReplSettingsContent { /// /// Default: 0 pub output_max_height_lines: Option, - /// Maximum number of columns of output to display before scaling images. - /// Set to 0 to disable output width limits. - /// - /// Default: 0 - pub output_max_width_columns: Option, } /// Settings for configuring the which-key popup behaviour. diff --git a/crates/settings_ui/src/pages/edit_prediction_provider_setup.rs b/crates/settings_ui/src/pages/edit_prediction_provider_setup.rs index 338fe4de14f1f7e9060fafe865253f09f0bdc481..32c4bee84bd1f72263ed28bcd44d7e6349c4b24c 100644 --- a/crates/settings_ui/src/pages/edit_prediction_provider_setup.rs +++ b/crates/settings_ui/src/pages/edit_prediction_provider_setup.rs @@ -2,6 +2,7 @@ use codestral::{CODESTRAL_API_URL, codestral_api_key_state, codestral_api_url}; use edit_prediction::{ ApiKeyState, mercury::{MERCURY_CREDENTIALS_URL, mercury_api_token}, + open_ai_compatible::{open_ai_compatible_api_token, open_ai_compatible_api_url}, sweep_ai::{SWEEP_CREDENTIALS_URL, sweep_api_token}, }; use edit_prediction_ui::{get_available_providers, set_completion_provider}; @@ -33,7 +34,9 @@ pub(crate) fn render_edit_prediction_setup_page( render_api_key_provider( IconName::Inception, "Mercury", - "https://platform.inceptionlabs.ai/dashboard/api-keys".into(), + ApiKeyDocs::Link { + dashboard_url: "https://platform.inceptionlabs.ai/dashboard/api-keys".into(), + }, mercury_api_token(cx), |_cx| MERCURY_CREDENTIALS_URL, None, @@ -46,7 +49,9 @@ pub(crate) fn render_edit_prediction_setup_page( render_api_key_provider( IconName::SweepAi, "Sweep", - "https://app.sweep.dev/".into(), + ApiKeyDocs::Link { + dashboard_url: "https://app.sweep.dev/".into(), + }, sweep_api_token(cx), |_cx| SWEEP_CREDENTIALS_URL, Some( @@ -68,7 +73,9 @@ pub(crate) fn render_edit_prediction_setup_page( render_api_key_provider( IconName::AiMistral, "Codestral", - "https://console.mistral.ai/codestral".into(), + ApiKeyDocs::Link { + dashboard_url: "https://console.mistral.ai/codestral".into(), + }, codestral_api_key_state(cx), |cx| codestral_api_url(cx), Some( @@ -87,7 +94,31 @@ pub(crate) fn render_edit_prediction_setup_page( .into_any_element(), ), Some(render_ollama_provider(settings_window, window, cx).into_any_element()), - Some(render_open_ai_compatible_provider(settings_window, window, cx).into_any_element()), + Some( + render_api_key_provider( + IconName::AiOpenAiCompat, + "OpenAI Compatible API", + ApiKeyDocs::Custom { + message: "Set an API key here. It will be sent as Authorization: Bearer {key}." + .into(), + }, + open_ai_compatible_api_token(cx), + |cx| open_ai_compatible_api_url(cx), + Some( + settings_window + .render_sub_page_items_section( + open_ai_compatible_settings().iter().enumerate(), + true, + window, + cx, + ) + .into_any_element(), + ), + window, + cx, + ) + .into_any_element(), + ), ]; div() @@ -162,10 +193,15 @@ fn render_provider_dropdown(window: &mut Window, cx: &mut App) -> AnyElement { .into_any_element() } +enum ApiKeyDocs { + Link { dashboard_url: SharedString }, + Custom { message: SharedString }, +} + fn render_api_key_provider( icon: IconName, title: &'static str, - link: SharedString, + docs: ApiKeyDocs, api_key_state: Entity, current_url: fn(&mut App) -> SharedString, additional_fields: Option, @@ -209,25 +245,32 @@ fn render_api_key_provider( .icon(icon) .no_padding(true); let button_link_label = format!("{} dashboard", title); - let description = h_flex() - .min_w_0() - .gap_0p5() - .child( - Label::new("Visit the") + let description = match docs { + ApiKeyDocs::Custom { message } => h_flex().min_w_0().gap_0p5().child( + Label::new(message) .size(LabelSize::Small) .color(Color::Muted), - ) - .child( - ButtonLink::new(button_link_label, link) - .no_icon(true) - .label_size(LabelSize::Small) - .label_color(Color::Muted), - ) - .child( - Label::new("to generate an API key.") - .size(LabelSize::Small) - .color(Color::Muted), - ); + ), + ApiKeyDocs::Link { dashboard_url } => h_flex() + .min_w_0() + .gap_0p5() + .child( + Label::new("Visit the") + .size(LabelSize::Small) + .color(Color::Muted), + ) + .child( + ButtonLink::new(button_link_label, dashboard_url) + .no_icon(true) + .label_size(LabelSize::Small) + .label_color(Color::Muted), + ) + .child( + Label::new("to generate an API key.") + .size(LabelSize::Small) + .color(Color::Muted), + ), + }; let configured_card_label = if is_from_env_var { "API Key Set in Environment Variable" } else { @@ -484,34 +527,6 @@ fn ollama_settings() -> Box<[SettingsPageItem]> { ]) } -fn render_open_ai_compatible_provider( - settings_window: &SettingsWindow, - window: &mut Window, - cx: &mut Context, -) -> impl IntoElement { - let open_ai_compatible_settings = open_ai_compatible_settings(); - let additional_fields = settings_window - .render_sub_page_items_section( - open_ai_compatible_settings.iter().enumerate(), - true, - window, - cx, - ) - .into_any_element(); - - v_flex() - .id("open-ai-compatible") - .min_w_0() - .pt_8() - .gap_1p5() - .child( - SettingsSectionHeader::new("OpenAI Compatible API") - .icon(IconName::AiOpenAiCompat) - .no_padding(true), - ) - .child(div().px_neg_8().child(additional_fields)) -} - fn open_ai_compatible_settings() -> Box<[SettingsPageItem]> { Box::new([ SettingsPageItem::SettingItem(SettingItem { diff --git a/crates/theme/src/icon_theme.rs b/crates/theme/src/icon_theme.rs index 7c2d603281ec50c1daa6f21e1dc3487bfc394a67..121ff9d7d4fbd841315b89e631606c7e67bc5cde 100644 --- a/crates/theme/src/icon_theme.rs +++ b/crates/theme/src/icon_theme.rs @@ -89,7 +89,7 @@ const FILE_SUFFIXES_BY_ICON_KEY: &[(&str, &[&str])] = &[ ( "cpp", &[ - "c++", "h++", "cc", "cpp", "cxx", "hh", "hpp", "hxx", "inl", "ixx", + "c++", "h++", "cc", "cpp", "cppm", "cxx", "hh", "hpp", "hxx", "inl", "ixx", ], ), ("crystal", &["cr", "ecr"]), diff --git a/crates/web_search_providers/Cargo.toml b/crates/web_search_providers/Cargo.toml index ecdca5883ff541459e94170986df3b7f16036c5a..ff264edcb150063237c633de746b2f6b9f6f250c 100644 --- a/crates/web_search_providers/Cargo.toml +++ b/crates/web_search_providers/Cargo.toml @@ -14,6 +14,7 @@ path = "src/web_search_providers.rs" [dependencies] anyhow.workspace = true client.workspace = true +cloud_api_types.workspace = true cloud_llm_client.workspace = true futures.workspace = true gpui.workspace = true diff --git a/crates/web_search_providers/src/cloud.rs b/crates/web_search_providers/src/cloud.rs index 2f3ccdbb52a884471250ad458e8b7922437cb9ae..c8bc89953f2b2d3ec62bac07e80f2737522824f7 100644 --- a/crates/web_search_providers/src/cloud.rs +++ b/crates/web_search_providers/src/cloud.rs @@ -1,7 +1,8 @@ use std::sync::Arc; use anyhow::{Context as _, Result}; -use client::Client; +use client::{Client, UserStore}; +use cloud_api_types::OrganizationId; use cloud_llm_client::{WebSearchBody, WebSearchResponse}; use futures::AsyncReadExt as _; use gpui::{App, AppContext, Context, Entity, Subscription, Task}; @@ -14,8 +15,8 @@ pub struct CloudWebSearchProvider { } impl CloudWebSearchProvider { - pub fn new(client: Arc, cx: &mut App) -> Self { - let state = cx.new(|cx| State::new(client, cx)); + pub fn new(client: Arc, user_store: Entity, cx: &mut App) -> Self { + let state = cx.new(|cx| State::new(client, user_store, cx)); Self { state } } @@ -23,24 +24,31 @@ impl CloudWebSearchProvider { pub struct State { client: Arc, + user_store: Entity, llm_api_token: LlmApiToken, _llm_token_subscription: Subscription, } impl State { - pub fn new(client: Arc, cx: &mut Context) -> Self { + pub fn new(client: Arc, user_store: Entity, cx: &mut Context) -> Self { let refresh_llm_token_listener = RefreshLlmTokenListener::global(cx); Self { client, + user_store, llm_api_token: LlmApiToken::default(), _llm_token_subscription: cx.subscribe( &refresh_llm_token_listener, |this, _, _event, cx| { let client = this.client.clone(); let llm_api_token = this.llm_api_token.clone(); + let organization_id = this + .user_store + .read(cx) + .current_organization() + .map(|o| o.id.clone()); cx.spawn(async move |_this, _cx| { - llm_api_token.refresh(&client).await?; + llm_api_token.refresh(&client, organization_id).await?; anyhow::Ok(()) }) .detach_and_log_err(cx); @@ -61,21 +69,31 @@ impl WebSearchProvider for CloudWebSearchProvider { let state = self.state.read(cx); let client = state.client.clone(); let llm_api_token = state.llm_api_token.clone(); + let organization_id = state + .user_store + .read(cx) + .current_organization() + .map(|o| o.id.clone()); let body = WebSearchBody { query }; - cx.background_spawn(async move { perform_web_search(client, llm_api_token, body).await }) + cx.background_spawn(async move { + perform_web_search(client, llm_api_token, organization_id, body).await + }) } } async fn perform_web_search( client: Arc, llm_api_token: LlmApiToken, + organization_id: Option, body: WebSearchBody, ) -> Result { const MAX_RETRIES: usize = 3; let http_client = &client.http_client(); let mut retries_remaining = MAX_RETRIES; - let mut token = llm_api_token.acquire(&client).await?; + let mut token = llm_api_token + .acquire(&client, organization_id.clone()) + .await?; loop { if retries_remaining == 0 { @@ -100,7 +118,9 @@ async fn perform_web_search( response.body_mut().read_to_string(&mut body).await?; return Ok(serde_json::from_str(&body)?); } else if response.needs_llm_token_refresh() { - token = llm_api_token.refresh(&client).await?; + token = llm_api_token + .refresh(&client, organization_id.clone()) + .await?; retries_remaining -= 1; } else { // For now we will only retry if the LLM token is expired, diff --git a/crates/web_search_providers/src/web_search_providers.rs b/crates/web_search_providers/src/web_search_providers.rs index 8ab0aee47a414c4cc669ab05e727a827d17c2844..509632429fb167cd489cd4253ceae0ce479b10a8 100644 --- a/crates/web_search_providers/src/web_search_providers.rs +++ b/crates/web_search_providers/src/web_search_providers.rs @@ -1,26 +1,28 @@ mod cloud; -use client::Client; +use client::{Client, UserStore}; use gpui::{App, Context, Entity}; use language_model::LanguageModelRegistry; use std::sync::Arc; use web_search::{WebSearchProviderId, WebSearchRegistry}; -pub fn init(client: Arc, cx: &mut App) { +pub fn init(client: Arc, user_store: Entity, cx: &mut App) { let registry = WebSearchRegistry::global(cx); registry.update(cx, |registry, cx| { - register_web_search_providers(registry, client, cx); + register_web_search_providers(registry, client, user_store, cx); }); } fn register_web_search_providers( registry: &mut WebSearchRegistry, client: Arc, + user_store: Entity, cx: &mut Context, ) { register_zed_web_search_provider( registry, client.clone(), + user_store.clone(), &LanguageModelRegistry::global(cx), cx, ); @@ -29,7 +31,13 @@ fn register_web_search_providers( &LanguageModelRegistry::global(cx), move |this, registry, event, cx| { if let language_model::Event::DefaultModelChanged = event { - register_zed_web_search_provider(this, client.clone(), ®istry, cx) + register_zed_web_search_provider( + this, + client.clone(), + user_store.clone(), + ®istry, + cx, + ) } }, ) @@ -39,6 +47,7 @@ fn register_web_search_providers( fn register_zed_web_search_provider( registry: &mut WebSearchRegistry, client: Arc, + user_store: Entity, language_model_registry: &Entity, cx: &mut Context, ) { @@ -47,7 +56,10 @@ fn register_zed_web_search_provider( .default_model() .is_some_and(|default| default.is_provided_by_zed()); if using_zed_provider { - registry.register_provider(cloud::CloudWebSearchProvider::new(client, cx), cx) + registry.register_provider( + cloud::CloudWebSearchProvider::new(client, user_store, cx), + cx, + ) } else { registry.unregister_provider(WebSearchProviderId( cloud::ZED_WEB_SEARCH_PROVIDER_ID.into(), diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 69b0be24e7ffb09d3fe759ec0bd3d54b54db21d3..9e62beb3c375fb8d580be02382091cafe04d31e2 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -2945,7 +2945,7 @@ impl BackgroundScannerState { self.snapshot.check_invariants(false); } - fn remove_path(&mut self, path: &RelPath) { + fn remove_path(&mut self, path: &RelPath, watcher: &dyn Watcher) { log::trace!("background scanner removing path {path:?}"); let mut new_entries; let removed_entries; @@ -2961,7 +2961,12 @@ impl BackgroundScannerState { self.snapshot.entries_by_path = new_entries; let mut removed_ids = Vec::with_capacity(removed_entries.summary().count); + let mut removed_dir_abs_paths = Vec::new(); for entry in removed_entries.cursor::<()>(()) { + if entry.is_dir() { + removed_dir_abs_paths.push(self.snapshot.absolutize(&entry.path)); + } + match self.removed_entries.entry(entry.inode) { hash_map::Entry::Occupied(mut e) => { let prev_removed_entry = e.get_mut(); @@ -2997,6 +3002,10 @@ impl BackgroundScannerState { .git_repositories .retain(|id, _| removed_ids.binary_search(id).is_err()); + for removed_dir_abs_path in removed_dir_abs_paths { + watcher.remove(&removed_dir_abs_path).log_err(); + } + #[cfg(feature = "test-support")] self.snapshot.check_invariants(false); } @@ -4461,7 +4470,10 @@ impl BackgroundScanner { if self.settings.is_path_excluded(&child_path) { log::debug!("skipping excluded child entry {child_path:?}"); - self.state.lock().await.remove_path(&child_path); + self.state + .lock() + .await + .remove_path(&child_path, self.watcher.as_ref()); continue; } @@ -4651,7 +4663,7 @@ impl BackgroundScanner { // detected regardless of the order of the paths. for (path, metadata) in relative_paths.iter().zip(metadata.iter()) { if matches!(metadata, Ok(None)) || doing_recursive_update { - state.remove_path(path); + state.remove_path(path, self.watcher.as_ref()); } } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 38238d8af519c0506ab451bccaa1abe3a893e4c9..a3379a6017b7e3b7c26e2a98346e4926e90e0999 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -645,7 +645,7 @@ fn main() { zed::remote_debug::init(cx); edit_prediction_ui::init(cx); web_search::init(cx); - web_search_providers::init(app_state.client.clone(), cx); + web_search_providers::init(app_state.client.clone(), app_state.user_store.clone(), cx); snippet_provider::init(cx); edit_prediction_registry::init(app_state.client.clone(), app_state.user_store.clone(), cx); let prompt_builder = PromptBuilder::load(app_state.fs.clone(), stdout_is_a_pty(), cx); diff --git a/crates/zed/src/reliability.rs b/crates/zed/src/reliability.rs index b291b9c8493db75e20282c8c9bc5a3750fb5e705..d8dc1c4f8fe412b5e8eeb6b09e482a9ed243aaa3 100644 --- a/crates/zed/src/reliability.rs +++ b/crates/zed/src/reliability.rs @@ -144,7 +144,7 @@ fn cleanup_old_hang_traces() { entry .path() .extension() - .is_some_and(|ext| ext == "miniprof") + .is_some_and(|ext| ext == "json" || ext == "miniprof") }) .collect(); @@ -175,7 +175,7 @@ fn save_hang_trace( .collect::>(); let trace_path = paths::hang_traces_dir().join(&format!( - "hang-{}.miniprof", + "hang-{}.miniprof.json", hang_time.format("%Y-%m-%d_%H-%M-%S") )); @@ -193,7 +193,7 @@ fn save_hang_trace( entry .path() .extension() - .is_some_and(|ext| ext == "miniprof") + .is_some_and(|ext| ext == "json" || ext == "miniprof") }) .collect(); diff --git a/crates/zed/src/visual_test_runner.rs b/crates/zed/src/visual_test_runner.rs index df673f0b4869af8fa55b0e83af10553df8afb4d8..8f005fa68b6accb5cf5686157bbb065e33bb1b0c 100644 --- a/crates/zed/src/visual_test_runner.rs +++ b/crates/zed/src/visual_test_runner.rs @@ -2032,32 +2032,9 @@ fn run_agent_thread_view_test( // Create the necessary entities for the ReadFileTool let action_log = cx.update(|cx| cx.new(|_| action_log::ActionLog::new(project.clone()))); - let context_server_registry = cx.update(|cx| { - cx.new(|cx| agent::ContextServerRegistry::new(project.read(cx).context_server_store(), cx)) - }); - let fake_model = Arc::new(language_model::fake_provider::FakeLanguageModel::default()); - let project_context = cx.update(|cx| cx.new(|_| prompt_store::ProjectContext::default())); - - // Create the agent Thread - let thread = cx.update(|cx| { - cx.new(|cx| { - agent::Thread::new( - project.clone(), - project_context, - context_server_registry, - agent::Templates::new(), - Some(fake_model), - cx, - ) - }) - }); // Create the ReadFileTool - let tool = Arc::new(agent::ReadFileTool::new( - thread.downgrade(), - project.clone(), - action_log, - )); + let tool = Arc::new(agent::ReadFileTool::new(project.clone(), action_log, true)); // Create a test event stream to capture tool output let (event_stream, mut event_receiver) = agent::ToolCallEventStream::test(); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 17832bdd1833cabb42af2195f9d9aab1a6bf3fab..20629785c7172241f49a0e7a69f9dcc1953f6a95 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -5021,7 +5021,7 @@ mod tests { language_models::init(app_state.user_store.clone(), app_state.client.clone(), cx); web_search::init(cx); git_graph::init(cx); - web_search_providers::init(app_state.client.clone(), cx); + web_search_providers::init(app_state.client.clone(), app_state.user_store.clone(), cx); let prompt_builder = PromptBuilder::load(app_state.fs.clone(), false, cx); project::AgentRegistryStore::init_global( cx, diff --git a/crates/zed/src/zed/edit_prediction_registry.rs b/crates/zed/src/zed/edit_prediction_registry.rs index 67b0d26c88cf0bd254a776834de09fb89d6ea195..9f05c5795e6f16cab231df8a5586106ed25b03ee 100644 --- a/crates/zed/src/zed/edit_prediction_registry.rs +++ b/crates/zed/src/zed/edit_prediction_registry.rs @@ -2,15 +2,12 @@ use client::{Client, UserStore}; use codestral::{CodestralEditPredictionDelegate, load_codestral_api_key}; use collections::HashMap; use copilot::CopilotEditPredictionDelegate; -use edit_prediction::{EditPredictionModel, ZedEditPredictionDelegate, Zeta2FeatureFlag}; +use edit_prediction::{EditPredictionModel, ZedEditPredictionDelegate}; use editor::Editor; -use feature_flags::FeatureFlagAppExt; use gpui::{AnyWindowHandle, App, AppContext as _, Context, Entity, WeakEntity}; use language::language_settings::{EditPredictionProvider, all_language_settings}; -use settings::{ - EXPERIMENTAL_ZETA2_EDIT_PREDICTION_PROVIDER_NAME, EditPredictionPromptFormat, SettingsStore, -}; +use settings::{EditPredictionPromptFormat, SettingsStore}; use std::{cell::RefCell, rc::Rc, sync::Arc}; use ui::Window; @@ -81,9 +78,6 @@ pub fn init(client: Arc, user_store: Entity, cx: &mut App) { .detach(); cx.observe_global::({ - let editors = editors.clone(); - let client = client.clone(); - let user_store = user_store.clone(); let mut previous_config = edit_prediction_provider_config_for_settings(cx); move |cx| { let new_provider_config = edit_prediction_provider_config_for_settings(cx); @@ -107,24 +101,6 @@ pub fn init(client: Arc, user_store: Entity, cx: &mut App) { } }) .detach(); - - cx.observe_flag::({ - let mut previous_config = edit_prediction_provider_config_for_settings(cx); - move |_is_enabled, cx| { - let new_provider_config = edit_prediction_provider_config_for_settings(cx); - if new_provider_config != previous_config { - previous_config = new_provider_config; - assign_edit_prediction_providers( - &editors, - new_provider_config, - &client, - user_store.clone(), - cx, - ); - } - } - }) - .detach(); } fn edit_prediction_provider_config_for_settings(cx: &App) -> Option { @@ -154,7 +130,10 @@ fn edit_prediction_provider_config_for_settings(cx: &App) -> Option Option Some(EditPredictionProviderConfig::Zed( EditPredictionModel::Mercury, )), - EditPredictionProvider::Experimental(name) => { - if name == EXPERIMENTAL_ZETA2_EDIT_PREDICTION_PROVIDER_NAME - && cx.has_flag::() - { - Some(EditPredictionProviderConfig::Zed(EditPredictionModel::Zeta)) - } else { - None - } - } + EditPredictionProvider::Experimental(_) => None, } } diff --git a/crates/zeta_prompt/src/zeta_prompt.rs b/crates/zeta_prompt/src/zeta_prompt.rs index 0cd37a455397334933dbfa2464c2dbcb72bba456..2ec12e8bebb4a868c0784e2fe52541a1de580555 100644 --- a/crates/zeta_prompt/src/zeta_prompt.rs +++ b/crates/zeta_prompt/src/zeta_prompt.rs @@ -86,6 +86,7 @@ pub enum ZetaFormat { V0131GitMergeMarkersPrefix, V0211Prefill, V0211SeedCoder, + v0226Hashline, } impl std::fmt::Display for ZetaFormat { @@ -122,25 +123,6 @@ impl ZetaFormat { .collect::>() .concat() } - - pub fn special_tokens(&self) -> &'static [&'static str] { - match self { - ZetaFormat::V0112MiddleAtEnd - | ZetaFormat::V0113Ordered - | ZetaFormat::V0114180EditableRegion => &[ - "<|fim_prefix|>", - "<|fim_suffix|>", - "<|fim_middle|>", - "<|file_sep|>", - CURSOR_MARKER, - ], - ZetaFormat::V0120GitMergeMarkers => v0120_git_merge_markers::special_tokens(), - ZetaFormat::V0131GitMergeMarkersPrefix | ZetaFormat::V0211Prefill => { - v0131_git_merge_markers_prefix::special_tokens() - } - ZetaFormat::V0211SeedCoder => seed_coder::special_tokens(), - } - } } #[derive(Clone, Debug, PartialEq, Hash, Serialize, Deserialize)] @@ -212,33 +194,29 @@ pub struct RelatedExcerpt { } pub fn prompt_input_contains_special_tokens(input: &ZetaPromptInput, format: ZetaFormat) -> bool { - format - .special_tokens() + special_tokens_for_format(format) .iter() .any(|token| input.cursor_excerpt.contains(token)) } pub fn format_zeta_prompt(input: &ZetaPromptInput, format: ZetaFormat) -> String { - format_zeta_prompt_with_budget(input, format, MAX_PROMPT_TOKENS) + format_prompt_with_budget_for_format(input, format, MAX_PROMPT_TOKENS) } -/// Post-processes model output for the given zeta format by stripping format-specific suffixes. -pub fn clean_zeta2_model_output(output: &str, format: ZetaFormat) -> &str { +pub fn special_tokens_for_format(format: ZetaFormat) -> &'static [&'static str] { match format { - ZetaFormat::V0120GitMergeMarkers => output - .strip_suffix(v0120_git_merge_markers::END_MARKER) - .unwrap_or(output), - ZetaFormat::V0131GitMergeMarkersPrefix => output - .strip_suffix(v0131_git_merge_markers_prefix::END_MARKER) - .unwrap_or(output), - ZetaFormat::V0211SeedCoder => output - .strip_suffix(seed_coder::END_MARKER) - .unwrap_or(output), - _ => output, + ZetaFormat::V0112MiddleAtEnd => v0112_middle_at_end::special_tokens(), + ZetaFormat::V0113Ordered => v0113_ordered::special_tokens(), + ZetaFormat::V0114180EditableRegion => v0114180_editable_region::special_tokens(), + ZetaFormat::V0120GitMergeMarkers => v0120_git_merge_markers::special_tokens(), + ZetaFormat::V0131GitMergeMarkersPrefix => v0131_git_merge_markers_prefix::special_tokens(), + ZetaFormat::V0211Prefill => v0211_prefill::special_tokens(), + ZetaFormat::V0211SeedCoder => seed_coder::special_tokens(), + ZetaFormat::v0226Hashline => hashline::special_tokens(), } } -pub fn excerpt_range_for_format( +pub fn excerpt_ranges_for_format( format: ZetaFormat, ranges: &ExcerptRanges, ) -> (Range, Range) { @@ -247,129 +225,257 @@ pub fn excerpt_range_for_format( ranges.editable_150.clone(), ranges.editable_150_context_350.clone(), ), - ZetaFormat::V0114180EditableRegion - | ZetaFormat::V0120GitMergeMarkers + ZetaFormat::V0114180EditableRegion => ( + ranges.editable_180.clone(), + ranges.editable_180_context_350.clone(), + ), + ZetaFormat::V0120GitMergeMarkers | ZetaFormat::V0131GitMergeMarkersPrefix | ZetaFormat::V0211Prefill - | ZetaFormat::V0211SeedCoder => ( + | ZetaFormat::V0211SeedCoder + | ZetaFormat::v0226Hashline => ( ranges.editable_350.clone(), ranges.editable_350_context_150.clone(), ), } } -pub fn resolve_cursor_region( - input: &ZetaPromptInput, - format: ZetaFormat, -) -> (&str, Range, usize) { - let (editable_range, context_range) = excerpt_range_for_format(format, &input.excerpt_ranges); - let context_start = context_range.start; - let context_text = &input.cursor_excerpt[context_range]; - let adjusted_editable = - (editable_range.start - context_start)..(editable_range.end - context_start); - let adjusted_cursor = input.cursor_offset_in_excerpt - context_start; - - (context_text, adjusted_editable, adjusted_cursor) -} - -fn format_zeta_prompt_with_budget( - input: &ZetaPromptInput, +pub fn write_cursor_excerpt_section_for_format( format: ZetaFormat, - max_tokens: usize, -) -> String { - let (context, editable_range, cursor_offset) = resolve_cursor_region(input, format); - let path = &*input.cursor_path; - - let mut cursor_section = String::new(); + prompt: &mut String, + path: &Path, + context: &str, + editable_range: &Range, + cursor_offset: usize, +) { match format { - ZetaFormat::V0112MiddleAtEnd => { - v0112_middle_at_end::write_cursor_excerpt_section( - &mut cursor_section, - path, - context, - &editable_range, - cursor_offset, - ); - } + ZetaFormat::V0112MiddleAtEnd => v0112_middle_at_end::write_cursor_excerpt_section( + prompt, + path, + context, + editable_range, + cursor_offset, + ), ZetaFormat::V0113Ordered | ZetaFormat::V0114180EditableRegion => { v0113_ordered::write_cursor_excerpt_section( - &mut cursor_section, + prompt, path, context, - &editable_range, + editable_range, cursor_offset, ) } ZetaFormat::V0120GitMergeMarkers => v0120_git_merge_markers::write_cursor_excerpt_section( - &mut cursor_section, + prompt, path, context, - &editable_range, + editable_range, cursor_offset, ), ZetaFormat::V0131GitMergeMarkersPrefix | ZetaFormat::V0211Prefill => { v0131_git_merge_markers_prefix::write_cursor_excerpt_section( - &mut cursor_section, + prompt, path, context, - &editable_range, + editable_range, cursor_offset, ) } - ZetaFormat::V0211SeedCoder => { - return seed_coder::format_prompt_with_budget( + ZetaFormat::V0211SeedCoder => seed_coder::write_cursor_excerpt_section( + prompt, + path, + context, + editable_range, + cursor_offset, + ), + ZetaFormat::v0226Hashline => hashline::write_cursor_excerpt_section( + prompt, + path, + context, + editable_range, + cursor_offset, + ), + } +} + +pub fn format_prompt_with_budget_for_format( + input: &ZetaPromptInput, + format: ZetaFormat, + max_tokens: usize, +) -> String { + let (context, editable_range, cursor_offset) = resolve_cursor_region(input, format); + let path = &*input.cursor_path; + + match format { + ZetaFormat::V0211SeedCoder => seed_coder::format_prompt_with_budget( + path, + context, + &editable_range, + cursor_offset, + &input.events, + &input.related_files, + max_tokens, + ), + _ => { + let mut cursor_section = String::new(); + write_cursor_excerpt_section_for_format( + format, + &mut cursor_section, path, context, &editable_range, cursor_offset, + ); + + let cursor_tokens = estimate_tokens(cursor_section.len()); + let budget_after_cursor = max_tokens.saturating_sub(cursor_tokens); + + let edit_history_section = format_edit_history_within_budget( &input.events, + "<|file_sep|>", + "edit history", + budget_after_cursor, + ); + let edit_history_tokens = estimate_tokens(edit_history_section.len()); + let budget_after_edit_history = budget_after_cursor.saturating_sub(edit_history_tokens); + + let related_files_section = format_related_files_within_budget( &input.related_files, - max_tokens, + "<|file_sep|>", + "", + budget_after_edit_history, ); + + let mut prompt = String::new(); + prompt.push_str(&related_files_section); + prompt.push_str(&edit_history_section); + prompt.push_str(&cursor_section); + prompt } } - - let cursor_tokens = estimate_tokens(cursor_section.len()); - let budget_after_cursor = max_tokens.saturating_sub(cursor_tokens); - - let edit_history_section = format_edit_history_within_budget( - &input.events, - "<|file_sep|>", - "edit history", - budget_after_cursor, - ); - let edit_history_tokens = estimate_tokens(edit_history_section.len()); - let budget_after_edit_history = budget_after_cursor.saturating_sub(edit_history_tokens); - - let related_files_section = format_related_files_within_budget( - &input.related_files, - "<|file_sep|>", - "", - budget_after_edit_history, - ); - - let mut prompt = String::new(); - prompt.push_str(&related_files_section); - prompt.push_str(&edit_history_section); - prompt.push_str(&cursor_section); - prompt } -pub fn get_prefill(input: &ZetaPromptInput, format: ZetaFormat) -> String { +pub fn get_prefill_for_format( + format: ZetaFormat, + context: &str, + editable_range: &Range, +) -> String { match format { + ZetaFormat::V0211Prefill => v0211_prefill::get_prefill(context, editable_range), ZetaFormat::V0112MiddleAtEnd | ZetaFormat::V0113Ordered | ZetaFormat::V0114180EditableRegion | ZetaFormat::V0120GitMergeMarkers | ZetaFormat::V0131GitMergeMarkersPrefix - | ZetaFormat::V0211SeedCoder => String::new(), - ZetaFormat::V0211Prefill => { - let (context, editable_range, _) = resolve_cursor_region(input, format); - v0211_prefill::get_prefill(context, &editable_range) + | ZetaFormat::V0211SeedCoder + | ZetaFormat::v0226Hashline => String::new(), + } +} + +pub fn output_end_marker_for_format(format: ZetaFormat) -> Option<&'static str> { + match format { + ZetaFormat::V0120GitMergeMarkers => Some(v0120_git_merge_markers::END_MARKER), + ZetaFormat::V0131GitMergeMarkersPrefix => Some(v0131_git_merge_markers_prefix::END_MARKER), + ZetaFormat::V0211Prefill => Some(v0131_git_merge_markers_prefix::END_MARKER), + ZetaFormat::V0211SeedCoder => Some(seed_coder::END_MARKER), + ZetaFormat::V0112MiddleAtEnd + | ZetaFormat::V0113Ordered + | ZetaFormat::V0114180EditableRegion + | ZetaFormat::v0226Hashline => None, + } +} + +pub fn current_region_markers_for_format(format: ZetaFormat) -> (&'static str, &'static str) { + match format { + ZetaFormat::V0112MiddleAtEnd => ("<|fim_middle|>current\n", "<|fim_middle|>updated"), + ZetaFormat::V0113Ordered + | ZetaFormat::V0114180EditableRegion + | ZetaFormat::v0226Hashline => ("<|fim_middle|>current\n", "<|fim_suffix|>"), + ZetaFormat::V0120GitMergeMarkers + | ZetaFormat::V0131GitMergeMarkersPrefix + | ZetaFormat::V0211Prefill => ( + v0120_git_merge_markers::START_MARKER, + v0120_git_merge_markers::SEPARATOR, + ), + ZetaFormat::V0211SeedCoder => (seed_coder::START_MARKER, seed_coder::SEPARATOR), + } +} + +pub fn clean_extracted_region_for_format(format: ZetaFormat, region: &str) -> String { + match format { + ZetaFormat::v0226Hashline => hashline::strip_hashline_prefixes(region), + _ => region.to_string(), + } +} + +pub fn encode_patch_as_output_for_format( + format: ZetaFormat, + old_editable_region: &str, + patch: &str, + cursor_offset: Option, +) -> Result> { + match format { + ZetaFormat::v0226Hashline => { + hashline::patch_to_edit_commands(old_editable_region, patch, cursor_offset).map(Some) + } + _ => Ok(None), + } +} + +pub fn output_with_context_for_format( + format: ZetaFormat, + old_editable_region: &str, + output: &str, +) -> Result> { + match format { + ZetaFormat::v0226Hashline => { + if hashline::output_has_edit_commands(output) { + Ok(Some(hashline::apply_edit_commands( + old_editable_region, + output, + ))) + } else { + Ok(None) + } } + _ => Ok(None), } } +/// Post-processes model output for the given zeta format by stripping format-specific suffixes. +pub fn clean_zeta2_model_output(output: &str, format: ZetaFormat) -> &str { + match output_end_marker_for_format(format) { + Some(marker) => output.strip_suffix(marker).unwrap_or(output), + None => output, + } +} + +pub fn excerpt_range_for_format( + format: ZetaFormat, + ranges: &ExcerptRanges, +) -> (Range, Range) { + excerpt_ranges_for_format(format, ranges) +} + +pub fn resolve_cursor_region( + input: &ZetaPromptInput, + format: ZetaFormat, +) -> (&str, Range, usize) { + let (editable_range, context_range) = excerpt_range_for_format(format, &input.excerpt_ranges); + let context_start = context_range.start; + let context_text = &input.cursor_excerpt[context_range]; + let adjusted_editable = + (editable_range.start - context_start)..(editable_range.end - context_start); + let adjusted_cursor = input.cursor_offset_in_excerpt - context_start; + + (context_text, adjusted_editable, adjusted_cursor) +} + +pub fn get_prefill(input: &ZetaPromptInput, format: ZetaFormat) -> String { + let (context, editable_range, _) = resolve_cursor_region(input, format); + get_prefill_for_format(format, context, &editable_range) +} + fn format_edit_history_within_budget( events: &[Arc], file_marker: &str, @@ -533,6 +639,16 @@ pub fn write_related_files( mod v0112_middle_at_end { use super::*; + pub fn special_tokens() -> &'static [&'static str] { + &[ + "<|fim_prefix|>", + "<|fim_suffix|>", + "<|fim_middle|>", + "<|file_sep|>", + CURSOR_MARKER, + ] + } + pub fn write_cursor_excerpt_section( prompt: &mut String, path: &Path, @@ -567,6 +683,16 @@ mod v0112_middle_at_end { mod v0113_ordered { use super::*; + pub fn special_tokens() -> &'static [&'static str] { + &[ + "<|fim_prefix|>", + "<|fim_suffix|>", + "<|fim_middle|>", + "<|file_sep|>", + CURSOR_MARKER, + ] + } + pub fn write_cursor_excerpt_section( prompt: &mut String, path: &Path, @@ -601,6 +727,14 @@ mod v0113_ordered { } } +mod v0114180_editable_region { + use super::*; + + pub fn special_tokens() -> &'static [&'static str] { + v0113_ordered::special_tokens() + } +} + pub mod v0120_git_merge_markers { //! A prompt that uses git-style merge conflict markers to represent the editable region. //! @@ -752,6 +886,10 @@ pub mod v0131_git_merge_markers_prefix { pub mod v0211_prefill { use super::*; + pub fn special_tokens() -> &'static [&'static str] { + v0131_git_merge_markers_prefix::special_tokens() + } + pub fn get_prefill(context: &str, editable_range: &Range) -> String { let editable_region = &context[editable_range.start..editable_range.end]; @@ -783,6 +921,1413 @@ pub mod v0211_prefill { } } +pub mod hashline { + + use std::fmt::Display; + + pub const END_MARKER: &str = "<|fim_middle|>updated"; + pub const START_MARKER: &str = "<|fim_middle|>current"; + + use super::*; + + const SET_COMMAND_MARKER: &str = "<|set|>"; + const INSERT_COMMAND_MARKER: &str = "<|insert|>"; + + pub fn special_tokens() -> &'static [&'static str] { + return &[ + SET_COMMAND_MARKER, + "<|set_range|>", + INSERT_COMMAND_MARKER, + CURSOR_MARKER, + "<|file_sep|>", + "<|fim_prefix|>", + "<|fim_suffix|>", + "<|fim_middle|>", + ]; + } + + /// A parsed line reference like `3:c3` (line index 3 with hash 0xc3). + #[derive(Debug, Clone, PartialEq, Eq)] + struct LineRef { + index: usize, + hash: u8, + } + + impl Display for LineRef { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}:{:02x}", self.index, self.hash) + } + } + + pub fn hash_line(line: &[u8]) -> u8 { + let mut h: u8 = 0; + for &byte in line { + h = h.wrapping_add(byte); + } + return h; + } + + /// Write the hashline-encoded editable region into `out`. Each line of + /// `editable_text` is prefixed with `{line_index}:{hash}|` and the cursor + /// marker is inserted at `cursor_offset_in_editable` (byte offset relative + /// to the start of `editable_text`). + pub fn write_hashline_editable_region( + out: &mut String, + editable_text: &str, + cursor_offset_in_editable: usize, + ) { + let mut offset = 0; + for (i, line) in editable_text.lines().enumerate() { + let (head, cursor, tail) = if cursor_offset_in_editable > offset + && cursor_offset_in_editable < offset + line.len() + { + ( + &line[..cursor_offset_in_editable - offset], + CURSOR_MARKER, + &line[cursor_offset_in_editable - offset..], + ) + } else { + (line, "", "") + }; + write!( + out, + "\n{}|{head}{cursor}{tail}", + LineRef { + index: i, + hash: hash_line(line.as_bytes()) + } + ) + .unwrap(); + offset += line.len() + 1; + } + } + + pub fn write_cursor_excerpt_section( + prompt: &mut String, + path: &Path, + context: &str, + editable_range: &Range, + cursor_offset: usize, + ) { + let path_str = path.to_string_lossy(); + write!(prompt, "<|file_sep|>{}\n", path_str).ok(); + + prompt.push_str("<|fim_prefix|>\n"); + prompt.push_str(&context[..editable_range.start]); + prompt.push_str(START_MARKER); + + let cursor_offset_in_editable = cursor_offset.saturating_sub(editable_range.start); + let editable_region = &context[editable_range.clone()]; + write_hashline_editable_region(prompt, editable_region, cursor_offset_in_editable); + + if !prompt.ends_with('\n') { + prompt.push('\n'); + } + + prompt.push_str("<|fim_suffix|>\n"); + prompt.push_str(&context[editable_range.end..]); + if !prompt.ends_with('\n') { + prompt.push('\n'); + } + + prompt.push_str(END_MARKER); + } + + /// A single edit command parsed from the model output. + #[derive(Debug)] + enum EditCommand<'a> { + /// Replace a range of lines (inclusive on both ends). Single-line set is + /// represented by `start == end`. + Set { + start: LineRef, + end: LineRef, + content: &'a str, + }, + /// Insert new lines after the given line, or before the first line if + /// `after` is `None`. + Insert { + after: Option, + content: &'a str, + }, + } + + /// Parse a line reference like `3:c3` into a `LineRef`. + fn parse_line_ref(s: &str) -> Option { + let (idx_str, hash_str) = s.split_once(':')?; + let index = idx_str.parse::().ok()?; + let hash = u8::from_str_radix(hash_str, 16).ok()?; + Some(LineRef { index, hash }) + } + + /// Parse the model output into a list of `EditCommand`s. + fn parse_edit_commands(model_output: &str) -> Vec> { + let mut commands = Vec::new(); + let mut offset = 0usize; + + while offset < model_output.len() { + let next_nl = model_output[offset..] + .find('\n') + .map(|i| offset + i) + .unwrap_or(model_output.len()); + let line = &model_output[offset..next_nl]; + let line_end = if next_nl < model_output.len() { + next_nl + 1 + } else { + next_nl + }; + + let trimmed = line.trim(); + let (is_set, specifier) = if let Some(spec) = trimmed.strip_prefix(SET_COMMAND_MARKER) { + (true, spec) + } else if let Some(spec) = trimmed.strip_prefix(INSERT_COMMAND_MARKER) { + (false, spec) + } else { + offset = line_end; + continue; + }; + + let mut content_end = line_end; + let mut scan = line_end; + + while scan < model_output.len() { + let body_nl = model_output[scan..] + .find('\n') + .map(|i| scan + i) + .unwrap_or(model_output.len()); + let body_line = &model_output[scan..body_nl]; + if body_line.trim().starts_with(SET_COMMAND_MARKER) + || body_line.trim().starts_with(INSERT_COMMAND_MARKER) + { + break; + } + scan = if body_nl < model_output.len() { + body_nl + 1 + } else { + body_nl + }; + content_end = scan; + } + + let content = &model_output[line_end..content_end]; + + if is_set { + if let Some((start_str, end_str)) = specifier.split_once('-') { + if let (Some(start), Some(end)) = + (parse_line_ref(start_str), parse_line_ref(end_str)) + { + commands.push(EditCommand::Set { + start, + end, + content, + }); + } + } else if let Some(target) = parse_line_ref(specifier) { + commands.push(EditCommand::Set { + start: target.clone(), + end: target, + content, + }); + } + } else { + let after = parse_line_ref(specifier); + commands.push(EditCommand::Insert { after, content }); + } + + offset = scan; + } + + commands + } + + /// Returns `true` if the model output contains `<|set|>` or `<|insert|>` commands + /// (as opposed to being a plain full-replacement output). + /// Strip the `{line_num}:{hash}|` prefixes from each line of a hashline-encoded + /// editable region, returning the plain text content. + pub fn strip_hashline_prefixes(region: &str) -> String { + let mut decoded: String = region + .lines() + .map(|line| line.find('|').map_or(line, |pos| &line[pos + 1..])) + .collect::>() + .join("\n"); + if region.ends_with('\n') { + decoded.push('\n'); + } + decoded + } + + pub fn output_has_edit_commands(model_output: &str) -> bool { + model_output.contains(SET_COMMAND_MARKER) || model_output.contains(INSERT_COMMAND_MARKER) + } + + /// Apply `<|set|>` and `<|insert|>` edit commands from the model output to the + /// original editable region text. + /// + /// `editable_region` is the original text of the editable region (without hash + /// prefixes). `model_output` is the raw model response containing edit commands. + /// + /// Returns the full replacement text for the editable region. + pub fn apply_edit_commands(editable_region: &str, model_output: &str) -> String { + let original_lines: Vec<&str> = editable_region.lines().collect(); + let old_hashes: Vec = original_lines + .iter() + .map(|line| hash_line(line.as_bytes())) + .collect(); + + let commands = parse_edit_commands(model_output); + + // For set operations: indexed by start line → Some((end line index, content)) + // For insert operations: indexed by line index → vec of content to insert after + // Insert-before-first is tracked separately. + let mut set_ops: Vec> = vec![None; original_lines.len()]; + let mut insert_before_first: Vec<&str> = Vec::new(); + let mut insert_after: Vec> = vec![Vec::new(); original_lines.len()]; + + for command in &commands { + match command { + EditCommand::Set { + start, + end, + content, + } => { + if start.index < old_hashes.len() + && end.index < old_hashes.len() + && start.index <= end.index + && old_hashes[start.index] == start.hash + && old_hashes[end.index] == end.hash + { + set_ops[start.index] = Some((end.index, *content)); + } + } + EditCommand::Insert { after, content } => match after { + None => insert_before_first.push(*content), + Some(line_ref) => { + if line_ref.index < old_hashes.len() + && old_hashes[line_ref.index] == line_ref.hash + { + insert_after[line_ref.index].push(*content); + } + } + }, + } + } + + let mut result = String::new(); + + // Emit any insertions before the first line + for content in &insert_before_first { + result.push_str(content); + if !content.ends_with('\n') { + result.push('\n'); + } + } + + let mut i = 0; + while i < original_lines.len() { + if let Some((end_index, replacement)) = set_ops[i].as_ref() { + // Replace lines i..=end_index with the replacement content + result.push_str(replacement); + if !replacement.is_empty() && !replacement.ends_with('\n') { + result.push('\n'); + } + // Emit any insertions after the end of this set range + if *end_index < insert_after.len() { + for content in &insert_after[*end_index] { + result.push_str(content); + if !content.ends_with('\n') { + result.push('\n'); + } + } + } + i = end_index + 1; + } else { + // Keep the original line + result.push_str(original_lines[i]); + result.push('\n'); + // Emit any insertions after this line + for content in &insert_after[i] { + result.push_str(content); + if !content.ends_with('\n') { + result.push('\n'); + } + } + i += 1; + } + } + + // Preserve trailing newline behavior: if the original ended with a + // newline the result already has one; if it didn't, trim the extra one + // we added. + if !editable_region.ends_with('\n') && result.ends_with('\n') { + result.pop(); + } + + result + } + + /// Convert a unified diff patch into hashline edit commands. + /// + /// Parses the unified diff `patch` directly to determine which lines of + /// `old_text` are deleted/replaced and what new lines are added, then emits + /// `<|set|>` and `<|insert|>` edit commands referencing old lines by their + /// `{index}:{hash}` identifiers. + /// + /// `cursor_offset` is an optional byte offset into the first hunk's new + /// text (context + additions) where the cursor marker should be placed. + pub fn patch_to_edit_commands( + old_text: &str, + patch: &str, + cursor_offset: Option, + ) -> Result { + let old_lines: Vec<&str> = old_text.lines().collect(); + let old_hashes: Vec = old_lines + .iter() + .map(|line| hash_line(line.as_bytes())) + .collect(); + + let mut result = String::new(); + let mut first_hunk = true; + + struct Hunk<'a> { + line_range: Range, + new_text_lines: Vec<&'a str>, + cursor_line_offset_in_new_text: Option<(usize, usize)>, + } + + // Parse the patch line by line. We only care about hunk headers, + // context, deletions, and additions. + let mut old_line_index: usize = 0; + let mut current_hunk: Option = None; + // Byte offset tracking within the hunk's new text for cursor placement. + let mut new_text_byte_offset: usize = 0; + // The line index of the last old line seen before/in the current hunk + // (used for insert-after reference). + let mut last_old_line_before_hunk: Option = None; + + fn flush_hunk( + hunk: Hunk, + last_old_line: Option, + result: &mut String, + old_hashes: &[u8], + ) { + if hunk.line_range.is_empty() { + // Pure insertion — reference the old line to insert after when in bounds. + if let Some(after) = last_old_line + && let Some(&hash) = old_hashes.get(after) + { + write!( + result, + "{INSERT_COMMAND_MARKER}{}\n", + LineRef { index: after, hash } + ) + .unwrap(); + } else { + result.push_str(INSERT_COMMAND_MARKER); + result.push('\n'); + } + } else { + let start = hunk.line_range.start; + let end_exclusive = hunk.line_range.end; + let deleted_line_count = end_exclusive.saturating_sub(start); + + if deleted_line_count == 1 { + if let Some(&hash) = old_hashes.get(start) { + write!( + result, + "{SET_COMMAND_MARKER}{}\n", + LineRef { index: start, hash } + ) + .unwrap(); + } else { + result.push_str(SET_COMMAND_MARKER); + result.push('\n'); + } + } else { + let end_inclusive = end_exclusive - 1; + match ( + old_hashes.get(start).copied(), + old_hashes.get(end_inclusive).copied(), + ) { + (Some(start_hash), Some(end_hash)) => { + write!( + result, + "{SET_COMMAND_MARKER}{}-{}\n", + LineRef { + index: start, + hash: start_hash + }, + LineRef { + index: end_inclusive, + hash: end_hash + } + ) + .unwrap(); + } + _ => { + result.push_str(SET_COMMAND_MARKER); + result.push('\n'); + } + } + } + } + for (line_offset, line) in hunk.new_text_lines.iter().enumerate() { + if let Some((cursor_line_offset, char_offset)) = hunk.cursor_line_offset_in_new_text + && line_offset == cursor_line_offset + { + result.push_str(&line[..char_offset]); + result.push_str(CURSOR_MARKER); + result.push_str(&line[char_offset..]); + continue; + } + + result.push_str(line); + } + } + + for raw_line in patch.split_inclusive('\n') { + if raw_line.starts_with("@@") { + // Flush any pending change hunk from a previous patch hunk. + if let Some(hunk) = current_hunk.take() { + flush_hunk(hunk, last_old_line_before_hunk, &mut result, &old_hashes); + } + + // Parse hunk header: @@ -old_start[,old_count] +new_start[,new_count] @@ + // We intentionally do not trust old_start as a direct local index into `old_text`, + // because some patches are produced against a larger file region and carry + // non-local line numbers. We keep indexing local by advancing from parsed patch lines. + if first_hunk { + new_text_byte_offset = 0; + first_hunk = false; + } + continue; + } + + if raw_line.starts_with("---") || raw_line.starts_with("+++") { + continue; + } + if raw_line.starts_with("\\ No newline") { + continue; + } + + if raw_line.starts_with('-') { + // Extend or start a change hunk with this deleted old line. + match &mut current_hunk { + Some(Hunk { + line_range: range, .. + }) => range.end = old_line_index + 1, + None => { + current_hunk = Some(Hunk { + line_range: old_line_index..old_line_index + 1, + new_text_lines: Vec::new(), + cursor_line_offset_in_new_text: None, + }); + } + } + old_line_index += 1; + } else if let Some(added_content) = raw_line.strip_prefix('+') { + // Place cursor marker if cursor_offset falls within this line. + let mut cursor_line_offset = None; + if let Some(cursor_off) = cursor_offset + && (first_hunk + || cursor_off >= new_text_byte_offset + && cursor_off <= new_text_byte_offset + added_content.len()) + { + let line_offset = added_content.floor_char_boundary( + cursor_off + .saturating_sub(new_text_byte_offset) + .min(added_content.len()), + ); + cursor_line_offset = Some(line_offset); + } + + new_text_byte_offset += added_content.len(); + + let hunk = current_hunk.get_or_insert(Hunk { + line_range: old_line_index..old_line_index, + new_text_lines: vec![], + cursor_line_offset_in_new_text: None, + }); + hunk.new_text_lines.push(added_content); + hunk.cursor_line_offset_in_new_text = cursor_line_offset + .map(|offset_in_line| (hunk.new_text_lines.len() - 1, offset_in_line)); + } else { + // Context line (starts with ' ' or is empty). + if let Some(hunk) = current_hunk.take() { + flush_hunk(hunk, last_old_line_before_hunk, &mut result, &old_hashes); + } + last_old_line_before_hunk = Some(old_line_index); + old_line_index += 1; + let content = raw_line.strip_prefix(' ').unwrap_or(raw_line); + new_text_byte_offset += content.len(); + } + } + + // Flush final group. + if let Some(hunk) = current_hunk.take() { + flush_hunk(hunk, last_old_line_before_hunk, &mut result, &old_hashes); + } + + // Trim a single trailing newline. + if result.ends_with('\n') { + result.pop(); + } + + Ok(result) + } + + #[cfg(test)] + mod tests { + use super::*; + use indoc::indoc; + + #[test] + fn test_format_cursor_region() { + struct Case { + name: &'static str, + context: &'static str, + editable_range: Range, + cursor_offset: usize, + expected: &'static str, + } + + let cases = [ + Case { + name: "basic_cursor_placement", + context: "hello world\n", + editable_range: 0..12, + cursor_offset: 5, + expected: indoc! {" + <|file_sep|>test.rs + <|fim_prefix|> + <|fim_middle|>current + 0:5c|hello<|user_cursor|> world + <|fim_suffix|> + <|fim_middle|>updated"}, + }, + Case { + name: "multiline_cursor_on_second_line", + context: "aaa\nbbb\nccc\n", + editable_range: 0..12, + cursor_offset: 5, // byte 5 → 1 byte into "bbb" + expected: indoc! {" + <|file_sep|>test.rs + <|fim_prefix|> + <|fim_middle|>current + 0:23|aaa + 1:26|b<|user_cursor|>bb + 2:29|ccc + <|fim_suffix|> + <|fim_middle|>updated"}, + }, + Case { + name: "no_trailing_newline_in_context", + context: "line1\nline2", + editable_range: 0..11, + cursor_offset: 3, + expected: indoc! {" + <|file_sep|>test.rs + <|fim_prefix|> + <|fim_middle|>current + 0:d9|lin<|user_cursor|>e1 + 1:da|line2 + <|fim_suffix|> + <|fim_middle|>updated"}, + }, + Case { + name: "leading_newline_in_editable_region", + context: "\nabc\n", + editable_range: 0..5, + cursor_offset: 2, // byte 2 = 'a' in "abc" (after leading \n) + expected: indoc! {" + <|file_sep|>test.rs + <|fim_prefix|> + <|fim_middle|>current + 0:00| + 1:26|a<|user_cursor|>bc + <|fim_suffix|> + <|fim_middle|>updated"}, + }, + Case { + name: "with_suffix", + context: "abc\ndef", + editable_range: 0..4, // editable region = "abc\n", suffix = "def" + cursor_offset: 2, + expected: indoc! {" + <|file_sep|>test.rs + <|fim_prefix|> + <|fim_middle|>current + 0:26|ab<|user_cursor|>c + <|fim_suffix|> + def + <|fim_middle|>updated"}, + }, + Case { + name: "unicode_two_byte_chars", + context: "héllo\n", + editable_range: 0..7, + cursor_offset: 3, // byte 3 = after "hé" (h=1 byte, é=2 bytes), before "llo" + expected: indoc! {" + <|file_sep|>test.rs + <|fim_prefix|> + <|fim_middle|>current + 0:1b|hé<|user_cursor|>llo + <|fim_suffix|> + <|fim_middle|>updated"}, + }, + Case { + name: "unicode_three_byte_chars", + context: "日本語\n", + editable_range: 0..10, + cursor_offset: 6, // byte 6 = after "日本" (3+3 bytes), before "語" + expected: indoc! {" + <|file_sep|>test.rs + <|fim_prefix|> + <|fim_middle|>current + 0:80|日本<|user_cursor|>語 + <|fim_suffix|> + <|fim_middle|>updated"}, + }, + Case { + name: "unicode_four_byte_chars", + context: "a🌍b\n", + editable_range: 0..7, + cursor_offset: 5, // byte 5 = after "a🌍" (1+4 bytes), before "b" + expected: indoc! {" + <|file_sep|>test.rs + <|fim_prefix|> + <|fim_middle|>current + 0:6b|a🌍<|user_cursor|>b + <|fim_suffix|> + <|fim_middle|>updated"}, + }, + Case { + name: "cursor_at_start_of_region_not_placed", + context: "abc\n", + editable_range: 0..4, + cursor_offset: 0, // cursor_offset(0) > offset(0) is false → cursor not placed + expected: indoc! {" + <|file_sep|>test.rs + <|fim_prefix|> + <|fim_middle|>current + 0:26|abc + <|fim_suffix|> + <|fim_middle|>updated"}, + }, + Case { + name: "cursor_at_end_of_line_not_placed", + context: "abc\ndef\n", + editable_range: 0..8, + cursor_offset: 3, // byte 3 = the \n after "abc" → falls between lines, not placed + expected: indoc! {" + <|file_sep|>test.rs + <|fim_prefix|> + <|fim_middle|>current + 0:26|abc + 1:2f|def + <|fim_suffix|> + <|fim_middle|>updated"}, + }, + Case { + name: "cursor_offset_relative_to_context_not_editable_region", + // cursor_offset is relative to `context`, so when editable_range.start > 0, + // write_cursor_excerpt_section must subtract it before comparing against + // per-line offsets within the editable region. + context: "pre\naaa\nbbb\nsuf\n", + editable_range: 4..12, // editable region = "aaa\nbbb\n" + cursor_offset: 9, // byte 9 in context = second 'b' in "bbb" + expected: indoc! {" + <|file_sep|>test.rs + <|fim_prefix|> + pre + <|fim_middle|>current + 0:23|aaa + 1:26|b<|user_cursor|>bb + <|fim_suffix|> + suf + <|fim_middle|>updated"}, + }, + ]; + + for case in &cases { + let mut prompt = String::new(); + hashline::write_cursor_excerpt_section( + &mut prompt, + Path::new("test.rs"), + case.context, + &case.editable_range, + case.cursor_offset, + ); + assert_eq!(prompt, case.expected, "failed case: {}", case.name); + } + } + + #[test] + fn test_apply_edit_commands() { + struct Case { + name: &'static str, + original: &'static str, + model_output: &'static str, + expected: &'static str, + } + + let cases = vec![ + Case { + name: "set_single_line", + original: indoc! {" + let mut total = 0; + for product in products { + total += ; + } + total + "}, + model_output: indoc! {" + <|set|>2:87 + total += product.price; + "}, + expected: indoc! {" + let mut total = 0; + for product in products { + total += product.price; + } + total + "}, + }, + Case { + name: "set_range", + original: indoc! {" + fn foo() { + let x = 1; + let y = 2; + let z = 3; + } + "}, + model_output: indoc! {" + <|set|>1:46-3:4a + let sum = 6; + "}, + expected: indoc! {" + fn foo() { + let sum = 6; + } + "}, + }, + Case { + name: "insert_after_line", + original: indoc! {" + fn main() { + let x = 1; + } + "}, + model_output: indoc! {" + <|insert|>1:46 + let y = 2; + "}, + expected: indoc! {" + fn main() { + let x = 1; + let y = 2; + } + "}, + }, + Case { + name: "insert_before_first", + original: indoc! {" + let x = 1; + let y = 2; + "}, + model_output: indoc! {" + <|insert|> + use std::io; + "}, + expected: indoc! {" + use std::io; + let x = 1; + let y = 2; + "}, + }, + Case { + name: "set_with_cursor_marker", + original: indoc! {" + fn main() { + println!(); + } + "}, + model_output: indoc! {" + <|set|>1:34 + eprintln!(\"<|user_cursor|>\"); + "}, + expected: indoc! {" + fn main() { + eprintln!(\"<|user_cursor|>\"); + } + "}, + }, + Case { + name: "multiple_set_commands", + original: indoc! {" + aaa + bbb + ccc + ddd + "}, + model_output: indoc! {" + <|set|>0:23 + AAA + <|set|>2:29 + CCC + "}, + expected: indoc! {" + AAA + bbb + CCC + ddd + "}, + }, + Case { + name: "set_range_multiline_replacement", + original: indoc! {" + fn handle_submit() { + } + + fn handle_keystroke() { + "}, + model_output: indoc! {" + <|set|>0:3f-1:7d + fn handle_submit(modal_state: &mut ModalState) { + <|user_cursor|> + } + "}, + expected: indoc! {" + fn handle_submit(modal_state: &mut ModalState) { + <|user_cursor|> + } + + fn handle_keystroke() { + "}, + }, + Case { + name: "no_edit_commands_returns_original", + original: indoc! {" + hello + world + "}, + model_output: "some random text with no commands", + expected: indoc! {" + hello + world + "}, + }, + Case { + name: "wrong_hash_set_ignored", + original: indoc! {" + aaa + bbb + "}, + model_output: indoc! {" + <|set|>0:ff + ZZZ + "}, + expected: indoc! {" + aaa + bbb + "}, + }, + Case { + name: "insert_and_set_combined", + original: indoc! {" + alpha + beta + gamma + "}, + model_output: indoc! {" + <|set|>0:06 + ALPHA + <|insert|>1:9c + beta_extra + "}, + expected: indoc! {" + ALPHA + beta + beta_extra + gamma + "}, + }, + Case { + name: "no_trailing_newline_preserved", + original: "hello\nworld", + model_output: indoc! {" + <|set|>0:14 + HELLO + "}, + expected: "HELLO\nworld", + }, + Case { + name: "set_range_hash_mismatch_in_end_bound", + original: indoc! {" + one + two + three + "}, + model_output: indoc! {" + <|set|>0:42-2:ff + ONE_TWO_THREE + "}, + expected: indoc! {" + one + two + three + "}, + }, + Case { + name: "set_range_start_greater_than_end_ignored", + original: indoc! {" + a + b + c + "}, + model_output: indoc! {" + <|set|>2:63-1:62 + X + "}, + expected: indoc! {" + a + b + c + "}, + }, + Case { + name: "insert_out_of_bounds_ignored", + original: indoc! {" + x + y + "}, + model_output: indoc! {" + <|insert|>99:aa + z + "}, + expected: indoc! {" + x + y + "}, + }, + Case { + name: "set_out_of_bounds_ignored", + original: indoc! {" + x + y + "}, + model_output: indoc! {" + <|set|>99:aa + z + "}, + expected: indoc! {" + x + y + "}, + }, + Case { + name: "malformed_set_command_ignored", + original: indoc! {" + alpha + beta + "}, + model_output: indoc! {" + <|set|>not-a-line-ref + UPDATED + "}, + expected: indoc! {" + alpha + beta + "}, + }, + Case { + name: "malformed_insert_hash_treated_as_before_first", + original: indoc! {" + alpha + beta + "}, + model_output: indoc! {" + <|insert|>1:nothex + preamble + "}, + expected: indoc! {" + preamble + alpha + beta + "}, + }, + Case { + name: "set_then_insert_same_target_orders_insert_after_replacement", + original: indoc! {" + cat + dog + "}, + model_output: indoc! {" + <|set|>0:38 + CAT + <|insert|>0:38 + TAIL + "}, + expected: indoc! {" + CAT + TAIL + dog + "}, + }, + Case { + name: "overlapping_set_ranges_last_wins", + original: indoc! {" + a + b + c + d + "}, + model_output: indoc! {" + <|set|>0:61-2:63 + FIRST + <|set|>1:62-3:64 + SECOND + "}, + expected: indoc! {" + FIRST + d + "}, + }, + Case { + name: "insert_before_first_and_after_line", + original: indoc! {" + a + b + "}, + model_output: indoc! {" + <|insert|> + HEAD + <|insert|>0:61 + MID + "}, + expected: indoc! {" + HEAD + a + MID + b + "}, + }, + ]; + + for case in &cases { + let result = hashline::apply_edit_commands(case.original, &case.model_output); + assert_eq!(result, case.expected, "failed case: {}", case.name); + } + } + + #[test] + fn test_output_has_edit_commands() { + assert!(hashline::output_has_edit_commands(&format!( + "{}0:ab\nnew", + SET_COMMAND_MARKER + ))); + assert!(hashline::output_has_edit_commands(&format!( + "{}0:ab\nnew", + INSERT_COMMAND_MARKER + ))); + assert!(hashline::output_has_edit_commands(&format!( + "some text\n{}1:cd\nstuff", + SET_COMMAND_MARKER + ))); + assert!(!hashline::output_has_edit_commands("just plain text")); + assert!(!hashline::output_has_edit_commands("NO_EDITS")); + } + + // ---- hashline::patch_to_edit_commands round-trip tests ---- + + #[test] + fn test_patch_to_edit_commands() { + struct Case { + name: &'static str, + old: &'static str, + patch: &'static str, + expected_new: &'static str, + } + + let cases = [ + Case { + name: "single_line_replacement", + old: indoc! {" + let mut total = 0; + for product in products { + total += ; + } + total + "}, + patch: indoc! {" + @@ -1,5 +1,5 @@ + let mut total = 0; + for product in products { + - total += ; + + total += product.price; + } + total + "}, + expected_new: indoc! {" + let mut total = 0; + for product in products { + total += product.price; + } + total + "}, + }, + Case { + name: "multiline_replacement", + old: indoc! {" + fn foo() { + let x = 1; + let y = 2; + let z = 3; + } + "}, + patch: indoc! {" + @@ -1,5 +1,3 @@ + fn foo() { + - let x = 1; + - let y = 2; + - let z = 3; + + let sum = 1 + 2 + 3; + } + "}, + expected_new: indoc! {" + fn foo() { + let sum = 1 + 2 + 3; + } + "}, + }, + Case { + name: "insertion", + old: indoc! {" + fn main() { + let x = 1; + } + "}, + patch: indoc! {" + @@ -1,3 +1,4 @@ + fn main() { + let x = 1; + + let y = 2; + } + "}, + expected_new: indoc! {" + fn main() { + let x = 1; + let y = 2; + } + "}, + }, + Case { + name: "insertion_before_first", + old: indoc! {" + let x = 1; + let y = 2; + "}, + patch: indoc! {" + @@ -1,2 +1,3 @@ + +use std::io; + let x = 1; + let y = 2; + "}, + expected_new: indoc! {" + use std::io; + let x = 1; + let y = 2; + "}, + }, + Case { + name: "deletion", + old: indoc! {" + aaa + bbb + ccc + ddd + "}, + patch: indoc! {" + @@ -1,4 +1,2 @@ + aaa + -bbb + -ccc + ddd + "}, + expected_new: indoc! {" + aaa + ddd + "}, + }, + Case { + name: "multiple_changes", + old: indoc! {" + alpha + beta + gamma + delta + epsilon + "}, + patch: indoc! {" + @@ -1,5 +1,5 @@ + -alpha + +ALPHA + beta + gamma + -delta + +DELTA + epsilon + "}, + expected_new: indoc! {" + ALPHA + beta + gamma + DELTA + epsilon + "}, + }, + Case { + name: "replace_with_insertion", + old: indoc! {r#" + fn handle() { + modal_state.close(); + modal_state.dismiss(); + "#}, + patch: indoc! {r#" + @@ -1,3 +1,4 @@ + fn handle() { + modal_state.close(); + + eprintln!(""); + modal_state.dismiss(); + "#}, + expected_new: indoc! {r#" + fn handle() { + modal_state.close(); + eprintln!(""); + modal_state.dismiss(); + "#}, + }, + Case { + name: "complete_replacement", + old: indoc! {" + aaa + bbb + ccc + "}, + patch: indoc! {" + @@ -1,3 +1,3 @@ + -aaa + -bbb + -ccc + +xxx + +yyy + +zzz + "}, + expected_new: indoc! {" + xxx + yyy + zzz + "}, + }, + Case { + name: "add_function_body", + old: indoc! {" + fn foo() { + modal_state.dismiss(); + } + + fn + + fn handle_keystroke() { + "}, + patch: indoc! {" + @@ -1,6 +1,8 @@ + fn foo() { + modal_state.dismiss(); + } + + -fn + +fn handle_submit() { + + todo() + +} + + fn handle_keystroke() { + "}, + expected_new: indoc! {" + fn foo() { + modal_state.dismiss(); + } + + fn handle_submit() { + todo() + } + + fn handle_keystroke() { + "}, + }, + Case { + name: "with_cursor_offset", + old: indoc! {r#" + fn main() { + println!(); + } + "#}, + patch: indoc! {r#" + @@ -1,3 +1,3 @@ + fn main() { + - println!(); + + eprintln!(""); + } + "#}, + expected_new: indoc! {r#" + fn main() { + eprintln!("<|user_cursor|>"); + } + "#}, + }, + Case { + name: "non_local_hunk_header_pure_insertion_repro", + old: indoc! {" + aaa + bbb + "}, + patch: indoc! {" + @@ -20,2 +20,3 @@ + aaa + +xxx + bbb + "}, + expected_new: indoc! {" + aaa + xxx + bbb + "}, + }, + ]; + + for case in &cases { + // The cursor_offset for patch_to_edit_commands is relative to + // the first hunk's new text (context + additions). We compute + // it by finding where the marker sits in the expected output + // (which mirrors the new text of the hunk). + let cursor_offset = case.expected_new.find(CURSOR_MARKER); + + let commands = + hashline::patch_to_edit_commands(case.old, case.patch, cursor_offset) + .unwrap_or_else(|e| panic!("failed case {}: {e}", case.name)); + + assert!( + hashline::output_has_edit_commands(&commands), + "case {}: expected edit commands, got: {commands:?}", + case.name, + ); + + let applied = hashline::apply_edit_commands(case.old, &commands); + assert_eq!(applied, case.expected_new, "case {}", case.name); + } + } + } +} + pub mod seed_coder { //! Seed-Coder prompt format using SPM (Suffix-Prefix-Middle) FIM mode. //! @@ -847,6 +2392,17 @@ pub mod seed_coder { ] } + pub fn write_cursor_excerpt_section( + prompt: &mut String, + path: &Path, + context: &str, + editable_range: &Range, + cursor_offset: usize, + ) { + let section = build_cursor_prefix_section(path, context, editable_range, cursor_offset); + prompt.push_str(§ion); + } + pub fn format_prompt_with_budget( path: &Path, context: &str, @@ -1186,7 +2742,7 @@ mod tests { } fn format_with_budget(input: &ZetaPromptInput, max_tokens: usize) -> String { - format_zeta_prompt_with_budget(input, ZetaFormat::V0114180EditableRegion, max_tokens) + format_prompt_with_budget_for_format(input, ZetaFormat::V0114180EditableRegion, max_tokens) } #[test] @@ -1551,11 +3107,11 @@ mod tests { } fn format_seed_coder(input: &ZetaPromptInput) -> String { - format_zeta_prompt_with_budget(input, ZetaFormat::V0211SeedCoder, 10000) + format_prompt_with_budget_for_format(input, ZetaFormat::V0211SeedCoder, 10000) } fn format_seed_coder_with_budget(input: &ZetaPromptInput, max_tokens: usize) -> String { - format_zeta_prompt_with_budget(input, ZetaFormat::V0211SeedCoder, max_tokens) + format_prompt_with_budget_for_format(input, ZetaFormat::V0211SeedCoder, max_tokens) } #[test] diff --git a/docs/.prettierignore b/docs/.prettierignore index a52439689a83a1c2e834918c39441186b47120e5..c742ed4b6859f32219cecbac9f722db8a6929710 100644 --- a/docs/.prettierignore +++ b/docs/.prettierignore @@ -1,2 +1,5 @@ # Handlebars partials are not supported by Prettier. *.hbs + +# Automatically generated +theme/c15t@*.js diff --git a/docs/README.md b/docs/README.md index e1649f4bc99e1668352a46ee2071dcfe1775f4a7..a0f9bbd5c628f41d291880239ca555ea7ec0e3ea 100644 --- a/docs/README.md +++ b/docs/README.md @@ -64,6 +64,22 @@ This will render a human-readable version of the action name, e.g., "zed: open s Templates are functions that modify the source of the docs pages (usually with a regex match and replace). You can see how the actions and keybindings are templated in `crates/docs_preprocessor/src/main.rs` for reference on how to create new templates. +## Consent Banner + +We pre-bundle the `c15t` package because the docs pipeline does not include a JS bundler. If you need to update `c15t` and rebuild the bundle, use: + +``` +mkdir c15t-bundle && cd c15t-bundle +npm init -y +npm install c15t@ esbuild +echo "import { getOrCreateConsentRuntime } from 'c15t'; window.c15t = { getOrCreateConsentRuntime };" > entry.js +npx esbuild entry.js --bundle --format=iife --minify --outfile=c15t@.js +cp c15t@.js ../theme/c15t@.js +cd .. && rm -rf c15t-bundle +``` + +Replace `` with the new version of `c15t` you are installing. Then update `book.toml` to reference the new bundle filename. + ### References - Template Trait: `crates/docs_preprocessor/src/templates.rs` diff --git a/docs/book.toml b/docs/book.toml index 86fa447f581fba88ff7df53bb51e08440585a9dc..3269003a1d37ede19ec18b62809a928a08764d2f 100644 --- a/docs/book.toml +++ b/docs/book.toml @@ -23,8 +23,8 @@ default-description = "Learn how to use and customize Zed, the fast, collaborati default-title = "Zed Code Editor Documentation" no-section-label = true preferred-dark-theme = "dark" -additional-css = ["theme/page-toc.css", "theme/plugins.css", "theme/highlight.css"] -additional-js = ["theme/page-toc.js", "theme/plugins.js"] +additional-css = ["theme/page-toc.css", "theme/plugins.css", "theme/highlight.css", "theme/consent-banner.css"] +additional-js = ["theme/page-toc.js", "theme/plugins.js", "theme/c15t@2.0.0-rc.3.js", "theme/analytics.js"] [output.zed-html.print] enable = false diff --git a/docs/src/ai/llm-providers.md b/docs/src/ai/llm-providers.md index 3a32bd96e73d9df427897798681f203c4ceb2273..a4a6274af10d1aea20ed27160704136d9f0eb586 100644 --- a/docs/src/ai/llm-providers.md +++ b/docs/src/ai/llm-providers.md @@ -88,7 +88,7 @@ With that done, choose one of the three authentication methods: While it's possible to configure through the Agent Panel settings UI by entering your AWS access key and secret directly, we recommend using named profiles instead for better security practices. To do this: -1. Create an IAM User that you can assume in the [IAM Console](https://us-east-1.console.aws.amazon.com/iam/home?region=us-east-1#/users). +1. Create an IAM User in the [IAM Console](https://us-east-1.console.aws.amazon.com/iam/home?region=us-east-1#/users). 2. Create security credentials for that User, save them and keep them secure. 3. Open the Agent Configuration with (`agent: open settings`) and go to the Amazon Bedrock section 4. Copy the credentials from Step 2 into the respective **Access Key ID**, **Secret Access Key**, and **Region** fields. diff --git a/docs/src/ai/tools.md b/docs/src/ai/tools.md index 66f0af571d70fb8db7add2bd89139bf788369de6..faafc76b164f7f786c91c212bf51960f24a6bb0a 100644 --- a/docs/src/ai/tools.md +++ b/docs/src/ai/tools.md @@ -91,6 +91,6 @@ Executes shell commands and returns the combined output, creating a new shell pr ## Other Tools -### `subagent` +### `spawn_agent` -Spawns a subagent with its own context window to perform a delegated task. Useful for running parallel investigations, completing self-contained tasks, or performing research where only the outcome matters. Each subagent has access to the same tools as the parent agent. +Spawns a subagent with its own context window to perform a delegated task. Each subagent has access to the same tools as the parent agent. diff --git a/docs/src/configuring-languages.md b/docs/src/configuring-languages.md index 4e9bbce822f2f0d87ac2a8c9617698acd5983243..91775c3df137e38eb0b6b7b333b49d269b2f3a7c 100644 --- a/docs/src/configuring-languages.md +++ b/docs/src/configuring-languages.md @@ -122,11 +122,40 @@ You can specify your preference using the `language_servers` setting: In this example: -- `intelephense` is set as the primary language server -- `phpactor` is disabled (note the `!` prefix) -- `...` expands to the rest of the language servers that are registered for PHP +- `intelephense` is set as the primary language server. +- `phpactor` and `phptools` are disabled (note the `!` prefix). +- `"..."` expands to the rest of the language servers registered for PHP that are not already listed. -This configuration allows you to tailor the language server setup to your specific needs, ensuring that you get the most suitable functionality for your development workflow. +The `"..."` entry acts as a wildcard that includes any registered language server you haven't explicitly mentioned. Servers you list by name keep their position, and `"..."` fills in the remaining ones at that point in the list. Servers prefixed with `!` are excluded entirely. This means that if a new language server extension is installed or a new server is registered for a language, `"..."` will automatically include it. If you want full control over which servers are enabled, omit `"..."` — only the servers you list by name will be used. + +#### Examples + +Suppose you're working with Ruby. The default configuration is: + +```json [settings] +{ + "language_servers": [ + "solargraph", + "!ruby-lsp", + "!rubocop", + "!sorbet", + "!steep", + "!kanayago", + "..." + ] +} +``` + +When you override `language_servers` in your settings, your list **replaces** the default entirely. This means default-disabled servers like `kanayago` will be re-enabled by `"..."` unless you explicitly disable them again. + +| Configuration | Result | +| ------------------------------------------------- | ------------------------------------------------------------------ | +| `["..."]` | `solargraph`, `ruby-lsp`, `rubocop`, `sorbet`, `steep`, `kanayago` | +| `["ruby-lsp", "..."]` | `ruby-lsp`, `solargraph`, `rubocop`, `sorbet`, `steep`, `kanayago` | +| `["ruby-lsp", "!solargraph", "!kanayago", "..."]` | `ruby-lsp`, `rubocop`, `sorbet`, `steep` | +| `["ruby-lsp", "solargraph"]` | `ruby-lsp`, `solargraph` | + +> Note: In the first example, `"..."` includes `kanayago` even though it is disabled by default. The override replaced the default list, so the `"!kanayago"` entry is no longer present. To keep it disabled, you must include `"!kanayago"` in your configuration. ### Toolchains diff --git a/docs/src/languages/python.md b/docs/src/languages/python.md index d66f52c71cb9295fe9ca94e5890de48cd1275e57..fdeabec5069ed20a9b168ab19129dde0cc6280ba 100644 --- a/docs/src/languages/python.md +++ b/docs/src/languages/python.md @@ -89,8 +89,8 @@ Configure language servers in Settings ({#kb zed::OpenSettings}) under Languages "languages": { "Python": { "language_servers": [ - // Disable basedpyright and enable ty, and otherwise - // use the default configuration. + // Disable basedpyright and enable ty, and include all + // other registered language servers (ruff, pylsp, pyright). "ty", "!basedpyright", "..." diff --git a/docs/src/performance.md b/docs/src/performance.md index 09abecdeffe4e268413a73b189ef301511b1a20e..e974d63f8816b68d30a1c06d7cbbc083f8564327 100644 --- a/docs/src/performance.md +++ b/docs/src/performance.md @@ -78,7 +78,7 @@ Download the importer - `cd import && mkdir build && cd build` - Run cmake to generate build files: `cmake -G Ninja -DCMAKE_BUILD_TYPE=Release ..` - Build the importer: `ninja` -- Run the importer on the trace file: `./tracy-import-miniprofiler /path/to/trace.miniprof /path/to/output.tracy` +- Run the importer on the trace file: `./tracy-import-miniprofiler /path/to/trace.miniprof.json /path/to/output.tracy` - Open the trace in tracy: - If you're on windows download the v0.12.2 version from the releases on the upstream repo - If you're on other platforms open it on the website: https://tracy.nereid.pl/ (the version might mismatch so your luck might vary, we need to host our own ideally..) @@ -87,7 +87,7 @@ Download the importer - Run the action: `zed open performance profiler` - Hit the save button. This opens a save dialog or if that fails to open the trace gets saved in your working directory. -- Convert the profile so it can be imported in tracy using the importer: `./tracy-import-miniprofiler output.tracy` +- Convert the profile so it can be imported in tracy using the importer: `./tracy-import-miniprofiler output.tracy` - Go to hit the 'power button' in the top left and then open saved trace. - Now zoom in to see the tasks and how long they took diff --git a/docs/theme/analytics.js b/docs/theme/analytics.js new file mode 100644 index 0000000000000000000000000000000000000000..6e9df27f30fc6d38ba6fb322f9888fda089bb20c --- /dev/null +++ b/docs/theme/analytics.js @@ -0,0 +1,93 @@ +const amplitudeKey = document.querySelector( + 'meta[name="amplitude-key"]', +)?.content; +const consentInstance = document.querySelector( + 'meta[name="consent-io-instance"]', +)?.content; + +document.addEventListener("DOMContentLoaded", () => { + if (!consentInstance || consentInstance.length === 0) return; + const { getOrCreateConsentRuntime } = window.c15t; + + const { consentStore } = getOrCreateConsentRuntime({ + mode: "c15t", + backendURL: consentInstance, + consentCategories: ["necessary", "measurement", "marketing"], + storageConfig: { + crossSubdomain: true, + }, + scripts: [ + { + id: "amplitude", + src: `https://cdn.amplitude.com/script/${amplitudeKey}.js`, + category: "measurement", + onLoad: () => { + window.amplitude.init(amplitudeKey, { + fetchRemoteConfig: true, + autocapture: true, + }); + }, + }, + ], + }); + + let previousActiveUI = consentStore.getState().activeUI; + const banner = document.getElementById("c15t-banner"); + const configureSection = document.getElementById("c15t-configure-section"); + const configureBtn = document.getElementById("c15t-configure-btn"); + const measurementToggle = document.getElementById("c15t-toggle-measurement"); + const marketingToggle = document.getElementById("c15t-toggle-marketing"); + + const toggleConfigureMode = () => { + const currentConsents = consentStore.getState().consents; + measurementToggle.checked = currentConsents + ? (currentConsents.measurement ?? false) + : false; + marketingToggle.checked = currentConsents + ? (currentConsents.marketing ?? false) + : false; + configureSection.style.display = "flex"; + configureBtn.innerHTML = "Save"; + configureBtn.className = "c15t-button secondary"; + configureBtn.title = ""; + }; + + consentStore.subscribe((state) => { + const hideBanner = + state.activeUI === "none" || + (state.activeUI === "banner" && state.mode === "opt-out"); + banner.style.display = hideBanner ? "none" : "block"; + + if (state.activeUI === "dialog" && previousActiveUI !== "dialog") { + toggleConfigureMode(); + } + + previousActiveUI = state.activeUI; + }); + + configureBtn.addEventListener("click", () => { + if (consentStore.getState().activeUI === "dialog") { + consentStore + .getState() + .setConsent("measurement", measurementToggle.checked); + consentStore.getState().setConsent("marketing", marketingToggle.checked); + consentStore.getState().saveConsents("custom"); + } else { + consentStore.getState().setActiveUI("dialog"); + } + }); + + document.getElementById("c15t-accept").addEventListener("click", () => { + consentStore.getState().saveConsents("all"); + }); + + document.getElementById("c15t-decline").addEventListener("click", () => { + consentStore.getState().saveConsents("necessary"); + }); + + document + .getElementById("c15t-manage-consent-btn") + .addEventListener("click", () => { + consentStore.getState().setActiveUI("dialog"); + }); +}); diff --git a/docs/theme/c15t@2.0.0-rc.3.js b/docs/theme/c15t@2.0.0-rc.3.js new file mode 100644 index 0000000000000000000000000000000000000000..5e4a38c12b605062bd8e7e77809d03e3aa11ff74 --- /dev/null +++ b/docs/theme/c15t@2.0.0-rc.3.js @@ -0,0 +1 @@ +(()=>{var ni=Object.defineProperty;var P=(n,e)=>()=>(n&&(e=n(n=0)),e);var Nt=(n,e)=>{for(var t in e)ni(n,t,{get:e[t],enumerable:!0})};var G,$t=P(()=>{G=class extends Error{constructor(e){super(e),this.name="DecodingError"}}});var q,Kt=P(()=>{q=class extends Error{constructor(e){super(e),this.name="EncodingError"}}});var oe,Yt=P(()=>{oe=class extends Error{constructor(e){super(e),this.name="GVLError"}}});var W,Wt=P(()=>{W=class extends Error{constructor(e,t,i=""){super(`invalid value ${t} passed for ${e} ${i}`),this.name="TCModelError"}}});var J=P(()=>{$t();Kt();Yt();Wt()});var pe,rt=P(()=>{J();pe=class{static DICT="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";static REVERSE_DICT=new Map([["A",0],["B",1],["C",2],["D",3],["E",4],["F",5],["G",6],["H",7],["I",8],["J",9],["K",10],["L",11],["M",12],["N",13],["O",14],["P",15],["Q",16],["R",17],["S",18],["T",19],["U",20],["V",21],["W",22],["X",23],["Y",24],["Z",25],["a",26],["b",27],["c",28],["d",29],["e",30],["f",31],["g",32],["h",33],["i",34],["j",35],["k",36],["l",37],["m",38],["n",39],["o",40],["p",41],["q",42],["r",43],["s",44],["t",45],["u",46],["v",47],["w",48],["x",49],["y",50],["z",51],["0",52],["1",53],["2",54],["3",55],["4",56],["5",57],["6",58],["7",59],["8",60],["9",61],["-",62],["_",63]]);static BASIS=6;static LCM=24;static encode(e){if(!/^[0-1]+$/.test(e))throw new q("Invalid bitField");let t=e.length%this.LCM;e+=t?"0".repeat(this.LCM-t):"";let i="";for(let s=0;s{Ee=class n{static langSet=new Set(["AR","BG","BS","CA","CS","CY","DA","DE","EL","EN","ES","ET","EU","FI","FR","GL","HE","HI","HR","HU","ID","IS","IT","JA","KA","KO","LT","LV","MK","MS","MT","NL","NO","PL","PT-BR","PT-PT","RO","RU","SK","SL","SQ","SR-LATN","SR-CYRL","SV","SW","TH","TL","TR","UK","VI","ZH","ZH-HANT"]);has(e){return n.langSet.has(e)}parseLanguage(e){e=e.toUpperCase();let t=e.split("-")[0];if(e.length>=2&&t.length==2){if(n.langSet.has(e))return e;if(n.langSet.has(t))return t;let i=t+"-"+t;if(n.langSet.has(i))return i;for(let s of n.langSet)if(s.indexOf(e)!==-1||s.indexOf(t)!==-1)return s}throw new Error(`unsupported language ${e}`)}forEach(e){n.langSet.forEach(e)}get size(){return n.langSet.size}}});var v,ot=P(()=>{v=class{static cmpId="cmpId";static cmpVersion="cmpVersion";static consentLanguage="consentLanguage";static consentScreen="consentScreen";static created="created";static supportOOB="supportOOB";static isServiceSpecific="isServiceSpecific";static lastUpdated="lastUpdated";static numCustomPurposes="numCustomPurposes";static policyVersion="policyVersion";static publisherCountryCode="publisherCountryCode";static publisherCustomConsents="publisherCustomConsents";static publisherCustomLegitimateInterests="publisherCustomLegitimateInterests";static publisherLegitimateInterests="publisherLegitimateInterests";static publisherConsents="publisherConsents";static publisherRestrictions="publisherRestrictions";static purposeConsents="purposeConsents";static purposeLegitimateInterests="purposeLegitimateInterests";static purposeOneTreatment="purposeOneTreatment";static specialFeatureOptins="specialFeatureOptins";static useNonStandardTexts="useNonStandardTexts";static vendorConsents="vendorConsents";static vendorLegitimateInterests="vendorLegitimateInterests";static vendorListVersion="vendorListVersion";static vendorsAllowed="vendorsAllowed";static vendorsDisclosed="vendorsDisclosed";static version="version"}});var Zt=P(()=>{});var Qt=P(()=>{});var te,fe=P(()=>{te=class{clone(){let e=new this.constructor;return Object.keys(this).forEach(i=>{let s=this.deepClone(this[i]);s!==void 0&&(e[i]=s)}),e}deepClone(e){let t=typeof e;if(t==="number"||t==="string"||t==="boolean")return e;if(e!==null&&t==="object"){if(typeof e.clone=="function")return e.clone();if(e instanceof Date)return new Date(e.getTime());if(e[Symbol.iterator]!==void 0){let i=[];for(let s of e)i.push(this.deepClone(s));return e instanceof Array?i:new e.constructor(i)}else{let i={};for(let s in e)e.hasOwnProperty(s)&&(i[s]=this.deepClone(e[s]));return i}}}}});var Z,Ge=P(()=>{(function(n){n[n.NOT_ALLOWED=0]="NOT_ALLOWED",n[n.REQUIRE_CONSENT=1]="REQUIRE_CONSENT",n[n.REQUIRE_LI=2]="REQUIRE_LI"})(Z||(Z={}))});var ae,at=P(()=>{fe();J();Ge();ae=class n extends te{static hashSeparator="-";purposeId_;restrictionType;constructor(e,t){super(),e!==void 0&&(this.purposeId=e),t!==void 0&&(this.restrictionType=t)}static unHash(e){let t=e.split(this.hashSeparator),i=new n;if(t.length!==2)throw new W("hash",e);return i.purposeId=parseInt(t[0],10),i.restrictionType=parseInt(t[1],10),i}get hash(){if(!this.isValid())throw new Error("cannot hash invalid PurposeRestriction");return`${this.purposeId}${n.hashSeparator}${this.restrictionType}`}get purposeId(){return this.purposeId_}set purposeId(e){this.purposeId_=e}isValid(){return Number.isInteger(this.purposeId)&&this.purposeId>0&&(this.restrictionType===Z.NOT_ALLOWED||this.restrictionType===Z.REQUIRE_CONSENT||this.restrictionType===Z.REQUIRE_LI)}isSameAs(e){return this.purposeId===e.purposeId&&this.restrictionType===e.restrictionType}}});var he,Xt=P(()=>{at();Ge();fe();he=class extends te{bitLength=0;map=new Map;gvl_;has(e){return this.map.has(e)}isOkToHave(e,t,i){let s=!0;if(this.gvl?.vendors){let r=this.gvl.vendors[i];if(r)if(e===Z.NOT_ALLOWED)s=r.legIntPurposes.includes(t)||r.purposes.includes(t);else if(r.flexiblePurposes.length)switch(e){case Z.REQUIRE_CONSENT:s=r.flexiblePurposes.includes(t)&&r.legIntPurposes.includes(t);break;case Z.REQUIRE_LI:s=r.flexiblePurposes.includes(t)&&r.purposes.includes(t);break}else s=!1;else s=!1}return s}add(e,t){if(this.isOkToHave(t.restrictionType,t.purposeId,e)){let i=t.hash;this.has(i)||(this.map.set(i,new Set),this.bitLength=0),this.map.get(i).add(e)}}restrictPurposeToLegalBasis(e){let t=Array.from(this.gvl.vendorIds),i=e.hash,s=t[t.length-1],r=[...Array(s).keys()].map(o=>o+1);if(!this.has(i))this.map.set(i,new Set(r)),this.bitLength=0;else for(let o=1;o<=s;o++)this.map.get(i).add(o)}getVendors(e){let t=[];if(e){let i=e.hash;this.has(i)&&(t=Array.from(this.map.get(i)))}else{let i=new Set;this.map.forEach(s=>{s.forEach(r=>{i.add(r)})}),t=Array.from(i)}return t.sort((i,s)=>i-s)}getRestrictionType(e,t){let i;return this.getRestrictions(e).forEach(s=>{s.purposeId===t&&(i===void 0||i>s.restrictionType)&&(i=s.restrictionType)}),i}vendorHasRestriction(e,t){let i=!1,s=this.getRestrictions(e);for(let r=0;r{e=Math.max(Array.from(t)[t.size-1],e)}),e}getRestrictions(e){let t=[];return this.map.forEach((i,s)=>{e?i.has(e)&&t.push(ae.unHash(s)):t.push(ae.unHash(s))}),t}getPurposes(){let e=new Set;return this.map.forEach((t,i)=>{e.add(ae.unHash(i).purposeId)}),Array.from(e)}remove(e,t){let i=t.hash,s=this.map.get(i);s&&(s.delete(e),s.size==0&&(this.map.delete(i),this.bitLength=0))}set gvl(e){this.gvl_||(this.gvl_=e,this.map.forEach((t,i)=>{let s=ae.unHash(i);Array.from(t).forEach(o=>{this.isOkToHave(s.restrictionType,s.purposeId,o)||t.delete(o)})}))}get gvl(){return this.gvl_}isEmpty(){return this.map.size===0}get numRestrictions(){return this.map.size}}});var ct,en=P(()=>{(function(n){n.COOKIE="cookie",n.WEB="web",n.APP="app"})(ct||(ct={}))});var tn=P(()=>{});var N,lt=P(()=>{(function(n){n.CORE="core",n.VENDORS_DISCLOSED="vendorsDisclosed",n.VENDORS_ALLOWED="vendorsAllowed",n.PUBLISHER_TC="publisherTC"})(N||(N={}))});var ke,nn=P(()=>{lt();ke=class{static ID_TO_KEY=[N.CORE,N.VENDORS_DISCLOSED,N.VENDORS_ALLOWED,N.PUBLISHER_TC];static KEY_TO_ID={[N.CORE]:0,[N.VENDORS_DISCLOSED]:1,[N.VENDORS_ALLOWED]:2,[N.PUBLISHER_TC]:3}}});var H,sn=P(()=>{fe();J();H=class extends te{bitLength=0;maxId_=0;set_=new Set;*[Symbol.iterator](){for(let e=1;e<=this.maxId;e++)yield[e,this.has(e)]}values(){return this.set_.values()}get maxId(){return this.maxId_}has(e){return this.set_.has(e)}unset(e){Array.isArray(e)?e.forEach(t=>this.unset(t)):typeof e=="object"?this.unset(Object.keys(e).map(t=>Number(t))):(this.set_.delete(Number(e)),this.bitLength=0,e===this.maxId&&(this.maxId_=0,this.set_.forEach(t=>{this.maxId_=Math.max(this.maxId,t)})))}isIntMap(e){let t=typeof e=="object";return t=t&&Object.keys(e).every(i=>{let s=Number.isInteger(parseInt(i,10));return s=s&&this.isValidNumber(e[i].id),s=s&&e[i].name!==void 0,s}),t}isValidNumber(e){return parseInt(e,10)>0}isSet(e){let t=!1;return e instanceof Set&&(t=Array.from(e).every(this.isValidNumber)),t}set(e){if(Array.isArray(e))e.forEach(t=>this.set(t));else if(this.isSet(e))this.set(Array.from(e));else if(this.isIntMap(e))this.set(Object.keys(e).map(t=>Number(t)));else if(this.isValidNumber(e))this.set_.add(e),this.maxId_=Math.max(this.maxId,e),this.bitLength=0;else throw new W("set()",e,"must be positive integer array, positive integer, Set, or IntMap")}empty(){this.set_=new Set,this.maxId_=0}forEach(e){for(let t=1;t<=this.maxId;t++)e(this.has(t),t)}get size(){return this.set_.size}setAll(e){this.set(e)}unsetAll(e){this.unset(e)}}});var rn=P(()=>{});var on=P(()=>{});var an=P(()=>{});var cn=P(()=>{});var ln=P(()=>{});var un=P(()=>{});var dn=P(()=>{});var pn=P(()=>{});var gn=P(()=>{});var mn=P(()=>{});var fn=P(()=>{});var hn=P(()=>{rn();on();an();cn();ln();un();dn();pn();gn();mn();fn()});var Q=P(()=>{Jt();ot();Zt();Qt();at();Xt();en();tn();Ge();lt();nn();sn();hn()});var S,He=P(()=>{Q();S=class{static[v.cmpId]=12;static[v.cmpVersion]=12;static[v.consentLanguage]=12;static[v.consentScreen]=6;static[v.created]=36;static[v.isServiceSpecific]=1;static[v.lastUpdated]=36;static[v.policyVersion]=6;static[v.publisherCountryCode]=12;static[v.publisherLegitimateInterests]=24;static[v.publisherConsents]=24;static[v.purposeConsents]=24;static[v.purposeLegitimateInterests]=24;static[v.purposeOneTreatment]=1;static[v.specialFeatureOptins]=12;static[v.useNonStandardTexts]=1;static[v.vendorListVersion]=12;static[v.version]=6;static anyBoolean=1;static encodingType=1;static maxId=16;static numCustomPurposes=6;static numEntries=12;static numRestrictions=12;static purposeId=6;static restrictionType=2;static segmentType=3;static singleOrRange=1;static vendorId=16}});var kn=P(()=>{});var $,je=P(()=>{$=class{static encode(e){return String(Number(e))}static decode(e){return e==="1"}}});var L,ge=P(()=>{J();L=class{static encode(e,t){let i;if(typeof e=="string"&&(e=parseInt(e,10)),i=e.toString(2),i.length>t||e<0)throw new q(`${e} too large to encode into ${t}`);return i.length{ge();J();Se=class{static encode(e,t){return L.encode(Math.round(e.getTime()/100),t)}static decode(e,t){if(t!==e.length)throw new G("invalid bit length");let i=new Date;return i.setTime(L.decode(e,t)*100),i}}});var ne,qe=P(()=>{je();J();Q();ne=class{static encode(e,t){let i="";for(let s=1;s<=t;s++)i+=$.encode(e.has(s));return i}static decode(e,t){if(e.length!==t)throw new G("bitfield encoding length mismatch");let i=new H;for(let s=1;s<=t;s++)$.decode(e[s-1])&&i.set(s);return i.bitLength=e.length,i}}});var Ae,dt=P(()=>{ge();J();Ae=class{static encode(e,t){e=e.toUpperCase();let i=65,s=e.charCodeAt(0)-i,r=e.charCodeAt(1)-i;if(s<0||s>25||r<0||r>25)throw new q(`invalid language code: ${e}`);if(t%2===1)throw new q(`numBits must be even, ${t} is not valid`);t=t/2;let o=L.encode(s,t),a=L.encode(r,t);return o+a}static decode(e,t){let i;if(t===e.length&&!(e.length%2)){let r=e.length/2,o=L.decode(e.slice(0,r),r)+65,a=L.decode(e.slice(r),r)+65;i=String.fromCharCode(o)+String.fromCharCode(a)}else throw new G("invalid bit length for language");return i}}});var Ve,pt=P(()=>{He();je();J();ge();Q();Ve=class{static encode(e){let t=L.encode(e.numRestrictions,S.numRestrictions);if(!e.isEmpty()){let i=(s,r)=>{for(let o=s+1;o<=r;o++)if(e.gvl.vendorIds.has(o))return o;return s};e.getRestrictions().forEach(s=>{t+=L.encode(s.purposeId,S.purposeId),t+=L.encode(s.restrictionType,S.restrictionType);let r=e.getVendors(s),o=r.length,a=0,c=0,u="";for(let p=0;pi(d,r[o-1])){let l=d!==c;u+=$.encode(l),u+=L.encode(c,S.vendorId),l&&(u+=L.encode(d,S.vendorId)),c=0}}t+=L.encode(a,S.numEntries),t+=u})}return t}static decode(e){let t=0,i=new he,s=L.decode(e.substr(t,S.numRestrictions),S.numRestrictions);t+=S.numRestrictions;for(let r=0;r{(function(n){n[n.FIELD=0]="FIELD",n[n.RANGE=1]="RANGE"})(ve||(ve={}))});var ce,mt=P(()=>{Q();$e();ge();je();qe();gt();J();ce=class{static encode(e){let t=[],i=[],s=L.encode(e.maxId,S.maxId),r="",o,a=S.maxId+S.encodingType,c=a+e.maxId,u=S.vendorId*2+S.singleOrRange+S.numEntries,p=a+S.numEntries;return e.forEach((d,l)=>{r+=$.encode(d),o=e.maxId>u&&p{let r=s.length===1;i+=$.encode(!r),i+=L.encode(s[0],S.vendorId),r||(i+=L.encode(s[1],S.vendorId))}),i}}});function Ke(){return{[v.version]:L,[v.created]:Se,[v.lastUpdated]:Se,[v.cmpId]:L,[v.cmpVersion]:L,[v.consentScreen]:L,[v.consentLanguage]:Ae,[v.vendorListVersion]:L,[v.policyVersion]:L,[v.isServiceSpecific]:$,[v.useNonStandardTexts]:$,[v.specialFeatureOptins]:ne,[v.purposeConsents]:ne,[v.purposeLegitimateInterests]:ne,[v.purposeOneTreatment]:$,[v.publisherCountryCode]:Ae,[v.vendorConsents]:ce,[v.vendorLegitimateInterests]:ce,[v.publisherRestrictions]:Ve,segmentType:L,[v.vendorsDisclosed]:ce,[v.vendorsAllowed]:ce,[v.publisherConsents]:ne,[v.publisherLegitimateInterests]:ne,[v.numCustomPurposes]:L,[v.publisherCustomConsents]:ne,[v.publisherCustomLegitimateInterests]:ne}}var vn=P(()=>{Q();je();ut();qe();ge();dt();pt();mt()});var ft=P(()=>{je();ut();vn();qe();ge();dt();pt();gt();mt()});var xe,yn=P(()=>{Q();xe=class{1={[N.CORE]:[v.version,v.created,v.lastUpdated,v.cmpId,v.cmpVersion,v.consentScreen,v.consentLanguage,v.vendorListVersion,v.purposeConsents,v.vendorConsents]};2={[N.CORE]:[v.version,v.created,v.lastUpdated,v.cmpId,v.cmpVersion,v.consentScreen,v.consentLanguage,v.vendorListVersion,v.policyVersion,v.isServiceSpecific,v.useNonStandardTexts,v.specialFeatureOptins,v.purposeConsents,v.purposeLegitimateInterests,v.purposeOneTreatment,v.publisherCountryCode,v.vendorConsents,v.vendorLegitimateInterests,v.publisherRestrictions],[N.VENDORS_DISCLOSED]:[v.vendorsDisclosed],[N.PUBLISHER_TC]:[v.publisherConsents,v.publisherLegitimateInterests,v.numCustomPurposes,v.publisherCustomConsents,v.publisherCustomLegitimateInterests],[N.VENDORS_ALLOWED]:[v.vendorsAllowed]}}});var Fe,bn=P(()=>{Q();Fe=class{1=[N.CORE];2=[N.CORE];constructor(e,t){if(e.version===2)if(e.isServiceSpecific)this[2].push(N.VENDORS_DISCLOSED),this[2].push(N.PUBLISHER_TC);else{let i=!!(t&&t.isForVendors);(!i||e[v.supportOOB]===!0)&&this[2].push(N.VENDORS_DISCLOSED),i&&(e[v.supportOOB]&&e[v.vendorsAllowed].size>0&&this[2].push(N.VENDORS_ALLOWED),this[2].push(N.PUBLISHER_TC))}}}});var wn=P(()=>{});var ht=P(()=>{yn();bn();wn()});var ze,Cn=P(()=>{rt();He();ft();ht();J();ot();Q();ze=class{static fieldSequence=new xe;static encode(e,t){let i;try{i=this.fieldSequence[String(e.version)][t]}catch{throw new q(`Unable to encode version: ${e.version}, segment: ${t}`)}let s="";t!==N.CORE&&(s=L.encode(ke.KEY_TO_ID[t],S.segmentType));let r=Ke();return i.forEach(o=>{let a=e[o],c=r[o],u=S[o];u===void 0&&this.isPublisherCustom(o)&&(u=Number(e[v.numCustomPurposes]));try{s+=c.encode(a,u)}catch(p){throw new q(`Error encoding ${t}->${o}: ${p.message}`)}}),pe.encode(s)}static decode(e,t,i){let s=pe.decode(e),r=0;i===N.CORE&&(t.version=L.decode(s.substr(r,S[v.version]),S[v.version])),i!==N.CORE&&(r+=S.segmentType);let o=this.fieldSequence[String(t.version)][i],a=Ke();return o.forEach(c=>{let u=a[c],p=S[c];if(p===void 0&&this.isPublisherCustom(c)&&(p=Number(t[v.numCustomPurposes])),p!==0){let d=s.substr(r,p);if(u===ce?t[c]=u.decode(d,t.version):t[c]=u.decode(d,p),Number.isInteger(p))r+=p;else if(Number.isInteger(t[c].bitLength))r+=t[c].bitLength;else throw new G(c)}}),t}static isPublisherCustom(e){return e.indexOf("publisherCustom")===0}}});var Be,In=P(()=>{J();Q();Be=class{static processor=[e=>e,(e,t)=>{e.publisherRestrictions.gvl=t,e.purposeLegitimateInterests.unset([1,3,4,5,6]);let i=new Map;return i.set("legIntPurposes",e.vendorLegitimateInterests),i.set("purposes",e.vendorConsents),i.forEach((s,r)=>{s.forEach((o,a)=>{if(o){let c=t.vendors[a];if(!c||c.deletedDate)s.unset(a);else if(c[r].length===0)if(r==="legIntPurposes"&&c.purposes.length===0&&c.legIntPurposes.length===0&&c.specialPurposes.length>0)s.set(a);else if(r==="legIntPurposes"&&c.purposes.length>0&&c.legIntPurposes.length===0&&c.specialPurposes.length>0)s.set(a);else if(e.isServiceSpecific)if(c.flexiblePurposes.length===0)s.unset(a);else{let u=e.publisherRestrictions.getRestrictions(a),p=!1;for(let d=0,l=u.length;d0&&t?.version<=this.processor.length?e.version=t.version:e.version=this.processor.length;let s=e.version-1;if(!this.processor[s])throw new q(`Invalid version: ${e.version}`);return this.processor[s](e,i)}}});var $e=P(()=>{rt();He();kn();Cn();In();ft();ht()});var De,kt=P(()=>{De=class{static absCall(e,t,i,s){return new Promise((r,o)=>{let a=new XMLHttpRequest,c=()=>{if(a.readyState==XMLHttpRequest.DONE)if(a.status>=200&&a.status<300){let l=a.response;if(typeof l=="string")try{l=JSON.parse(l)}catch{}r(l)}else o(new Error(`HTTP Status: ${a.status} response type: ${a.responseType}`))},u=()=>{o(new Error("error"))},p=()=>{o(new Error("aborted"))},d=()=>{o(new Error("Timeout "+s+"ms "+e))};a.withCredentials=i,a.addEventListener("load",c),a.addEventListener("error",u),a.addEventListener("abort",p),t===null?a.open("GET",e,!0):a.open("POST",e,!0),a.responseType="json",a.timeout=s,a.ontimeout=d,a.send(t)})}static post(e,t,i=!1,s=0){return this.absCall(e,JSON.stringify(t),i,s)}static fetch(e,t=!1,i=0){return this.absCall(e,null,t,i)}}});var ye,vt=P(()=>{fe();J();kt();Q();ye=class n extends te{static LANGUAGE_CACHE=new Map;static CACHE=new Map;static LATEST_CACHE_KEY=0;static DEFAULT_LANGUAGE="EN";static consentLanguages=new Ee;static baseUrl_;static set baseUrl(e){if(/^https?:\/\/vendorlist\.consensu\.org\//.test(e))throw new oe("Invalid baseUrl! You may not pull directly from vendorlist.consensu.org and must provide your own cache");e.length>0&&e[e.length-1]!=="/"&&(e+="/"),this.baseUrl_=e}static get baseUrl(){return this.baseUrl_}static latestFilename="vendor-list.json";static versionedFilename="archives/vendor-list-v[VERSION].json";static languageFilename="purposes-[LANG].json";readyPromise;gvlSpecificationVersion;vendorListVersion;tcfPolicyVersion;lastUpdated;purposes;specialPurposes;features;specialFeatures;isReady_=!1;vendors_;vendorIds;fullVendorList;byPurposeVendorMap;bySpecialPurposeVendorMap;byFeatureVendorMap;bySpecialFeatureVendorMap;stacks;dataCategories;lang_;cacheLang_;isLatest=!1;constructor(e,t){super();let i=n.baseUrl,s=t?.language;if(s)try{s=n.consentLanguages.parseLanguage(s)}catch(r){throw new oe("Error during parsing the language: "+r.message)}if(this.lang_=s||n.DEFAULT_LANGUAGE,this.cacheLang_=s||n.DEFAULT_LANGUAGE,this.isVendorList(e))this.populate(e),this.readyPromise=Promise.resolve();else{if(!i)throw new oe("must specify GVL.baseUrl before loading GVL json");if(e>0){let r=e;n.CACHE.has(r)?(this.populate(n.CACHE.get(r)),this.readyPromise=Promise.resolve()):(i+=n.versionedFilename.replace("[VERSION]",String(r)),this.readyPromise=this.fetchJson(i))}else n.CACHE.has(n.LATEST_CACHE_KEY)?(this.populate(n.CACHE.get(n.LATEST_CACHE_KEY)),this.readyPromise=Promise.resolve()):(this.isLatest=!0,this.readyPromise=this.fetchJson(i+n.latestFilename))}}static emptyLanguageCache(e){let t=!1;return e==null&&n.LANGUAGE_CACHE.size>0?(n.LANGUAGE_CACHE=new Map,t=!0):typeof e=="string"&&this.consentLanguages.has(e.toUpperCase())&&(n.LANGUAGE_CACHE.delete(e.toUpperCase()),t=!0),t}static emptyCache(e){let t=!1;return Number.isInteger(e)&&e>=0?(n.CACHE.delete(e),t=!0):e===void 0&&(n.CACHE=new Map,t=!0),t}cacheLanguage(){n.LANGUAGE_CACHE.has(this.cacheLang_)||n.LANGUAGE_CACHE.set(this.cacheLang_,{purposes:this.purposes,specialPurposes:this.specialPurposes,features:this.features,specialFeatures:this.specialFeatures,stacks:this.stacks,dataCategories:this.dataCategories})}async fetchJson(e){try{this.populate(await De.fetch(e))}catch(t){throw new oe(t.message)}}getJson(){return{gvlSpecificationVersion:this.gvlSpecificationVersion,vendorListVersion:this.vendorListVersion,tcfPolicyVersion:this.tcfPolicyVersion,lastUpdated:this.lastUpdated,purposes:this.clonePurposes(),specialPurposes:this.cloneSpecialPurposes(),features:this.cloneFeatures(),specialFeatures:this.cloneSpecialFeatures(),stacks:this.cloneStacks(),...this.dataCategories?{dataCategories:this.cloneDataCategories()}:{},vendors:this.cloneVendors()}}cloneSpecialFeatures(){let e={};for(let t of Object.keys(this.specialFeatures))e[t]=n.cloneFeature(this.specialFeatures[t]);return e}cloneFeatures(){let e={};for(let t of Object.keys(this.features))e[t]=n.cloneFeature(this.features[t]);return e}cloneStacks(){let e={};for(let t of Object.keys(this.stacks))e[t]=n.cloneStack(this.stacks[t]);return e}cloneDataCategories(){let e={};for(let t of Object.keys(this.dataCategories))e[t]=n.cloneDataCategory(this.dataCategories[t]);return e}cloneSpecialPurposes(){let e={};for(let t of Object.keys(this.specialPurposes))e[t]=n.clonePurpose(this.specialPurposes[t]);return e}clonePurposes(){let e={};for(let t of Object.keys(this.purposes))e[t]=n.clonePurpose(this.purposes[t]);return e}static clonePurpose(e){return{id:e.id,name:e.name,description:e.description,...e.descriptionLegal?{descriptionLegal:e.descriptionLegal}:{},...e.illustrations?{illustrations:Array.from(e.illustrations)}:{}}}static cloneFeature(e){return{id:e.id,name:e.name,description:e.description,...e.descriptionLegal?{descriptionLegal:e.descriptionLegal}:{},...e.illustrations?{illustrations:Array.from(e.illustrations)}:{}}}static cloneDataCategory(e){return{id:e.id,name:e.name,description:e.description}}static cloneStack(e){return{id:e.id,name:e.name,description:e.description,purposes:Array.from(e.purposes),specialFeatures:Array.from(e.specialFeatures)}}static cloneDataRetention(e){return{...typeof e.stdRetention=="number"?{stdRetention:e.stdRetention}:{},purposes:{...e.purposes},specialPurposes:{...e.specialPurposes}}}static cloneVendorUrls(e){return e.map(t=>({langId:t.langId,privacy:t.privacy,...t.legIntClaim?{legIntClaim:t.legIntClaim}:{}}))}static cloneVendor(e){return{id:e.id,name:e.name,purposes:Array.from(e.purposes),legIntPurposes:Array.from(e.legIntPurposes),flexiblePurposes:Array.from(e.flexiblePurposes),specialPurposes:Array.from(e.specialPurposes),features:Array.from(e.features),specialFeatures:Array.from(e.specialFeatures),...e.overflow?{overflow:{httpGetLimit:e.overflow.httpGetLimit}}:{},...typeof e.cookieMaxAgeSeconds=="number"||e.cookieMaxAgeSeconds===null?{cookieMaxAgeSeconds:e.cookieMaxAgeSeconds}:{},...e.usesCookies!==void 0?{usesCookies:e.usesCookies}:{},...e.policyUrl?{policyUrl:e.policyUrl}:{},...e.cookieRefresh!==void 0?{cookieRefresh:e.cookieRefresh}:{},...e.usesNonCookieAccess!==void 0?{usesNonCookieAccess:e.usesNonCookieAccess}:{},...e.dataRetention?{dataRetention:this.cloneDataRetention(e.dataRetention)}:{},...e.urls?{urls:this.cloneVendorUrls(e.urls)}:{},...e.dataDeclaration?{dataDeclaration:Array.from(e.dataDeclaration)}:{},...e.deviceStorageDisclosureUrl?{deviceStorageDisclosureUrl:e.deviceStorageDisclosureUrl}:{},...e.deletedDate?{deletedDate:e.deletedDate}:{}}}cloneVendors(){let e={};for(let t of Object.keys(this.fullVendorList))e[t]=n.cloneVendor(this.fullVendorList[t]);return e}async changeLanguage(e){let t=e;try{t=n.consentLanguages.parseLanguage(e)}catch(s){throw new oe("Error during parsing the language: "+s.message)}let i=e.toUpperCase();if(!(t.toLowerCase()===n.DEFAULT_LANGUAGE.toLowerCase()&&!n.LANGUAGE_CACHE.has(i))&&t!==this.lang_)if(this.lang_=t,n.LANGUAGE_CACHE.has(i)){let s=n.LANGUAGE_CACHE.get(i);for(let r in s)s.hasOwnProperty(r)&&(this[r]=s[r])}else{let s=n.baseUrl+n.languageFilename.replace("[LANG]",this.lang_.toLowerCase());try{await this.fetchJson(s),this.cacheLang_=i,this.cacheLanguage()}catch(r){throw new oe("unable to load language: "+r.message)}}}get language(){return this.lang_}isVendorList(e){return e!==void 0&&e.vendors!==void 0}populate(e){this.purposes=e.purposes,this.specialPurposes=e.specialPurposes,this.features=e.features,this.specialFeatures=e.specialFeatures,this.stacks=e.stacks,this.dataCategories=e.dataCategories,this.isVendorList(e)&&(this.gvlSpecificationVersion=e.gvlSpecificationVersion,this.tcfPolicyVersion=e.tcfPolicyVersion,this.vendorListVersion=e.vendorListVersion,this.lastUpdated=e.lastUpdated,typeof this.lastUpdated=="string"&&(this.lastUpdated=new Date(this.lastUpdated)),this.vendors_=e.vendors,this.fullVendorList=e.vendors,this.mapVendors(),this.isReady_=!0,this.isLatest&&n.CACHE.set(n.LATEST_CACHE_KEY,this.getJson()),n.CACHE.has(this.vendorListVersion)||n.CACHE.set(this.vendorListVersion,this.getJson())),this.cacheLanguage()}mapVendors(e){this.byPurposeVendorMap={},this.bySpecialPurposeVendorMap={},this.byFeatureVendorMap={},this.bySpecialFeatureVendorMap={},Object.keys(this.purposes).forEach(t=>{this.byPurposeVendorMap[t]={legInt:new Set,consent:new Set,flexible:new Set}}),Object.keys(this.specialPurposes).forEach(t=>{this.bySpecialPurposeVendorMap[t]=new Set}),Object.keys(this.features).forEach(t=>{this.byFeatureVendorMap[t]=new Set}),Object.keys(this.specialFeatures).forEach(t=>{this.bySpecialFeatureVendorMap[t]=new Set}),Array.isArray(e)||(e=Object.keys(this.fullVendorList).map(t=>+t)),this.vendorIds=new Set(e),this.vendors_=e.reduce((t,i)=>{let s=this.vendors_[String(i)];return s&&s.deletedDate===void 0&&(s.purposes.forEach(r=>{this.byPurposeVendorMap[String(r)].consent.add(i)}),s.specialPurposes.forEach(r=>{this.bySpecialPurposeVendorMap[String(r)].add(i)}),s.legIntPurposes.forEach(r=>{this.byPurposeVendorMap[String(r)].legInt.add(i)}),s.flexiblePurposes&&s.flexiblePurposes.forEach(r=>{this.byPurposeVendorMap[String(r)].flexible.add(i)}),s.features.forEach(r=>{this.byFeatureVendorMap[String(r)].add(i)}),s.specialFeatures.forEach(r=>{this.bySpecialFeatureVendorMap[String(r)].add(i)}),t[i]=s),t},{})}getFilteredVendors(e,t,i,s){let r=e.charAt(0).toUpperCase()+e.slice(1),o,a={};return e==="purpose"&&i?o=this["by"+r+"VendorMap"][String(t)][i]:o=this["by"+(s?"Special":"")+r+"VendorMap"][String(t)],o.forEach(c=>{a[String(c)]=this.vendors[String(c)]}),a}getVendorsWithConsentPurpose(e){return this.getFilteredVendors("purpose",e,"consent")}getVendorsWithLegIntPurpose(e){return this.getFilteredVendors("purpose",e,"legInt")}getVendorsWithFlexiblePurpose(e){return this.getFilteredVendors("purpose",e,"flexible")}getVendorsWithSpecialPurpose(e){return this.getFilteredVendors("purpose",e,void 0,!0)}getVendorsWithFeature(e){return this.getFilteredVendors("feature",e)}getVendorsWithSpecialFeature(e){return this.getFilteredVendors("feature",e,void 0,!0)}get vendors(){return this.vendors_}narrowVendorsTo(e){this.mapVendors(e)}get isReady(){return this.isReady_}clone(){let e=new n(this.getJson());return this.lang_!==n.DEFAULT_LANGUAGE&&e.changeLanguage(this.lang_),e}static isInstanceOf(e){return typeof e=="object"&&typeof e.narrowVendorsTo=="function"}}});var Ne,yt=P(()=>{fe();J();vt();Q();Ne=class extends te{static consentLanguages=ye.consentLanguages;isServiceSpecific_=!0;supportOOB_=!1;useNonStandardTexts_=!1;purposeOneTreatment_=!1;publisherCountryCode_="AA";version_=2;consentScreen_=0;policyVersion_=5;consentLanguage_="EN";cmpId_=0;cmpVersion_=0;vendorListVersion_=0;numCustomPurposes_=0;gvl_;created;lastUpdated;specialFeatureOptins=new H;purposeConsents=new H;purposeLegitimateInterests=new H;publisherConsents=new H;publisherLegitimateInterests=new H;publisherCustomConsents=new H;publisherCustomLegitimateInterests=new H;customPurposes;vendorConsents=new H;vendorLegitimateInterests=new H;vendorsDisclosed=new H;vendorsAllowed=new H;publisherRestrictions=new he;constructor(e){super(),e&&(this.gvl=e),this.updated()}set gvl(e){ye.isInstanceOf(e)||(e=new ye(e)),this.gvl_=e,this.publisherRestrictions.gvl=e}get gvl(){return this.gvl_}set cmpId(e){if(e=Number(e),Number.isInteger(e)&&e>1)this.cmpId_=e;else throw new W("cmpId",e)}get cmpId(){return this.cmpId_}set cmpVersion(e){if(e=Number(e),Number.isInteger(e)&&e>-1)this.cmpVersion_=e;else throw new W("cmpVersion",e)}get cmpVersion(){return this.cmpVersion_}set consentScreen(e){if(e=Number(e),Number.isInteger(e)&&e>-1)this.consentScreen_=e;else throw new W("consentScreen",e)}get consentScreen(){return this.consentScreen_}set consentLanguage(e){this.consentLanguage_=e}get consentLanguage(){return this.consentLanguage_}set publisherCountryCode(e){if(/^([A-z]){2}$/.test(e))this.publisherCountryCode_=e.toUpperCase();else throw new W("publisherCountryCode",e)}get publisherCountryCode(){return this.publisherCountryCode_}set vendorListVersion(e){if(e=Number(e)>>0,e<0)throw new W("vendorListVersion",e);this.vendorListVersion_=e}get vendorListVersion(){return this.gvl?this.gvl.vendorListVersion:this.vendorListVersion_}set policyVersion(e){if(this.policyVersion_=parseInt(e,10),this.policyVersion_<0)throw new W("policyVersion",e)}get policyVersion(){return this.gvl?this.gvl.tcfPolicyVersion:this.policyVersion_}set version(e){this.version_=parseInt(e,10)}get version(){return this.version_}set isServiceSpecific(e){this.isServiceSpecific_=e}get isServiceSpecific(){return this.isServiceSpecific_}set useNonStandardTexts(e){this.useNonStandardTexts_=e}get useNonStandardTexts(){return this.useNonStandardTexts_}set supportOOB(e){this.supportOOB_=e}get supportOOB(){return this.supportOOB_}set purposeOneTreatment(e){this.purposeOneTreatment_=e}get purposeOneTreatment(){return this.purposeOneTreatment_}setAllVendorConsents(){this.vendorConsents.set(this.gvl.vendors)}unsetAllVendorConsents(){this.vendorConsents.empty()}setAllVendorsDisclosed(){this.vendorsDisclosed.set(this.gvl.vendors)}unsetAllVendorsDisclosed(){this.vendorsDisclosed.empty()}setAllVendorsAllowed(){this.vendorsAllowed.set(this.gvl.vendors)}unsetAllVendorsAllowed(){this.vendorsAllowed.empty()}setAllVendorLegitimateInterests(){this.vendorLegitimateInterests.set(this.gvl.vendors)}unsetAllVendorLegitimateInterests(){this.vendorLegitimateInterests.empty()}setAllPurposeConsents(){this.purposeConsents.set(this.gvl.purposes)}unsetAllPurposeConsents(){this.purposeConsents.empty()}setAllPurposeLegitimateInterests(){this.purposeLegitimateInterests.set(this.gvl.purposes)}unsetAllPurposeLegitimateInterests(){this.purposeLegitimateInterests.empty()}setAllSpecialFeatureOptins(){this.specialFeatureOptins.set(this.gvl.specialFeatures)}unsetAllSpecialFeatureOptins(){this.specialFeatureOptins.empty()}setAll(){this.setAllVendorConsents(),this.setAllPurposeLegitimateInterests(),this.setAllSpecialFeatureOptins(),this.setAllPurposeConsents(),this.setAllVendorLegitimateInterests()}unsetAll(){this.unsetAllVendorConsents(),this.unsetAllPurposeLegitimateInterests(),this.unsetAllSpecialFeatureOptins(),this.unsetAllPurposeConsents(),this.unsetAllVendorLegitimateInterests()}get numCustomPurposes(){let e=this.numCustomPurposes_;if(typeof this.customPurposes=="object"){let t=Object.keys(this.customPurposes).sort((i,s)=>Number(i)-Number(s));e=parseInt(t.pop(),10)}return e}set numCustomPurposes(e){if(this.numCustomPurposes_=parseInt(e,10),this.numCustomPurposes_<0)throw new W("numCustomPurposes",e)}updated(){let e=new Date,t=new Date(Date.UTC(e.getUTCFullYear(),e.getUTCMonth(),e.getUTCDate()));this.created=t,this.lastUpdated=t}}});var bt,jn=P(()=>{$e();Q();ge();yt();bt=class{static encode(e,t){let i="",s;return e=Be.process(e,t),Array.isArray(t?.segments)?s=t.segments:s=new Fe(e,t)[""+e.version],s.forEach((r,o)=>{let a="";ope,BitLength:()=>S,BooleanEncoder:()=>$,Cloneable:()=>te,ConsentLanguages:()=>Ee,DateEncoder:()=>Se,DecodingError:()=>G,DeviceDisclosureStorageAccessType:()=>ct,EncodingError:()=>q,FieldEncoderMap:()=>Ke,FieldSequence:()=>xe,Fields:()=>v,FixedVectorEncoder:()=>ne,GVL:()=>ye,GVLError:()=>oe,IntEncoder:()=>L,Json:()=>De,LangEncoder:()=>Ae,PurposeRestriction:()=>ae,PurposeRestrictionVector:()=>he,PurposeRestrictionVectorEncoder:()=>Ve,RestrictionType:()=>Z,Segment:()=>N,SegmentEncoder:()=>ze,SegmentIDs:()=>ke,SegmentSequence:()=>Fe,SemanticPreEncoder:()=>Be,TCModel:()=>Ne,TCModelError:()=>W,TCString:()=>bt,Vector:()=>H,VectorEncodingType:()=>ve,VendorVectorEncoder:()=>ce});var An=P(()=>{$e();J();Q();fe();vt();kt();yt();jn()});var st={};Nt(st,{baseTranslations:()=>Bi,deepMergeTranslations:()=>Rt,detectBrowserLanguage:()=>Gt,enTranslations:()=>it,mergeTranslationConfigs:()=>Ut,parseAcceptLanguage:()=>Mt,prepareTranslationConfig:()=>Ni,selectLanguage:()=>Di});var ii={common:{acceptAll:"\u041F\u0440\u0438\u0435\u043C\u0438 \u0432\u0441\u0438\u0447\u043A\u0438",rejectAll:"\u041E\u0442\u0445\u0432\u044A\u0440\u043B\u0438 \u0432\u0441\u0438\u0447\u043A\u0438",customize:"\u041F\u0435\u0440\u0441\u043E\u043D\u0430\u043B\u0438\u0437\u0438\u0440\u0430\u0439",save:"\u0417\u0430\u043F\u0430\u0437\u0438 \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438\u0442\u0435"},cookieBanner:{title:"\u0426\u0435\u043D\u0438\u043C \u0432\u0430\u0448\u0430\u0442\u0430 \u043F\u043E\u0432\u0435\u0440\u0438\u0442\u0435\u043B\u043D\u043E\u0441\u0442",description:"\u0422\u043E\u0437\u0438 \u0441\u0430\u0439\u0442 \u0438\u0437\u043F\u043E\u043B\u0437\u0432\u0430 \u0431\u0438\u0441\u043A\u0432\u0438\u0442\u043A\u0438, \u0437\u0430 \u0434\u0430 \u043F\u043E\u0434\u043E\u0431\u0440\u0438 \u0432\u0430\u0448\u0435\u0442\u043E \u043F\u043E\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043B\u0441\u043A\u043E \u0438\u0437\u0436\u0438\u0432\u044F\u0432\u0430\u043D\u0435, \u0434\u0430 \u0430\u043D\u0430\u043B\u0438\u0437\u0438\u0440\u0430 \u0442\u0440\u0430\u0444\u0438\u043A\u0430 \u043D\u0430 \u0441\u0430\u0439\u0442\u0430 \u0438 \u0434\u0430 \u043F\u043E\u043A\u0430\u0437\u0432\u0430 \u043F\u0435\u0440\u0441\u043E\u043D\u0430\u043B\u0438\u0437\u0438\u0440\u0430\u043D\u043E \u0441\u044A\u0434\u044A\u0440\u0436\u0430\u043D\u0438\u0435."},consentManagerDialog:{title:"\u041D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438 \u0437\u0430 \u043F\u043E\u0432\u0435\u0440\u0438\u0442\u0435\u043B\u043D\u043E\u0441\u0442",description:"\u041F\u0435\u0440\u0441\u043E\u043D\u0430\u043B\u0438\u0437\u0438\u0440\u0430\u0439\u0442\u0435 \u0432\u0430\u0448\u0438\u0442\u0435 \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438 \u0437\u0430 \u043F\u043E\u0432\u0435\u0440\u0438\u0442\u0435\u043B\u043D\u043E\u0441\u0442 \u0442\u0443\u043A. \u041C\u043E\u0436\u0435\u0442\u0435 \u0434\u0430 \u0438\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u043A\u043E\u0438 \u0432\u0438\u0434\u043E\u0432\u0435 \u0431\u0438\u0441\u043A\u0432\u0438\u0442\u043A\u0438 \u0438 \u0442\u0435\u0445\u043D\u043E\u043B\u043E\u0433\u0438\u0438 \u0437\u0430 \u043F\u0440\u043E\u0441\u043B\u0435\u0434\u044F\u0432\u0430\u043D\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0430\u0432\u0430\u0442\u0435."},consentTypes:{necessary:{title:"\u0421\u0442\u0440\u043E\u0433\u043E \u043D\u0435\u043E\u0431\u0445\u043E\u0434\u0438\u043C\u0438",description:"\u0422\u0435\u0437\u0438 \u0431\u0438\u0441\u043A\u0432\u0438\u0442\u043A\u0438 \u0441\u0430 \u043E\u0442 \u0441\u044A\u0449\u0435\u0441\u0442\u0432\u0435\u043D\u043E \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435 \u0437\u0430 \u043F\u0440\u0430\u0432\u0438\u043B\u043D\u043E\u0442\u043E \u0444\u0443\u043D\u043A\u0446\u0438\u043E\u043D\u0438\u0440\u0430\u043D\u0435 \u043D\u0430 \u0443\u0435\u0431\u0441\u0430\u0439\u0442\u0430 \u0438 \u043D\u0435 \u043C\u043E\u0433\u0430\u0442 \u0434\u0430 \u0431\u044A\u0434\u0430\u0442 \u0434\u0435\u0430\u043A\u0442\u0438\u0432\u0438\u0440\u0430\u043D\u0438."},functionality:{title:"\u0424\u0443\u043D\u043A\u0446\u0438\u043E\u043D\u0430\u043B\u043D\u043E\u0441\u0442",description:"\u0422\u0435\u0437\u0438 \u0431\u0438\u0441\u043A\u0432\u0438\u0442\u043A\u0438 \u043F\u043E\u0437\u0432\u043E\u043B\u044F\u0432\u0430\u0442 \u043F\u043E\u0434\u043E\u0431\u0440\u0435\u043D\u0430 \u0444\u0443\u043D\u043A\u0446\u0438\u043E\u043D\u0430\u043B\u043D\u043E\u0441\u0442 \u0438 \u043F\u0435\u0440\u0441\u043E\u043D\u0430\u043B\u0438\u0437\u0438\u0440\u0430\u043D\u0435 \u043D\u0430 \u0443\u0435\u0431\u0441\u0430\u0439\u0442\u0430."},marketing:{title:"\u041C\u0430\u0440\u043A\u0435\u0442\u0438\u043D\u0433",description:"\u0422\u0435\u0437\u0438 \u0431\u0438\u0441\u043A\u0432\u0438\u0442\u043A\u0438 \u0441\u0435 \u0438\u0437\u043F\u043E\u043B\u0437\u0432\u0430\u0442 \u0437\u0430 \u043F\u043E\u043A\u0430\u0437\u0432\u0430\u043D\u0435 \u043D\u0430 \u043F\u043E\u0434\u0445\u043E\u0434\u044F\u0449\u0438 \u0440\u0435\u043A\u043B\u0430\u043C\u0438 \u0438 \u043F\u0440\u043E\u0441\u043B\u0435\u0434\u044F\u0432\u0430\u043D\u0435 \u043D\u0430 \u0442\u044F\u0445\u043D\u0430\u0442\u0430 \u0435\u0444\u0435\u043A\u0442\u0438\u0432\u043D\u043E\u0441\u0442."},measurement:{title:"\u0410\u043D\u0430\u043B\u0438\u0442\u0438\u043A\u0430",description:"\u0422\u0435\u0437\u0438 \u0431\u0438\u0441\u043A\u0432\u0438\u0442\u043A\u0438 \u043D\u0438 \u043F\u043E\u043C\u0430\u0433\u0430\u0442 \u0434\u0430 \u0440\u0430\u0437\u0431\u0435\u0440\u0435\u043C \u043A\u0430\u043A \u043F\u043E\u0441\u0435\u0442\u0438\u0442\u0435\u043B\u0438\u0442\u0435 \u0432\u0437\u0430\u0438\u043C\u043E\u0434\u0435\u0439\u0441\u0442\u0432\u0430\u0442 \u0441 \u0443\u0435\u0431\u0441\u0430\u0439\u0442\u0430 \u0438 \u0434\u0430 \u043F\u043E\u0434\u043E\u0431\u0440\u0438\u043C \u043D\u0435\u0433\u043E\u0432\u0430\u0442\u0430 \u043F\u0440\u043E\u0438\u0437\u0432\u043E\u0434\u0438\u0442\u0435\u043B\u043D\u043E\u0441\u0442."},experience:{title:"\u041F\u043E\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043B\u0441\u043A\u043E \u0438\u0437\u0436\u0438\u0432\u044F\u0432\u0430\u043D\u0435",description:"\u0422\u0435\u0437\u0438 \u0431\u0438\u0441\u043A\u0432\u0438\u0442\u043A\u0438 \u043D\u0438 \u043F\u043E\u043C\u0430\u0433\u0430\u0442 \u0434\u0430 \u043E\u0441\u0438\u0433\u0443\u0440\u0438\u043C \u043F\u043E-\u0434\u043E\u0431\u0440\u043E \u043F\u043E\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043B\u0441\u043A\u043E \u0438\u0437\u0436\u0438\u0432\u044F\u0432\u0430\u043D\u0435 \u0438 \u0434\u0430 \u0442\u0435\u0441\u0442\u0432\u0430\u043C\u0435 \u043D\u043E\u0432\u0438 \u0444\u0443\u043D\u043A\u0446\u0438\u0438."}},frame:{title:"\u041F\u0440\u0438\u0435\u043C\u0435\u0442\u0435 \u0441\u044A\u0433\u043B\u0430\u0441\u0438\u0435 \u0437\u0430 {category}, \u0437\u0430 \u0434\u0430 \u0432\u0438\u0434\u0438\u0442\u0435 \u0442\u043E\u0432\u0430 \u0441\u044A\u0434\u044A\u0440\u0436\u0430\u043D\u0438\u0435.",actionButton:"\u0410\u043A\u0442\u0438\u0432\u0438\u0440\u0430\u0439\u0442\u0435 \u0441\u044A\u0433\u043B\u0430\u0441\u0438\u0435 \u0437\u0430 {category}"},legalLinks:{privacyPolicy:"\u041F\u043E\u043B\u0438\u0442\u0438\u043A\u0430 \u0437\u0430 \u043F\u043E\u0432\u0435\u0440\u0438\u0442\u0435\u043B\u043D\u043E\u0441\u0442",cookiePolicy:"\u041F\u043E\u043B\u0438\u0442\u0438\u043A\u0430 \u0437\u0430 \u0431\u0438\u0441\u043A\u0432\u0438\u0442\u043A\u0438",termsOfService:"\u041E\u0431\u0449\u0438 \u0443\u0441\u043B\u043E\u0432\u0438\u044F"},iab:{banner:{title:"\u041D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438 \u0437\u0430 \u043F\u043E\u0432\u0435\u0440\u0438\u0442\u0435\u043B\u043D\u043E\u0441\u0442",description:"\u041D\u0438\u0435 \u0438 \u043D\u0430\u0448\u0438\u0442\u0435 {partnerCount} \u043F\u0430\u0440\u0442\u043D\u044C\u043E\u0440\u0438 \u0441\u044A\u0445\u0440\u0430\u043D\u044F\u0432\u0430\u043C\u0435 \u0438/\u0438\u043B\u0438 \u043E\u0441\u044A\u0449\u0435\u0441\u0442\u0432\u044F\u0432\u0430\u043C\u0435 \u0434\u043E\u0441\u0442\u044A\u043F \u0434\u043E \u0438\u043D\u0444\u043E\u0440\u043C\u0430\u0446\u0438\u044F \u043D\u0430 \u0432\u0430\u0448\u0435\u0442\u043E \u0443\u0441\u0442\u0440\u043E\u0439\u0441\u0442\u0432\u043E \u0438 \u043E\u0431\u0440\u0430\u0431\u043E\u0442\u0432\u0430\u043C\u0435 \u043B\u0438\u0447\u043D\u0438 \u0434\u0430\u043D\u043D\u0438, \u043A\u0430\u0442\u043E \u0443\u043D\u0438\u043A\u0430\u043B\u043D\u0438 \u0438\u0434\u0435\u043D\u0442\u0438\u0444\u0438\u043A\u0430\u0442\u043E\u0440\u0438 \u0438 \u0434\u0430\u043D\u043D\u0438 \u0437\u0430 \u0441\u044A\u0440\u0444\u0438\u0440\u0430\u043D\u0435, \u0437\u0430 \u0442\u043E\u0437\u0438 \u0443\u0435\u0431\u0441\u0430\u0439\u0442, \u0437\u0430 \u0434\u0430:",partnersLink:"{count, plural, one {# \u043F\u0430\u0440\u0442\u043D\u044C\u043E\u0440} other {# \u043F\u0430\u0440\u0442\u043D\u044C\u043E\u0440\u0430}}",andMore:"\u0418 \u043E\u0449\u0435 {count, plural, one {# \u043F\u0430\u0440\u0442\u043D\u044C\u043E\u0440} other {# \u043F\u0430\u0440\u0442\u043D\u044C\u043E\u0440\u0430}}...",legitimateInterestNotice:"\u041D\u044F\u043A\u043E\u0438 \u043F\u0430\u0440\u0442\u043D\u044C\u043E\u0440\u0438 \u043F\u0440\u0435\u0442\u0435\u043D\u0434\u0438\u0440\u0430\u0442 \u0437\u0430 \u0437\u0430\u043A\u043E\u043D\u0435\u043D \u0438\u043D\u0442\u0435\u0440\u0435\u0441 \u0434\u0430 \u043E\u0431\u0440\u0430\u0431\u043E\u0442\u0432\u0430\u0442 \u0432\u0430\u0448\u0438\u0442\u0435 \u0434\u0430\u043D\u043D\u0438. \u0418\u043C\u0430\u0442\u0435 \u043F\u0440\u0430\u0432\u043E \u0434\u0430 \u0432\u044A\u0437\u0440\u0430\u0437\u0438\u0442\u0435 \u0441\u0440\u0435\u0449\u0443 \u0442\u0430\u0437\u0438 \u043E\u0431\u0440\u0430\u0431\u043E\u0442\u043A\u0430, \u0434\u0430 \u043F\u0435\u0440\u0441\u043E\u043D\u0430\u043B\u0438\u0437\u0438\u0440\u0430\u0442\u0435 \u0432\u0430\u0448\u0438\u0442\u0435 \u0438\u0437\u0431\u043E\u0440\u0438 \u0438 \u0434\u0430 \u043E\u0442\u0442\u0435\u0433\u043B\u0438\u0442\u0435 \u0441\u044A\u0433\u043B\u0430\u0441\u0438\u0435\u0442\u043E \u0441\u0438 \u043F\u043E \u0432\u0441\u044F\u043A\u043E \u0432\u0440\u0435\u043C\u0435.",scopeServiceSpecific:"\u0412\u0430\u0448\u0435\u0442\u043E \u0441\u044A\u0433\u043B\u0430\u0441\u0438\u0435 \u0432\u0430\u0436\u0438 \u0441\u0430\u043C\u043E \u0437\u0430 \u0442\u043E\u0437\u0438 \u0443\u0435\u0431\u0441\u0430\u0439\u0442 \u0438 \u043D\u044F\u043C\u0430 \u0434\u0430 \u043F\u043E\u0432\u043B\u0438\u044F\u0435 \u043D\u0430 \u0434\u0440\u0443\u0433\u0438 \u0443\u0441\u043B\u0443\u0433\u0438.",scopeGroup:"\u0412\u0430\u0448\u0438\u044F\u0442 \u0438\u0437\u0431\u043E\u0440 \u0441\u0435 \u043F\u0440\u0438\u043B\u0430\u0433\u0430 \u043A\u044A\u043C \u0432\u0441\u0438\u0447\u043A\u0438 \u043D\u0430\u0448\u0438 \u0443\u0435\u0431\u0441\u0430\u0439\u0442\u043E\u0432\u0435 \u0432 \u0442\u0430\u0437\u0438 \u0433\u0440\u0443\u043F\u0430."},preferenceCenter:{title:"\u041D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438 \u0437\u0430 \u043F\u043E\u0432\u0435\u0440\u0438\u0442\u0435\u043B\u043D\u043E\u0441\u0442",description:"\u041F\u0435\u0440\u0441\u043E\u043D\u0430\u043B\u0438\u0437\u0438\u0440\u0430\u0439\u0442\u0435 \u0432\u0430\u0448\u0438\u0442\u0435 \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438 \u0437\u0430 \u043F\u043E\u0432\u0435\u0440\u0438\u0442\u0435\u043B\u043D\u043E\u0441\u0442 \u0442\u0443\u043A. \u041C\u043E\u0436\u0435\u0442\u0435 \u0434\u0430 \u0438\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u043A\u043E\u0438 \u0432\u0438\u0434\u043E\u0432\u0435 \u0431\u0438\u0441\u043A\u0432\u0438\u0442\u043A\u0438 \u0438 \u0442\u0435\u0445\u043D\u043E\u043B\u043E\u0433\u0438\u0438 \u0437\u0430 \u043F\u0440\u043E\u0441\u043B\u0435\u0434\u044F\u0432\u0430\u043D\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0430\u0432\u0430\u0442\u0435.",tabs:{purposes:"\u0426\u0435\u043B\u0438",vendors:"\u0414\u043E\u0441\u0442\u0430\u0432\u0447\u0438\u0446\u0438"},purposeItem:{partners:"{count, plural, one {# \u043F\u0430\u0440\u0442\u043D\u044C\u043E\u0440} other {# \u043F\u0430\u0440\u0442\u043D\u044C\u043E\u0440\u0430}}",vendorsUseLegitimateInterest:"{count, plural, one {# \u0434\u043E\u0441\u0442\u0430\u0432\u0447\u0438\u043A \u043F\u0440\u0435\u0442\u0435\u043D\u0434\u0438\u0440\u0430} other {# \u0434\u043E\u0441\u0442\u0430\u0432\u0447\u0438\u043A\u0430 \u043F\u0440\u0435\u0442\u0435\u043D\u0434\u0438\u0440\u0430\u0442}} \u0437\u0430 \u0437\u0430\u043A\u043E\u043D\u0435\u043D \u0438\u043D\u0442\u0435\u0440\u0435\u0441",examples:"\u041F\u0440\u0438\u043C\u0435\u0440\u0438",partnersUsingPurpose:"\u041F\u0430\u0440\u0442\u043D\u044C\u043E\u0440\u0438, \u0438\u0437\u043F\u043E\u043B\u0437\u0432\u0430\u0449\u0438 \u0442\u0430\u0437\u0438 \u0446\u0435\u043B",withYourPermission:"\u0421 \u0432\u0430\u0448\u0435\u0442\u043E \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043D\u0438\u0435",legitimateInterest:"\u0417\u0430\u043A\u043E\u043D\u0435\u043D \u0438\u043D\u0442\u0435\u0440\u0435\u0441",objectButton:"\u0412\u044A\u0437\u0440\u0430\u0437\u044F\u0432\u0430\u043C",objected:"\u0412\u044A\u0437\u0440\u0430\u0437\u0435\u043D\u043E",rightToObject:"\u0418\u043C\u0430\u0442\u0435 \u043F\u0440\u0430\u0432\u043E \u0434\u0430 \u0432\u044A\u0437\u0440\u0430\u0437\u0438\u0442\u0435 \u0441\u0440\u0435\u0449\u0443 \u043E\u0431\u0440\u0430\u0431\u043E\u0442\u043A\u0430, \u0431\u0430\u0437\u0438\u0440\u0430\u043D\u0430 \u043D\u0430 \u0437\u0430\u043A\u043E\u043D\u0435\u043D \u0438\u043D\u0442\u0435\u0440\u0435\u0441."},specialPurposes:{title:"\u041E\u0441\u043D\u043E\u0432\u043D\u0438 \u0444\u0443\u043D\u043A\u0446\u0438\u0438 (\u0437\u0430\u0434\u044A\u043B\u0436\u0438\u0442\u0435\u043B\u043D\u0438)",tooltip:"\u0422\u0435 \u0441\u0430 \u043D\u0435\u043E\u0431\u0445\u043E\u0434\u0438\u043C\u0438 \u0437\u0430 \u0444\u0443\u043D\u043A\u0446\u0438\u043E\u043D\u0430\u043B\u043D\u043E\u0441\u0442\u0442\u0430 \u0438 \u0441\u0438\u0433\u0443\u0440\u043D\u043E\u0441\u0442\u0442\u0430 \u043D\u0430 \u0441\u0430\u0439\u0442\u0430. \u0421\u044A\u0433\u043B\u0430\u0441\u043D\u043E IAB TCF \u043D\u0435 \u043C\u043E\u0436\u0435\u0442\u0435 \u0434\u0430 \u0432\u044A\u0437\u0440\u0430\u0437\u0438\u0442\u0435 \u0441\u0440\u0435\u0449\u0443 \u0442\u0435\u0437\u0438 \u0441\u043F\u0435\u0446\u0438\u0430\u043B\u043D\u0438 \u0446\u0435\u043B\u0438."},vendorList:{search:"\u0422\u044A\u0440\u0441\u0435\u043D\u0435 \u043D\u0430 \u0434\u043E\u0441\u0442\u0430\u0432\u0447\u0438\u0446\u0438...",showingCount:"{filtered} \u043E\u0442 {total, plural, one {# \u0434\u043E\u0441\u0442\u0430\u0432\u0447\u0438\u043A} other {# \u0434\u043E\u0441\u0442\u0430\u0432\u0447\u0438\u043A\u0430}}",iabVendorsHeading:"\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0430\u043D\u0438 \u0434\u043E\u0441\u0442\u0430\u0432\u0447\u0438\u0446\u0438 \u0432 IAB",iabVendorsNotice:"\u0422\u0435\u0437\u0438 \u043F\u0430\u0440\u0442\u043D\u044C\u043E\u0440\u0438 \u0441\u0430 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0430\u043D\u0438 \u0432 IAB Transparency & Consent Framework (TCF), \u0438\u043D\u0434\u0443\u0441\u0442\u0440\u0438\u0430\u043B\u0435\u043D \u0441\u0442\u0430\u043D\u0434\u0430\u0440\u0442 \u0437\u0430 \u0443\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0438\u0435 \u043D\u0430 \u0441\u044A\u0433\u043B\u0430\u0441\u0438\u0435\u0442\u043E",customVendorsHeading:"\u041F\u0435\u0440\u0441\u043E\u043D\u0430\u043B\u0438\u0437\u0438\u0440\u0430\u043D\u0438 \u043F\u0430\u0440\u0442\u043D\u044C\u043E\u0440\u0438",customVendorsNotice:"\u0422\u043E\u0432\u0430 \u0441\u0430 \u043F\u0435\u0440\u0441\u043E\u043D\u0430\u043B\u0438\u0437\u0438\u0440\u0430\u043D\u0438 \u043F\u0430\u0440\u0442\u043D\u044C\u043E\u0440\u0438, \u043A\u043E\u0438\u0442\u043E \u043D\u0435 \u0441\u0430 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0430\u043D\u0438 \u0432 IAB Transparency & Consent Framework (TCF). \u0422\u0435 \u043E\u0431\u0440\u0430\u0431\u043E\u0442\u0432\u0430\u0442 \u0434\u0430\u043D\u043D\u0438 \u0432\u044A\u0437 \u043E\u0441\u043D\u043E\u0432\u0430 \u043D\u0430 \u0432\u0430\u0448\u0435\u0442\u043E \u0441\u044A\u0433\u043B\u0430\u0441\u0438\u0435 \u0438 \u043C\u043E\u0436\u0435 \u0434\u0430 \u0438\u043C\u0430\u0442 \u0440\u0430\u0437\u043B\u0438\u0447\u043D\u0438 \u043F\u0440\u0430\u043A\u0442\u0438\u043A\u0438 \u0437\u0430 \u043F\u043E\u0432\u0435\u0440\u0438\u0442\u0435\u043B\u043D\u043E\u0441\u0442 \u043E\u0442 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0430\u043D\u0438\u0442\u0435 \u0432 IAB \u0434\u043E\u0441\u0442\u0430\u0432\u0447\u0438\u0446\u0438.",purposes:"\u0426\u0435\u043B\u0438",specialPurposes:"\u0421\u043F\u0435\u0446\u0438\u0430\u043B\u043D\u0438 \u0446\u0435\u043B\u0438",specialFeatures:"\u0421\u043F\u0435\u0446\u0438\u0430\u043B\u043D\u0438 \u0444\u0443\u043D\u043A\u0446\u0438\u0438",features:"\u0424\u0443\u043D\u043A\u0446\u0438\u0438",dataCategories:"\u041A\u0430\u0442\u0435\u0433\u043E\u0440\u0438\u0438 \u0434\u0430\u043D\u043D\u0438",usesCookies:"\u0418\u0437\u043F\u043E\u043B\u0437\u0432\u0430 \u0431\u0438\u0441\u043A\u0432\u0438\u0442\u043A\u0438",nonCookieAccess:"\u0414\u043E\u0441\u0442\u044A\u043F \u0431\u0435\u0437 \u0431\u0438\u0441\u043A\u0432\u0438\u0442\u043A\u0438",maxAge:"\u041C\u0430\u043A\u0441\u0438\u043C\u0430\u043B\u043D\u0430 \u0434\u0430\u0432\u043D\u043E\u0441\u0442: {days} \u0434",retention:"\u0421\u044A\u0445\u0440\u0430\u043D\u0435\u043D\u0438\u0435: {days} \u0434",legitimateInterest:"\u0417\u0430\u043A\u043E\u043D\u0435\u043D \u0438\u043D\u0442\u0435\u0440\u0435\u0441",privacyPolicy:"\u041F\u043E\u043B\u0438\u0442\u0438\u043A\u0430 \u0437\u0430 \u043F\u043E\u0432\u0435\u0440\u0438\u0442\u0435\u043B\u043D\u043E\u0441\u0442",storageDisclosure:"\u0414\u0435\u043A\u043B\u0430\u0440\u0430\u0446\u0438\u044F \u0437\u0430 \u0441\u044A\u0445\u0440\u0430\u043D\u0435\u043D\u0438\u0435",requiredNotice:"\u041D\u0435\u043E\u0431\u0445\u043E\u0434\u0438\u043C\u043E \u0437\u0430 \u0444\u0443\u043D\u043A\u0446\u0438\u043E\u043D\u0430\u043B\u043D\u043E\u0441\u0442\u0442\u0430 \u043D\u0430 \u0441\u0430\u0439\u0442\u0430, \u043D\u0435 \u043C\u043E\u0436\u0435 \u0434\u0430 \u0431\u044A\u0434\u0435 \u0434\u0435\u0430\u043A\u0442\u0438\u0432\u0438\u0440\u0430\u043D\u043E"},footer:{consentStorage:'\u041F\u0440\u0435\u0434\u043F\u043E\u0447\u0438\u0442\u0430\u043D\u0438\u044F\u0442\u0430 \u0437\u0430 \u0441\u044A\u0433\u043B\u0430\u0441\u0438\u0435 \u0441\u0435 \u0441\u044A\u0445\u0440\u0430\u043D\u044F\u0432\u0430\u0442 \u0432 \u0431\u0438\u0441\u043A\u0432\u0438\u0442\u043A\u0430 \u0441 \u0438\u043C\u0435 "euconsent-v2" \u0437\u0430 13 \u043C\u0435\u0441\u0435\u0446\u0430. The storage duration may be refreshed when you update your preferences.'}},common:{acceptAll:"\u041F\u0440\u0438\u0435\u043C\u0438 \u0432\u0441\u0438\u0447\u043A\u0438",rejectAll:"\u041E\u0442\u0445\u0432\u044A\u0440\u043B\u0438 \u0432\u0441\u0438\u0447\u043A\u0438",customize:"\u041F\u0435\u0440\u0441\u043E\u043D\u0430\u043B\u0438\u0437\u0438\u0440\u0430\u0439",saveSettings:"\u0417\u0430\u043F\u0430\u0437\u0438 \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438\u0442\u0435",loading:"\u0417\u0430\u0440\u0435\u0436\u0434\u0430\u043D\u0435...",showingSelectedVendor:"\u041F\u043E\u043A\u0430\u0437\u0432\u0430\u043D\u0435 \u043D\u0430 \u0438\u0437\u0431\u0440\u0430\u043D \u0434\u043E\u0441\u0442\u0430\u0432\u0447\u0438\u043A",clearSelection:"\u0418\u0437\u0447\u0438\u0441\u0442\u0438",customPartner:"\u041F\u0435\u0440\u0441\u043E\u043D\u0430\u043B\u0438\u0437\u0438\u0440\u0430\u043D \u043F\u0430\u0440\u0442\u043D\u044C\u043E\u0440, \u043D\u0435\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0430\u043D \u0432 IAB"}}},si={common:{acceptAll:"P\u0159ijmout v\u0161e",rejectAll:"Odm\xEDtnout v\u0161e",customize:"P\u0159izp\u016Fsobit",save:"Ulo\u017Eit nastaven\xED"},cookieBanner:{title:"V\xE1\u017E\xEDme si va\u0161eho soukrom\xED",description:"Tento web pou\u017E\xEDv\xE1 soubory cookie ke zlep\u0161en\xED va\u0161eho prohl\xED\u017Een\xED, anal\xFDze provozu na webu a zobrazov\xE1n\xED personalizovan\xE9ho obsahu."},consentManagerDialog:{title:"Nastaven\xED soukrom\xED",description:"Zde si m\u016F\u017Eete p\u0159izp\u016Fsobit nastaven\xED soukrom\xED. M\u016F\u017Eete zvolit, kter\xE9 typy soubor\u016F cookie a sledovac\xEDch technologi\xED povol\xEDte."},consentTypes:{necessary:{title:"Nezbytn\u011B nutn\xE9",description:"Tyto soubory cookie jsou nezbytn\xE9 pro spr\xE1vn\xE9 fungov\xE1n\xED webov\xFDch str\xE1nek a nelze je deaktivovat."},functionality:{title:"Funk\u010Dnost",description:"Tyto soubory cookie umo\u017E\u0148uj\xED roz\u0161\xED\u0159enou funk\u010Dnost a personalizaci webov\xFDch str\xE1nek."},marketing:{title:"Marketing",description:"Tyto soubory cookie se pou\u017E\xEDvaj\xED k doru\u010Dov\xE1n\xED relevantn\xEDch reklam a sledov\xE1n\xED jejich \xFA\u010Dinnosti."},measurement:{title:"Analytika",description:"Tyto soubory cookie n\xE1m pom\xE1haj\xED pochopit, jak n\xE1v\u0161t\u011Bvn\xEDci interaguj\xED s webem a zlep\u0161uj\xED jeho v\xFDkon."},experience:{title:"U\u017Eivatelsk\xE1 zku\u0161enost",description:"Tyto soubory cookie n\xE1m pom\xE1haj\xED poskytovat lep\u0161\xED u\u017Eivatelskou zku\u0161enost a testovat nov\xE9 funkce."}},frame:{title:"Pro zobrazen\xED tohoto obsahu p\u0159ijm\u011Bte souhlas s kategori\xED {category}.",actionButton:"Povolit souhlas s kategori\xED {category}"},legalLinks:{privacyPolicy:"Z\xE1sady ochrany osobn\xEDch \xFAdaj\u016F",cookiePolicy:"Z\xE1sady pou\u017E\xEDv\xE1n\xED soubor\u016F cookie",termsOfService:"Podm\xEDnky slu\u017Eby"},iab:{banner:{title:"Nastaven\xED soukrom\xED",description:"My a na\u0161ich {partnerCount} partner\u016F ukl\xE1d\xE1me a/nebo p\u0159istupujeme k informac\xEDm na va\u0161em za\u0159\xEDzen\xED a zpracov\xE1v\xE1me osobn\xED \xFAdaje, jako jsou jedine\u010Dn\xE9 identifik\xE1tory a \xFAdaje o prohl\xED\u017Een\xED, pro tento web za \xFA\u010Delem:",partnersLink:"{count, plural, one {# partner} few {# partne\u0159i} other {# partner\u016F}}",andMore:"A dal\u0161\xEDch {count}...",legitimateInterestNotice:"N\u011Bkte\u0159\xED partne\u0159i uplat\u0148uj\xED opr\xE1vn\u011Bn\xFD z\xE1jem na zpracov\xE1n\xED va\u0161ich \xFAdaj\u016F. M\xE1te pr\xE1vo proti tomuto zpracov\xE1n\xED vzn\xE9st n\xE1mitku, p\u0159izp\u016Fsobit sv\xE9 volby a kdykoli odvolat sv\u016Fj souhlas.",scopeServiceSpecific:"V\xE1\u0161 souhlas plat\xED pouze pro tento web a neovlivn\xED jin\xE9 slu\u017Eby.",scopeGroup:"Va\u0161e volba plat\xED pro v\u0161echny na\u0161e weby v t\xE9to skupin\u011B."},preferenceCenter:{title:"Nastaven\xED soukrom\xED",description:"Zde si m\u016F\u017Eete p\u0159izp\u016Fsobit nastaven\xED soukrom\xED. M\u016F\u017Eete zvolit, kter\xE9 typy soubor\u016F cookie a sledovac\xEDch technologi\xED povol\xEDte.",tabs:{purposes:"\xDA\u010Dely",vendors:"Partne\u0159i"},purposeItem:{partners:"{count, plural, one {# partner} few {# partne\u0159i} other {# partner\u016F}}",vendorsUseLegitimateInterest:"{count, plural, one {# partner uplat\u0148uje} few {# partne\u0159i uplat\u0148uj\xED} other {# partner\u016F uplat\u0148uje}} opr\xE1vn\u011Bn\xFD z\xE1jem",examples:"P\u0159\xEDklady",partnersUsingPurpose:"Partne\u0159i vyu\u017E\xEDvaj\xEDc\xED tento \xFA\u010Del",withYourPermission:"S va\u0161\xEDm svolen\xEDm",legitimateInterest:"Opr\xE1vn\u011Bn\xFD z\xE1jem",objectButton:"Vzn\xE9st n\xE1mitku",objected:"N\xE1mitka vznesena",rightToObject:"M\xE1te pr\xE1vo vzn\xE9st n\xE1mitku proti zpracov\xE1n\xED zalo\u017Een\xE9mu na opr\xE1vn\u011Bn\xE9m z\xE1jmu."},specialPurposes:{title:"Z\xE1kladn\xED funkce (povinn\xE9)",tooltip:"Tyto funkce jsou nezbytn\xE9 pro funk\u010Dnost a zabezpe\u010Den\xED webu. Podle IAB TCF nem\u016F\u017Eete proti t\u011Bmto zvl\xE1\u0161tn\xEDm \xFA\u010Del\u016Fm vzn\xE9st n\xE1mitku."},vendorList:{search:"Hledat partnery...",showingCount:"{filtered} z {total, plural, one {# partnera} few {# partner\u016F} other {# partner\u016F}}",iabVendorsHeading:"Partne\u0159i registrovan\xED v IAB",iabVendorsNotice:"Tito partne\u0159i jsou registrov\xE1ni v r\xE1mci IAB Transparency & Consent Framework (TCF), co\u017E je pr\u016Fmyslov\xFD standard pro spr\xE1vu souhlasu",customVendorsHeading:"Vlastn\xED partne\u0159i",customVendorsNotice:"Toto jsou vlastn\xED partne\u0159i, kte\u0159\xED nejsou registrov\xE1ni v r\xE1mci IAB Transparency & Consent Framework (TCF). Zpracov\xE1vaj\xED data na z\xE1klad\u011B va\u0161eho souhlasu a mohou m\xEDt odli\u0161n\xE9 postupy ochrany osobn\xEDch \xFAdaj\u016F ne\u017E partne\u0159i registrovan\xED v IAB.",purposes:"\xDA\u010Dely",specialPurposes:"Zvl\xE1\u0161tn\xED \xFA\u010Dely",specialFeatures:"Zvl\xE1\u0161tn\xED funkce",features:"Funkce",dataCategories:"Kategorie dat",usesCookies:"Pou\u017E\xEDv\xE1 cookies",nonCookieAccess:"P\u0159\xEDstup bez cookies",maxAge:"Maxim\xE1ln\xED doba: {days} d",retention:"Uchov\xE1v\xE1n\xED: {days} d",legitimateInterest:"Opr\xE1vn\u011Bn\xFD z\xE1jem",privacyPolicy:"Z\xE1sady ochrany osobn\xEDch \xFAdaj\u016F",storageDisclosure:"Informace o ukl\xE1d\xE1n\xED",requiredNotice:"Vy\u017Eadov\xE1no pro funk\u010Dnost webu, nelze zak\xE1zat"},footer:{consentStorage:'P\u0159edvolby souhlasu jsou ulo\u017Eeny v cookie s n\xE1zvem "euconsent-v2" po dobu 13 m\u011Bs\xEDc\u016F. The storage duration may be refreshed when you update your preferences.'}},common:{acceptAll:"P\u0159ijmout v\u0161e",rejectAll:"Odm\xEDtnout v\u0161e",customize:"P\u0159izp\u016Fsobit",saveSettings:"Ulo\u017Eit nastaven\xED",loading:"Na\u010D\xEDt\xE1n\xED...",showingSelectedVendor:"Zobrazen\xED vybran\xE9ho partnera",clearSelection:"Vymazat",customPartner:"Vlastn\xED partner neregistrovan\xFD v IAB"}}},ri={common:{acceptAll:"Derbyn pob un",rejectAll:"Gwrthod pob un",customize:"Addasu",save:"Cadw gosodiadau"},cookieBanner:{title:"Rydym yn gwerthfawrogi eich preifatrwydd",description:"Mae'r wefan hon yn defnyddio cwcis i wella eich profiad pori, dadansoddi traffig y wefan, a dangos cynnwys wedi'i bersonoli."},consentManagerDialog:{title:"Gosodiadau preifatrwydd",description:"Addaswch eich gosodiadau preifatrwydd yma. Gallwch ddewis pa fathau o gwcis a thechnolegau tracio rydych yn eu caniat\xE1u."},consentTypes:{necessary:{title:"Cwbl angenrheidiol",description:"Mae'r cwcis hyn yn hanfodol i'r wefan weithredu'n iawn ac ni ellir eu hanalluogi."},functionality:{title:"Swyddogaeth",description:"Mae'r cwcis hyn yn galluogi swyddogaeth a phersonoli gwell o'r wefan."},marketing:{title:"Marchnata",description:"Defnyddir y cwcis hyn i ddarparu hysbysebion perthnasol a thracio eu heffeithiolrwydd."},measurement:{title:"Dadansoddeg",description:"Mae'r cwcis hyn yn ein helpu i ddeall sut mae ymwelwyr yn rhyngweithio \xE2'r wefan a gwella ei pherfformiad."},experience:{title:"Profiad",description:"Mae'r cwcis hyn yn ein helpu i ddarparu profiad defnyddiwr gwell a phrofi nodweddion newydd."}},frame:{title:"Derbyn caniat\xE2d {category} i weld y cynnwys hwn.",actionButton:"Galluogi caniat\xE2d {category}"},legalLinks:{privacyPolicy:"Polisi preifatrwydd",cookiePolicy:"Polisi cwcis",termsOfService:"Telerau gwasanaeth"},iab:{banner:{title:"Gosodiadau preifatrwydd",description:"Rydym ni a\u2019n {partnerCount} partner yn storio a/neu\u2019n cyrchu gwybodaeth ar eich dyfais ac yn prosesu data personol, megis dynodwyr unigryw a data pori, ar gyfer y wefan hon, er mwyn:",partnersLink:"{count} partner",andMore:"Ac {count} arall...",legitimateInterestNotice:"Mae rhai partneriaid yn hawlio buddiant cyfreithlon i brosesu eich data. Mae gennych hawl i wrthwynebu\u2019r prosesu hwn, addasu eich dewisiadau, a thynnu eich cydsyniad yn \xF4l unrhyw bryd.",scopeServiceSpecific:"Mae eich caniat\xE2d yn berthnasol i\u2019r wefan hon yn unig ac ni fydd yn effeithio ar wasanaethau eraill.",scopeGroup:"Mae eich dewis yn berthnasol ar draws ein gwefannau yn y gr\u0175p hwn."},preferenceCenter:{title:"Gosodiadau preifatrwydd",description:"Addaswch eich gosodiadau preifatrwydd yma. Gallwch ddewis pa fathau o gwcis a thechnolegau tracio rydych yn eu caniat\xE1u.",tabs:{purposes:"Dibenion",vendors:"Gwerthwyr"},purposeItem:{partners:"{count} partner",vendorsUseLegitimateInterest:"{count} gwerthwr yn hawlio buddiant cyfreithlon",examples:"Enghreifftiau",partnersUsingPurpose:"Partneriaid sy\u2019n Defnyddio\u2019r Diben Hwn",withYourPermission:"Gyda\u2019ch Caniat\xE2d",legitimateInterest:"Buddiant Cyfreithlon",objectButton:"Gwrthwynebu",objected:"Gwrthwynebwyd",rightToObject:"Mae gennych hawl i wrthwynebu prosesu sy\u2019n seiliedig ar fuddiant cyfreithlon."},specialPurposes:{title:"Swyddogaethau Hanfodol (Angenrheidiol)",tooltip:"Mae\u2019r rhain yn angenrheidiol ar gyfer swyddogaethau a diogelwch y wefan. Yn unol ag IAB TCF, ni allwch wrthwynebu\u2019r dibenion arbennig hyn."},vendorList:{search:"Chwilio gwerthwyr...",showingCount:"{filtered} o {total} gwerthwr",iabVendorsHeading:"Gwerthwyr Cofrestredig IAB",iabVendorsNotice:"Mae\u2019r partneriaid hyn wedi\u2019u cofrestru gyda Fframwaith Tryloywder a Chydsyniad (TCF) yr IAB, safon diwydiant ar gyfer rheoli cydsyniad",customVendorsHeading:"Partneriaid Personol",customVendorsNotice:"Partneriaid personol yw\u2019r rhain nad ydynt wedi\u2019u cofrestru gyda Fframwaith Tryloywder a Chydsyniad (TCF) yr IAB. Maent yn prosesu data yn seiliedig ar eich cydsyniad ac fe allant fod ag arferion preifatrwydd gwahanol i werthwyr cofrestredig IAB.",purposes:"Dibenion",specialPurposes:"Dibenion Arbennig",specialFeatures:"Nodweddion Arbennig",features:"Nodweddion",dataCategories:"Categor\xEFau Data",usesCookies:"Yn Defnyddio Cwcis",nonCookieAccess:"Mynediad Heb Gwcis",maxAge:"Oed Uchaf: {days}d",retention:"Cadw: {days}d",legitimateInterest:"Buddiant Cyf.",privacyPolicy:"Polisi Preifatrwydd",storageDisclosure:"Datgelu Storio",requiredNotice:"Angenrheidiol ar gyfer swyddogaeth y wefan, ni ellir ei analluogi"},footer:{consentStorage:'Mae dewisiadau cydsyniad yn cael eu storio mewn cwci o\u2019r enw "euconsent-v2" am 13 mis. The storage duration may be refreshed when you update your preferences.'}},common:{acceptAll:"Derbyn pob un",rejectAll:"Gwrthod pob un",customize:"Addasu",saveSettings:"Cadw gosodiadau",loading:"Wrthi\u2019n llwytho...",showingSelectedVendor:"Yn dangos y gwerthwr a ddewiswyd",clearSelection:"Clirio",customPartner:"Partner personol heb ei gofrestru gyda\u2019r IAB"}}},oi={common:{acceptAll:"Accepter alle",rejectAll:"Afvis alle",customize:"Tilpas",save:"Gem indstillinger"},cookieBanner:{title:"Vi v\xE6rds\xE6tter dit privatliv",description:"Denne side bruger cookies til at forbedre din browsingoplevelse, analysere trafikken p\xE5 siden og vise personligt tilpasset indhold."},consentManagerDialog:{title:"Privatlivsindstillinger",description:"Tilpas dine privatlivsindstillinger her. Du kan v\xE6lge, hvilke typer cookies og sporingsteknologier du vil tillade."},consentTypes:{necessary:{title:"Strengt n\xF8dvendige",description:"Disse cookies er essentielle for, at hjemmesiden fungerer korrekt, og de kan ikke deaktiveres."},functionality:{title:"Funktionalitet",description:"Disse cookies muligg\xF8r forbedret funktionalitet og personalisering af hjemmesiden."},marketing:{title:"Markedsf\xF8ring",description:"Disse cookies bruges til at levere relevante annoncer og spore deres effektivitet."},measurement:{title:"Analyse",description:"Disse cookies hj\xE6lper os med at forst\xE5, hvordan bes\xF8gende interagerer med hjemmesiden og forbedre dens ydeevne."},experience:{title:"Oplevelse",description:"Disse cookies hj\xE6lper os med at levere en bedre brugeroplevelse og teste nye funktioner."}},frame:{title:"Accepter {category}-samtykke for at se dette indhold.",actionButton:"Aktiv\xE9r {category}-samtykke"},legalLinks:{privacyPolicy:"Privatlivspolitik",cookiePolicy:"Cookiepolitik",termsOfService:"Servicevilk\xE5r"},iab:{banner:{title:"Privatlivsindstillinger",description:"Vi og vores {partnerCount} partnere gemmer og/eller f\xE5r adgang til oplysninger p\xE5 din enhed og behandler personoplysninger, s\xE5som unikke id'er og browserdata, for dette website, for at:",partnersLink:"{count} partnere",andMore:"Og {count} mere...",legitimateInterestNotice:"Nogle partnere p\xE5ber\xE5ber sig legitim interesse for at behandle dine data. Du har ret til at g\xF8re indsigelse mod denne behandling, tilpasse dine valg og tr\xE6kke dit samtykke tilbage til enhver tid.",scopeServiceSpecific:"Dit samtykke g\xE6lder kun for dette websted og vil ikke p\xE5virke andre tjenester.",scopeGroup:"Dit valg g\xE6lder p\xE5 tv\xE6rs af vores websteder i denne gruppe."},preferenceCenter:{title:"Privatlivsindstillinger",description:"Tilpas dine privatlivsindstillinger her. Du kan v\xE6lge, hvilke typer cookies og sporingsteknologier du vil tillade.",tabs:{purposes:"Form\xE5l",vendors:"Leverand\xF8rer"},purposeItem:{partners:"{count} partnere",vendorsUseLegitimateInterest:"{count} leverand\xF8rer p\xE5ber\xE5ber sig legitim interesse",examples:"Eksempler",partnersUsingPurpose:"Partnere, der bruger dette form\xE5l",withYourPermission:"Med dit samtykke",legitimateInterest:"Legitim interesse",objectButton:"G\xF8r indsigelse",objected:"Indsigelse gjort",rightToObject:"Du har ret til at g\xF8re indsigelse mod behandling baseret p\xE5 legitim interesse."},specialPurposes:{title:"N\xF8dvendige funktioner (p\xE5kr\xE6vet)",tooltip:"Disse er n\xF8dvendige for sidens funktionalitet og sikkerhed. If\xF8lge IAB TCF kan du ikke g\xF8re indsigelse mod disse s\xE6rlige form\xE5l."},vendorList:{search:"S\xF8g leverand\xF8rer...",showingCount:"Viser {filtered} af {total} leverand\xF8rer",iabVendorsHeading:"IAB-registrerede leverand\xF8rer",iabVendorsNotice:"Disse partnere er registreret hos IAB Transparency & Consent Framework (TCF), en branchestandard for h\xE5ndtering af samtykke",customVendorsHeading:"Brugerdefinerede partnere",customVendorsNotice:"Disse er tilpassede partnere, som ikke er registreret hos IAB Transparency & Consent Framework (TCF). De behandler data baseret p\xE5 dit samtykke og kan have andre privatlivspraksisser end IAB-registrerede leverand\xF8rer.",purposes:"Form\xE5l",specialPurposes:"S\xE6rlige form\xE5l",specialFeatures:"S\xE6rlige funktioner",features:"Funktioner",dataCategories:"Datakategorier",usesCookies:"Bruger cookies",nonCookieAccess:"Adgang uden cookies",maxAge:"Maks. alder: {days}d",retention:"Opbevaring: {days}d",legitimateInterest:"Legitim interesse",privacyPolicy:"Privatlivspolitik",storageDisclosure:"Oplysning om lagring",requiredNotice:"P\xE5kr\xE6vet for sidens funktionalitet, kan ikke deaktiveres"},footer:{consentStorage:'Samtykkepr\xE6ferencer gemmes i en cookie med navnet "euconsent-v2" i 13 m\xE5neder. The storage duration may be refreshed when you update your preferences.'}},common:{acceptAll:"Accepter alle",rejectAll:"Afvis alle",customize:"Tilpas",saveSettings:"Gem indstillinger",loading:"Indl\xE6ser...",showingSelectedVendor:"Viser valgt leverand\xF8r",clearSelection:"Ryd",customPartner:"Tilpasset partner, ikke registreret hos IAB"}}},ai={common:{acceptAll:"Alle akzeptieren",rejectAll:"Alle ablehnen",customize:"Anpassen",save:"Einstellungen speichern"},cookieBanner:{title:"Wir respektieren Deine Privatsph\xE4re.",description:"Diese Website verwendet Cookies, um Deine Surf-Erfahrung zu verbessern, den Seitenverkehr zu analysieren und pers\xF6nliche Inhalte anzuzeigen."},consentManagerDialog:{title:"Einstellungen",description:"Passe Deine Datenschutz-Einstellungen hier an. W\xE4hle aus, welche Arten von Cookies und Tracking-Technologien zugelassen werden."},consentTypes:{necessary:{title:"Unbedingt erforderliche Cookies",description:"Diese Cookies sind f\xFCr das reibungslose Funktionieren der Website unerl\xE4sslich und k\xF6nnen nicht deaktiviert werden."},functionality:{title:"Funktionalit\xE4t",description:"Diese Cookies erm\xF6glichen erweiterte Funktionalit\xE4ten und eine Personalisierung der Website."},marketing:{title:"Marketing",description:"Diese Cookies werden verwendet, um relevante Werbung anzuzeigen und ihre Wirksamkeit zu messen."},measurement:{title:"Analyse",description:"Diese Cookies helfen uns zu verstehen, wie Besucher mit der Website interagieren um die Surf-Erfahrung zu verbessern."},experience:{title:"Erfahrung",description:"Diese Cookies helfen uns dabei, ein besseres Nutzerlebnis zu bieten und neue Funktionen zu testen."}},frame:{title:"Akzeptieren Sie {category}, um diesen Inhalt anzuzeigen.",actionButton:"Zustimmung f\xFCr {category} aktivieren"},legalLinks:{privacyPolicy:"Datenschutzerkl\xE4rung",cookiePolicy:"Cookie-Richtlinie",termsOfService:"Nutzungsbedingungen"},iab:{banner:{title:"Datenschutz-Einstellungen",description:"Wir und unsere {partnerCount} Partner speichern und/oder greifen auf Informationen auf Deinem Ger\xE4t zu und verarbeiten personenbezogene Daten, wie eindeutige Kennungen und Browsing-Daten, f\xFCr diese Website, um:",partnersLink:"{count} Partner",andMore:"Und {count} weitere...",legitimateInterestNotice:"Einige Partner beanspruchen ein berechtigtes Interesse zur Verarbeitung Deiner Daten. Du hast das Recht, dieser Verarbeitung zu widersprechen, Deine Auswahl anzupassen und Deine Einwilligung jederzeit zu widerrufen.",scopeServiceSpecific:"Deine Einwilligung gilt nur f\xFCr diese Website und hat keinen Einfluss auf andere Dienste.",scopeGroup:"Ihre Auswahl gilt f\xFCr alle unsere Websites in dieser Gruppe."},preferenceCenter:{title:"Datenschutz-Einstellungen",description:"Passe Deine Datenschutz-Einstellungen hier an. W\xE4hle aus, welche Arten von Cookies und Tracking-Technologien zugelassen werden.",tabs:{purposes:"Zwecke",vendors:"Anbieter"},purposeItem:{partners:"{count} Partner",vendorsUseLegitimateInterest:"{count} Anbieter beanspruchen berechtigtes Interesse",examples:"Beispiele",partnersUsingPurpose:"Partner, die diesen Zweck nutzen",withYourPermission:"Mit Deiner Erlaubnis",legitimateInterest:"Berechtigtes Interesse",objectButton:"Widersprechen",objected:"Widersprochen",rightToObject:"Du hast das Recht, der Verarbeitung auf Grundlage berechtigten Interesses zu widersprechen."},specialPurposes:{title:"Wesentliche Funktionen (erforderlich)",tooltip:"Diese sind f\xFCr die Funktionalit\xE4t und Sicherheit der Website erforderlich. Gem\xE4\xDF IAB TCF kannst Du diesen besonderen Zwecken nicht widersprechen."},vendorList:{search:"Anbieter suchen...",showingCount:"{filtered} von {total} Anbietern",iabVendorsHeading:"IAB-registrierte Anbieter",iabVendorsNotice:"Diese Partner sind beim IAB Transparency & Consent Framework (TCF) registriert, einem Industriestandard f\xFCr die Verwaltung von Einwilligungen",customVendorsHeading:"Benutzerdefinierte Partner",customVendorsNotice:"Dies sind benutzerdefinierte Partner, die nicht beim IAB Transparency & Consent Framework (TCF) registriert sind. Sie verarbeiten Daten auf Grundlage Ihrer Einwilligung und k\xF6nnen andere Datenschutzpraktiken haben als IAB-registrierte Anbieter.",purposes:"Zwecke",specialPurposes:"Besondere Zwecke",specialFeatures:"Besondere Merkmale",features:"Merkmale",dataCategories:"Datenkategorien",usesCookies:"Verwendet Cookies",nonCookieAccess:"Zugriff ohne Cookies",maxAge:"Max. Alter: {days} Tage",retention:"Aufbewahrung: {days} Tage",legitimateInterest:"Berecht. Interesse",privacyPolicy:"Datenschutzerkl\xE4rung",storageDisclosure:"Speicheroffenlegung",requiredNotice:"Erforderlich f\xFCr die Funktionalit\xE4t der Website, kann nicht deaktiviert werden"},footer:{consentStorage:'Einwilligungspr\xE4ferenzen werden in einem Cookie namens "euconsent-v2" f\xFCr 13 Monate gespeichert. The storage duration may be refreshed when you update your preferences.'}},common:{acceptAll:"Alle akzeptieren",rejectAll:"Alle ablehnen",customize:"Anpassen",saveSettings:"Einstellungen speichern",loading:"Wird geladen...",showingSelectedVendor:"Ausgew\xE4hlter Anbieter wird angezeigt",clearSelection:"L\xF6schen",customPartner:"Benutzerdefinierter Partner, nicht beim IAB registriert"}}},ci={common:{acceptAll:"\u0391\u03C0\u03BF\u03B4\u03BF\u03C7\u03AE \u03CC\u03BB\u03C9\u03BD",rejectAll:"\u0391\u03C0\u03CC\u03C1\u03C1\u03B9\u03C8\u03B7 \u03CC\u03BB\u03C9\u03BD",customize:"\u03A0\u03C1\u03BF\u03C3\u03B1\u03C1\u03BC\u03BF\u03B3\u03AE",save:"\u0391\u03C0\u03BF\u03B8\u03AE\u03BA\u03B5\u03C5\u03C3\u03B7 \u03C1\u03C5\u03B8\u03BC\u03AF\u03C3\u03B5\u03C9\u03BD"},cookieBanner:{title:"\u0395\u03BA\u03C4\u03B9\u03BC\u03BF\u03CD\u03BC\u03B5 \u03C4\u03BF \u03B1\u03C0\u03CC\u03C1\u03C1\u03B7\u03C4\u03CC \u03C3\u03B1\u03C2",description:"\u0391\u03C5\u03C4\u03CC\u03C2 \u03BF \u03B9\u03C3\u03C4\u03CC\u03C4\u03BF\u03C0\u03BF\u03C2 \u03C7\u03C1\u03B7\u03C3\u03B9\u03BC\u03BF\u03C0\u03BF\u03B9\u03B5\u03AF cookies \u03B3\u03B9\u03B1 \u03C4\u03B7 \u03B2\u03B5\u03BB\u03C4\u03AF\u03C9\u03C3\u03B7 \u03C4\u03B7\u03C2 \u03B5\u03BC\u03C0\u03B5\u03B9\u03C1\u03AF\u03B1\u03C2 \u03C0\u03B5\u03C1\u03B9\u03AE\u03B3\u03B7\u03C3\u03AE\u03C2 \u03C3\u03B1\u03C2, \u03C4\u03B7\u03BD \u03B1\u03BD\u03AC\u03BB\u03C5\u03C3\u03B7 \u03C4\u03B7\u03C2 \u03B5\u03C0\u03B9\u03C3\u03BA\u03B5\u03C8\u03B9\u03BC\u03CC\u03C4\u03B7\u03C4\u03B1\u03C2 \u03C4\u03BF\u03C5 \u03B9\u03C3\u03C4\u03CC\u03C4\u03BF\u03C0\u03BF\u03C5 \u03BA\u03B1\u03B9 \u03C4\u03B7\u03BD \u03C0\u03C1\u03BF\u03B2\u03BF\u03BB\u03AE \u03B5\u03BE\u03B1\u03C4\u03BF\u03BC\u03B9\u03BA\u03B5\u03C5\u03BC\u03AD\u03BD\u03BF\u03C5 \u03C0\u03B5\u03C1\u03B9\u03B5\u03C7\u03BF\u03BC\u03AD\u03BD\u03BF\u03C5."},consentManagerDialog:{title:"\u03A1\u03C5\u03B8\u03BC\u03AF\u03C3\u03B5\u03B9\u03C2 \u03B1\u03C0\u03BF\u03C1\u03C1\u03AE\u03C4\u03BF\u03C5",description:"\u03A0\u03C1\u03BF\u03C3\u03B1\u03C1\u03BC\u03CC\u03C3\u03C4\u03B5 \u03C4\u03B9\u03C2 \u03C1\u03C5\u03B8\u03BC\u03AF\u03C3\u03B5\u03B9\u03C2 \u03B1\u03C0\u03BF\u03C1\u03C1\u03AE\u03C4\u03BF\u03C5 \u03C3\u03B1\u03C2 \u03B5\u03B4\u03CE. \u039C\u03C0\u03BF\u03C1\u03B5\u03AF\u03C4\u03B5 \u03BD\u03B1 \u03B5\u03C0\u03B9\u03BB\u03AD\u03BE\u03B5\u03C4\u03B5 \u03C0\u03BF\u03B9\u03BF\u03C5\u03C2 \u03C4\u03CD\u03C0\u03BF\u03C5\u03C2 cookies \u03BA\u03B1\u03B9 \u03C4\u03B5\u03C7\u03BD\u03BF\u03BB\u03BF\u03B3\u03B9\u03CE\u03BD \u03C0\u03B1\u03C1\u03B1\u03BA\u03BF\u03BB\u03BF\u03CD\u03B8\u03B7\u03C3\u03B7\u03C2 \u03B5\u03C0\u03B9\u03C4\u03C1\u03AD\u03C0\u03B5\u03C4\u03B5."},consentTypes:{necessary:{title:"\u0391\u03C0\u03BF\u03BB\u03CD\u03C4\u03C9\u03C2 \u03B1\u03C0\u03B1\u03C1\u03B1\u03AF\u03C4\u03B7\u03C4\u03B1",description:"\u0391\u03C5\u03C4\u03AC \u03C4\u03B1 cookies \u03B5\u03AF\u03BD\u03B1\u03B9 \u03B1\u03C0\u03B1\u03C1\u03B1\u03AF\u03C4\u03B7\u03C4\u03B1 \u03B3\u03B9\u03B1 \u03C4\u03B7 \u03C3\u03C9\u03C3\u03C4\u03AE \u03BB\u03B5\u03B9\u03C4\u03BF\u03C5\u03C1\u03B3\u03AF\u03B1 \u03C4\u03BF\u03C5 \u03B9\u03C3\u03C4\u03CC\u03C4\u03BF\u03C0\u03BF\u03C5 \u03BA\u03B1\u03B9 \u03B4\u03B5\u03BD \u03BC\u03C0\u03BF\u03C1\u03BF\u03CD\u03BD \u03BD\u03B1 \u03B1\u03C0\u03B5\u03BD\u03B5\u03C1\u03B3\u03BF\u03C0\u03BF\u03B9\u03B7\u03B8\u03BF\u03CD\u03BD."},functionality:{title:"\u039B\u03B5\u03B9\u03C4\u03BF\u03C5\u03C1\u03B3\u03B9\u03BA\u03CC\u03C4\u03B7\u03C4\u03B1",description:"\u0391\u03C5\u03C4\u03AC \u03C4\u03B1 cookies \u03B5\u03C0\u03B9\u03C4\u03C1\u03AD\u03C0\u03BF\u03C5\u03BD \u03B2\u03B5\u03BB\u03C4\u03B9\u03C9\u03BC\u03AD\u03BD\u03B7 \u03BB\u03B5\u03B9\u03C4\u03BF\u03C5\u03C1\u03B3\u03B9\u03BA\u03CC\u03C4\u03B7\u03C4\u03B1 \u03BA\u03B1\u03B9 \u03B5\u03BE\u03B1\u03C4\u03BF\u03BC\u03AF\u03BA\u03B5\u03C5\u03C3\u03B7 \u03C4\u03BF\u03C5 \u03B9\u03C3\u03C4\u03CC\u03C4\u03BF\u03C0\u03BF\u03C5."},marketing:{title:"\u039C\u03AC\u03C1\u03BA\u03B5\u03C4\u03B9\u03BD\u03B3\u03BA",description:"\u0391\u03C5\u03C4\u03AC \u03C4\u03B1 cookies \u03C7\u03C1\u03B7\u03C3\u03B9\u03BC\u03BF\u03C0\u03BF\u03B9\u03BF\u03CD\u03BD\u03C4\u03B1\u03B9 \u03B3\u03B9\u03B1 \u03C4\u03B7\u03BD \u03C0\u03C1\u03BF\u03B2\u03BF\u03BB\u03AE \u03C3\u03C7\u03B5\u03C4\u03B9\u03BA\u03CE\u03BD \u03B4\u03B9\u03B1\u03C6\u03B7\u03BC\u03AF\u03C3\u03B5\u03C9\u03BD \u03BA\u03B1\u03B9 \u03C4\u03B7\u03BD \u03C0\u03B1\u03C1\u03B1\u03BA\u03BF\u03BB\u03BF\u03CD\u03B8\u03B7\u03C3\u03B7 \u03C4\u03B7\u03C2 \u03B1\u03C0\u03BF\u03C4\u03B5\u03BB\u03B5\u03C3\u03BC\u03B1\u03C4\u03B9\u03BA\u03CC\u03C4\u03B7\u03C4\u03AC\u03C2 \u03C4\u03BF\u03C5\u03C2."},measurement:{title:"\u0391\u03BD\u03B1\u03BB\u03C5\u03C4\u03B9\u03BA\u03AC \u03C3\u03C4\u03BF\u03B9\u03C7\u03B5\u03AF\u03B1",description:"\u0391\u03C5\u03C4\u03AC \u03C4\u03B1 cookies \u03BC\u03B1\u03C2 \u03B2\u03BF\u03B7\u03B8\u03BF\u03CD\u03BD \u03BD\u03B1 \u03BA\u03B1\u03C4\u03B1\u03BD\u03BF\u03AE\u03C3\u03BF\u03C5\u03BC\u03B5 \u03C0\u03CE\u03C2 \u03B1\u03BB\u03BB\u03B7\u03BB\u03B5\u03C0\u03B9\u03B4\u03C1\u03BF\u03CD\u03BD \u03BF\u03B9 \u03B5\u03C0\u03B9\u03C3\u03BA\u03AD\u03C0\u03C4\u03B5\u03C2 \u03BC\u03B5 \u03C4\u03BF\u03BD \u03B9\u03C3\u03C4\u03CC\u03C4\u03BF\u03C0\u03BF \u03BA\u03B1\u03B9 \u03BD\u03B1 \u03B2\u03B5\u03BB\u03C4\u03B9\u03CE\u03C3\u03BF\u03C5\u03BC\u03B5 \u03C4\u03B7\u03BD \u03B1\u03C0\u03CC\u03B4\u03BF\u03C3\u03AE \u03C4\u03BF\u03C5."},experience:{title:"\u0395\u03BC\u03C0\u03B5\u03B9\u03C1\u03AF\u03B1",description:"\u0391\u03C5\u03C4\u03AC \u03C4\u03B1 cookies \u03BC\u03B1\u03C2 \u03B2\u03BF\u03B7\u03B8\u03BF\u03CD\u03BD \u03BD\u03B1 \u03C0\u03B1\u03C1\u03AD\u03C7\u03BF\u03C5\u03BC\u03B5 \u03BA\u03B1\u03BB\u03CD\u03C4\u03B5\u03C1\u03B7 \u03B5\u03BC\u03C0\u03B5\u03B9\u03C1\u03AF\u03B1 \u03C7\u03C1\u03AE\u03C3\u03C4\u03B7 \u03BA\u03B1\u03B9 \u03BD\u03B1 \u03B4\u03BF\u03BA\u03B9\u03BC\u03AC\u03B6\u03BF\u03C5\u03BC\u03B5 \u03BD\u03AD\u03B5\u03C2 \u03BB\u03B5\u03B9\u03C4\u03BF\u03C5\u03C1\u03B3\u03AF\u03B5\u03C2."}},frame:{title:"\u0391\u03C0\u03BF\u03B4\u03B5\u03C7\u03C4\u03B5\u03AF\u03C4\u03B5 \u03C4\u03B7 \u03C3\u03C5\u03B3\u03BA\u03B1\u03C4\u03AC\u03B8\u03B5\u03C3\u03B7 {category} \u03B3\u03B9\u03B1 \u03BD\u03B1 \u03B4\u03B5\u03AF\u03C4\u03B5 \u03B1\u03C5\u03C4\u03CC \u03C4\u03BF \u03C0\u03B5\u03C1\u03B9\u03B5\u03C7\u03CC\u03BC\u03B5\u03BD\u03BF.",actionButton:"\u0395\u03BD\u03B5\u03C1\u03B3\u03BF\u03C0\u03BF\u03AF\u03B7\u03C3\u03B7 \u03C3\u03C5\u03B3\u03BA\u03B1\u03C4\u03AC\u03B8\u03B5\u03C3\u03B7\u03C2 {category}"},legalLinks:{privacyPolicy:"\u03A0\u03BF\u03BB\u03B9\u03C4\u03B9\u03BA\u03AE \u03B1\u03C0\u03BF\u03C1\u03C1\u03AE\u03C4\u03BF\u03C5",cookiePolicy:"\u03A0\u03BF\u03BB\u03B9\u03C4\u03B9\u03BA\u03AE cookies",termsOfService:"\u038C\u03C1\u03BF\u03B9 \u03C7\u03C1\u03AE\u03C3\u03B7\u03C2"},iab:{banner:{title:"\u03A1\u03C5\u03B8\u03BC\u03AF\u03C3\u03B5\u03B9\u03C2 \u03B1\u03C0\u03BF\u03C1\u03C1\u03AE\u03C4\u03BF\u03C5",description:"\u0395\u03BC\u03B5\u03AF\u03C2 \u03BA\u03B1\u03B9 \u03BF\u03B9 {partnerCount} \u03C3\u03C5\u03BD\u03B5\u03C1\u03B3\u03AC\u03C4\u03B5\u03C2 \u03BC\u03B1\u03C2 \u03B1\u03C0\u03BF\u03B8\u03B7\u03BA\u03B5\u03CD\u03BF\u03C5\u03BC\u03B5 \u03AE/\u03BA\u03B1\u03B9 \u03AD\u03C7\u03BF\u03C5\u03BC\u03B5 \u03C0\u03C1\u03CC\u03C3\u03B2\u03B1\u03C3\u03B7 \u03C3\u03B5 \u03C0\u03BB\u03B7\u03C1\u03BF\u03C6\u03BF\u03C1\u03AF\u03B5\u03C2 \u03C3\u03C4\u03B7 \u03C3\u03C5\u03C3\u03BA\u03B5\u03C5\u03AE \u03C3\u03B1\u03C2 \u03BA\u03B1\u03B9 \u03B5\u03C0\u03B5\u03BE\u03B5\u03C1\u03B3\u03B1\u03B6\u03CC\u03BC\u03B1\u03C3\u03C4\u03B5 \u03C0\u03C1\u03BF\u03C3\u03C9\u03C0\u03B9\u03BA\u03AC \u03B4\u03B5\u03B4\u03BF\u03BC\u03AD\u03BD\u03B1, \u03CC\u03C0\u03C9\u03C2 \u03BC\u03BF\u03BD\u03B1\u03B4\u03B9\u03BA\u03AC \u03B1\u03BD\u03B1\u03B3\u03BD\u03C9\u03C1\u03B9\u03C3\u03C4\u03B9\u03BA\u03AC \u03BA\u03B1\u03B9 \u03B4\u03B5\u03B4\u03BF\u03BC\u03AD\u03BD\u03B1 \u03C0\u03B5\u03C1\u03B9\u03AE\u03B3\u03B7\u03C3\u03B7\u03C2, \u03B3\u03B9\u03B1 \u03B1\u03C5\u03C4\u03CC\u03BD \u03C4\u03BF\u03BD \u03B9\u03C3\u03C4\u03CC\u03C4\u03BF\u03C0\u03BF, \u03B3\u03B9\u03B1 \u03BD\u03B1:",partnersLink:"{count} \u03C3\u03C5\u03BD\u03B5\u03C1\u03B3\u03AC\u03C4\u03B5\u03C2",andMore:"\u039A\u03B1\u03B9 {count} \u03B1\u03BA\u03CC\u03BC\u03B7...",legitimateInterestNotice:"\u039F\u03C1\u03B9\u03C3\u03BC\u03AD\u03BD\u03BF\u03B9 \u03C3\u03C5\u03BD\u03B5\u03C1\u03B3\u03AC\u03C4\u03B5\u03C2 \u03B5\u03C0\u03B9\u03BA\u03B1\u03BB\u03BF\u03CD\u03BD\u03C4\u03B1\u03B9 \u03AD\u03BD\u03BD\u03BF\u03BC\u03BF \u03C3\u03C5\u03BC\u03C6\u03AD\u03C1\u03BF\u03BD \u03B3\u03B9\u03B1 \u03C4\u03B7\u03BD \u03B5\u03C0\u03B5\u03BE\u03B5\u03C1\u03B3\u03B1\u03C3\u03AF\u03B1 \u03C4\u03C9\u03BD \u03B4\u03B5\u03B4\u03BF\u03BC\u03AD\u03BD\u03C9\u03BD \u03C3\u03B1\u03C2. \u0388\u03C7\u03B5\u03C4\u03B5 \u03C4\u03BF \u03B4\u03B9\u03BA\u03B1\u03AF\u03C9\u03BC\u03B1 \u03BD\u03B1 \u03B1\u03BD\u03C4\u03B9\u03C4\u03B1\u03C7\u03B8\u03B5\u03AF\u03C4\u03B5 \u03C3\u03B5 \u03B1\u03C5\u03C4\u03AE\u03BD \u03C4\u03B7\u03BD \u03B5\u03C0\u03B5\u03BE\u03B5\u03C1\u03B3\u03B1\u03C3\u03AF\u03B1, \u03BD\u03B1 \u03C0\u03C1\u03BF\u03C3\u03B1\u03C1\u03BC\u03CC\u03C3\u03B5\u03C4\u03B5 \u03C4\u03B9\u03C2 \u03B5\u03C0\u03B9\u03BB\u03BF\u03B3\u03AD\u03C2 \u03C3\u03B1\u03C2 \u03BA\u03B1\u03B9 \u03BD\u03B1 \u03B1\u03BD\u03B1\u03BA\u03B1\u03BB\u03AD\u03C3\u03B5\u03C4\u03B5 \u03C4\u03B7 \u03C3\u03C5\u03B3\u03BA\u03B1\u03C4\u03AC\u03B8\u03B5\u03C3\u03AE \u03C3\u03B1\u03C2 \u03B1\u03BD\u03AC \u03C0\u03AC\u03C3\u03B1 \u03C3\u03C4\u03B9\u03B3\u03BC\u03AE.",scopeServiceSpecific:"\u0397 \u03C3\u03C5\u03B3\u03BA\u03B1\u03C4\u03AC\u03B8\u03B5\u03C3\u03AE \u03C3\u03B1\u03C2 \u03B9\u03C3\u03C7\u03CD\u03B5\u03B9 \u03BC\u03CC\u03BD\u03BF \u03B3\u03B9\u03B1 \u03B1\u03C5\u03C4\u03CC\u03BD \u03C4\u03BF\u03BD \u03B9\u03C3\u03C4\u03CC\u03C4\u03BF\u03C0\u03BF \u03BA\u03B1\u03B9 \u03B4\u03B5\u03BD \u03B8\u03B1 \u03B5\u03C0\u03B7\u03C1\u03B5\u03AC\u03C3\u03B5\u03B9 \u03AC\u03BB\u03BB\u03B5\u03C2 \u03C5\u03C0\u03B7\u03C1\u03B5\u03C3\u03AF\u03B5\u03C2.",scopeGroup:"\u0397 \u03B5\u03C0\u03B9\u03BB\u03BF\u03B3\u03AE \u03C3\u03B1\u03C2 \u03B9\u03C3\u03C7\u03CD\u03B5\u03B9 \u03B3\u03B9\u03B1 \u03CC\u03BB\u03B5\u03C2 \u03C4\u03B9\u03C2 \u03B9\u03C3\u03C4\u03BF\u03C3\u03B5\u03BB\u03AF\u03B4\u03B5\u03C2 \u03BC\u03B1\u03C2 \u03C3\u03B5 \u03B1\u03C5\u03C4\u03AE \u03C4\u03B7\u03BD \u03BF\u03BC\u03AC\u03B4\u03B1."},preferenceCenter:{title:"\u03A1\u03C5\u03B8\u03BC\u03AF\u03C3\u03B5\u03B9\u03C2 \u03B1\u03C0\u03BF\u03C1\u03C1\u03AE\u03C4\u03BF\u03C5",description:"\u03A0\u03C1\u03BF\u03C3\u03B1\u03C1\u03BC\u03CC\u03C3\u03C4\u03B5 \u03C4\u03B9\u03C2 \u03C1\u03C5\u03B8\u03BC\u03AF\u03C3\u03B5\u03B9\u03C2 \u03B1\u03C0\u03BF\u03C1\u03C1\u03AE\u03C4\u03BF\u03C5 \u03C3\u03B1\u03C2 \u03B5\u03B4\u03CE. \u039C\u03C0\u03BF\u03C1\u03B5\u03AF\u03C4\u03B5 \u03BD\u03B1 \u03B5\u03C0\u03B9\u03BB\u03AD\u03BE\u03B5\u03C4\u03B5 \u03C0\u03BF\u03B9\u03BF\u03C5\u03C2 \u03C4\u03CD\u03C0\u03BF\u03C5\u03C2 cookies \u03BA\u03B1\u03B9 \u03C4\u03B5\u03C7\u03BD\u03BF\u03BB\u03BF\u03B3\u03B9\u03CE\u03BD \u03C0\u03B1\u03C1\u03B1\u03BA\u03BF\u03BB\u03BF\u03CD\u03B8\u03B7\u03C3\u03B7\u03C2 \u03B5\u03C0\u03B9\u03C4\u03C1\u03AD\u03C0\u03B5\u03C4\u03B5.",tabs:{purposes:"\u03A3\u03BA\u03BF\u03C0\u03BF\u03AF",vendors:"\u03A3\u03C5\u03BD\u03B5\u03C1\u03B3\u03AC\u03C4\u03B5\u03C2"},purposeItem:{partners:"{count} \u03C3\u03C5\u03BD\u03B5\u03C1\u03B3\u03AC\u03C4\u03B5\u03C2",vendorsUseLegitimateInterest:"{count} \u03C3\u03C5\u03BD\u03B5\u03C1\u03B3\u03AC\u03C4\u03B5\u03C2 \u03B5\u03C0\u03B9\u03BA\u03B1\u03BB\u03BF\u03CD\u03BD\u03C4\u03B1\u03B9 \u03AD\u03BD\u03BD\u03BF\u03BC\u03BF \u03C3\u03C5\u03BC\u03C6\u03AD\u03C1\u03BF\u03BD",examples:"\u03A0\u03B1\u03C1\u03B1\u03B4\u03B5\u03AF\u03B3\u03BC\u03B1\u03C4\u03B1",partnersUsingPurpose:"\u03A3\u03C5\u03BD\u03B5\u03C1\u03B3\u03AC\u03C4\u03B5\u03C2 \u03C0\u03BF\u03C5 \u03C7\u03C1\u03B7\u03C3\u03B9\u03BC\u03BF\u03C0\u03BF\u03B9\u03BF\u03CD\u03BD \u03B1\u03C5\u03C4\u03CC\u03BD \u03C4\u03BF\u03BD \u03C3\u03BA\u03BF\u03C0\u03CC",withYourPermission:"\u039C\u03B5 \u03C4\u03B7 \u03C3\u03C5\u03B3\u03BA\u03B1\u03C4\u03AC\u03B8\u03B5\u03C3\u03AE \u03C3\u03B1\u03C2",legitimateInterest:"\u0388\u03BD\u03BD\u03BF\u03BC\u03BF \u03C3\u03C5\u03BC\u03C6\u03AD\u03C1\u03BF\u03BD",objectButton:"\u0391\u03BD\u03C4\u03AF\u03C1\u03C1\u03B7\u03C3\u03B7",objected:"\u0391\u03BD\u03C4\u03B9\u03C4\u03AC\u03C7\u03B8\u03B7\u03BA\u03B5",rightToObject:"\u0388\u03C7\u03B5\u03C4\u03B5 \u03C4\u03BF \u03B4\u03B9\u03BA\u03B1\u03AF\u03C9\u03BC\u03B1 \u03BD\u03B1 \u03B1\u03BD\u03C4\u03B9\u03C4\u03B1\u03C7\u03B8\u03B5\u03AF\u03C4\u03B5 \u03C3\u03C4\u03B7\u03BD \u03B5\u03C0\u03B5\u03BE\u03B5\u03C1\u03B3\u03B1\u03C3\u03AF\u03B1 \u03C0\u03BF\u03C5 \u03B2\u03B1\u03C3\u03AF\u03B6\u03B5\u03C4\u03B1\u03B9 \u03C3\u03B5 \u03AD\u03BD\u03BD\u03BF\u03BC\u03BF \u03C3\u03C5\u03BC\u03C6\u03AD\u03C1\u03BF\u03BD."},specialPurposes:{title:"\u0392\u03B1\u03C3\u03B9\u03BA\u03AD\u03C2 \u03BB\u03B5\u03B9\u03C4\u03BF\u03C5\u03C1\u03B3\u03AF\u03B5\u03C2 (\u03B1\u03C0\u03B1\u03B9\u03C4\u03BF\u03CD\u03BD\u03C4\u03B1\u03B9)",tooltip:"\u0391\u03C5\u03C4\u03AD\u03C2 \u03B5\u03AF\u03BD\u03B1\u03B9 \u03B1\u03C0\u03B1\u03C1\u03B1\u03AF\u03C4\u03B7\u03C4\u03B5\u03C2 \u03B3\u03B9\u03B1 \u03C4\u03B7 \u03BB\u03B5\u03B9\u03C4\u03BF\u03C5\u03C1\u03B3\u03B9\u03BA\u03CC\u03C4\u03B7\u03C4\u03B1 \u03BA\u03B1\u03B9 \u03C4\u03B7\u03BD \u03B1\u03C3\u03C6\u03AC\u03BB\u03B5\u03B9\u03B1 \u03C4\u03BF\u03C5 \u03B9\u03C3\u03C4\u03CC\u03C4\u03BF\u03C0\u03BF\u03C5. \u03A3\u03CD\u03BC\u03C6\u03C9\u03BD\u03B1 \u03BC\u03B5 \u03C4\u03BF IAB TCF, \u03B4\u03B5\u03BD \u03BC\u03C0\u03BF\u03C1\u03B5\u03AF\u03C4\u03B5 \u03BD\u03B1 \u03B1\u03BD\u03C4\u03B9\u03C4\u03B1\u03C7\u03B8\u03B5\u03AF\u03C4\u03B5 \u03C3\u03B5 \u03B1\u03C5\u03C4\u03BF\u03CD\u03C2 \u03C4\u03BF\u03C5\u03C2 \u03B5\u03B9\u03B4\u03B9\u03BA\u03BF\u03CD\u03C2 \u03C3\u03BA\u03BF\u03C0\u03BF\u03CD\u03C2."},vendorList:{search:"\u0391\u03BD\u03B1\u03B6\u03AE\u03C4\u03B7\u03C3\u03B7 \u03C3\u03C5\u03BD\u03B5\u03C1\u03B3\u03B1\u03C4\u03CE\u03BD...",showingCount:"{filtered} \u03B1\u03C0\u03CC {total} \u03C3\u03C5\u03BD\u03B5\u03C1\u03B3\u03AC\u03C4\u03B5\u03C2",iabVendorsHeading:"\u0395\u03B3\u03B3\u03B5\u03B3\u03C1\u03B1\u03BC\u03BC\u03AD\u03BD\u03BF\u03B9 \u03C3\u03C5\u03BD\u03B5\u03C1\u03B3\u03AC\u03C4\u03B5\u03C2 IAB",iabVendorsNotice:"\u0391\u03C5\u03C4\u03BF\u03AF \u03BF\u03B9 \u03C3\u03C5\u03BD\u03B5\u03C1\u03B3\u03AC\u03C4\u03B5\u03C2 \u03B5\u03AF\u03BD\u03B1\u03B9 \u03B5\u03B3\u03B3\u03B5\u03B3\u03C1\u03B1\u03BC\u03BC\u03AD\u03BD\u03BF\u03B9 \u03C3\u03C4\u03BF IAB Transparency & Consent Framework (TCF), \u03AD\u03BD\u03B1 \u03B2\u03B9\u03BF\u03BC\u03B7\u03C7\u03B1\u03BD\u03B9\u03BA\u03CC \u03C0\u03C1\u03CC\u03C4\u03C5\u03C0\u03BF \u03B3\u03B9\u03B1 \u03C4\u03B7 \u03B4\u03B9\u03B1\u03C7\u03B5\u03AF\u03C1\u03B9\u03C3\u03B7 \u03C4\u03B7\u03C2 \u03C3\u03C5\u03B3\u03BA\u03B1\u03C4\u03AC\u03B8\u03B5\u03C3\u03B7\u03C2",customVendorsHeading:"\u03A0\u03C1\u03BF\u03C3\u03B1\u03C1\u03BC\u03BF\u03C3\u03BC\u03AD\u03BD\u03BF\u03B9 \u03C3\u03C5\u03BD\u03B5\u03C1\u03B3\u03AC\u03C4\u03B5\u03C2",customVendorsNotice:"\u0391\u03C5\u03C4\u03BF\u03AF \u03B5\u03AF\u03BD\u03B1\u03B9 \u03C0\u03C1\u03BF\u03C3\u03B1\u03C1\u03BC\u03BF\u03C3\u03BC\u03AD\u03BD\u03BF\u03B9 \u03C3\u03C5\u03BD\u03B5\u03C1\u03B3\u03AC\u03C4\u03B5\u03C2 \u03C0\u03BF\u03C5 \u03B4\u03B5\u03BD \u03B5\u03AF\u03BD\u03B1\u03B9 \u03B5\u03B3\u03B3\u03B5\u03B3\u03C1\u03B1\u03BC\u03BC\u03AD\u03BD\u03BF\u03B9 \u03C3\u03C4\u03BF IAB Transparency & Consent Framework (TCF). \u0395\u03C0\u03B5\u03BE\u03B5\u03C1\u03B3\u03AC\u03B6\u03BF\u03BD\u03C4\u03B1\u03B9 \u03B4\u03B5\u03B4\u03BF\u03BC\u03AD\u03BD\u03B1 \u03BC\u03B5 \u03B2\u03AC\u03C3\u03B7 \u03C4\u03B7 \u03C3\u03C5\u03B3\u03BA\u03B1\u03C4\u03AC\u03B8\u03B5\u03C3\u03AE \u03C3\u03B1\u03C2 \u03BA\u03B1\u03B9 \u03B5\u03BD\u03B4\u03AD\u03C7\u03B5\u03C4\u03B1\u03B9 \u03BD\u03B1 \u03AD\u03C7\u03BF\u03C5\u03BD \u03B4\u03B9\u03B1\u03C6\u03BF\u03C1\u03B5\u03C4\u03B9\u03BA\u03AD\u03C2 \u03C0\u03C1\u03B1\u03BA\u03C4\u03B9\u03BA\u03AD\u03C2 \u03B1\u03C0\u03BF\u03C1\u03C1\u03AE\u03C4\u03BF\u03C5 \u03B1\u03C0\u03CC \u03C4\u03BF\u03C5\u03C2 \u03B5\u03B3\u03B3\u03B5\u03B3\u03C1\u03B1\u03BC\u03BC\u03AD\u03BD\u03BF\u03C5\u03C2 \u03C3\u03C5\u03BD\u03B5\u03C1\u03B3\u03AC\u03C4\u03B5\u03C2 \u03C4\u03BF\u03C5 IAB.",purposes:"\u03A3\u03BA\u03BF\u03C0\u03BF\u03AF",specialPurposes:"\u0395\u03B9\u03B4\u03B9\u03BA\u03BF\u03AF \u03C3\u03BA\u03BF\u03C0\u03BF\u03AF",specialFeatures:"\u0395\u03B9\u03B4\u03B9\u03BA\u03AC \u03C7\u03B1\u03C1\u03B1\u03BA\u03C4\u03B7\u03C1\u03B9\u03C3\u03C4\u03B9\u03BA\u03AC",features:"\u03A7\u03B1\u03C1\u03B1\u03BA\u03C4\u03B7\u03C1\u03B9\u03C3\u03C4\u03B9\u03BA\u03AC",dataCategories:"\u039A\u03B1\u03C4\u03B7\u03B3\u03BF\u03C1\u03AF\u03B5\u03C2 \u03B4\u03B5\u03B4\u03BF\u03BC\u03AD\u03BD\u03C9\u03BD",usesCookies:"\u03A7\u03C1\u03B7\u03C3\u03B9\u03BC\u03BF\u03C0\u03BF\u03B9\u03B5\u03AF cookies",nonCookieAccess:"\u03A0\u03C1\u03CC\u03C3\u03B2\u03B1\u03C3\u03B7 \u03C7\u03C9\u03C1\u03AF\u03C2 cookies",maxAge:"\u039C\u03AD\u03B3\u03B9\u03C3\u03C4\u03B7 \u03B4\u03B9\u03AC\u03C1\u03BA\u03B5\u03B9\u03B1: {days} \u03B7\u03BC.",retention:"\u0394\u03B9\u03B1\u03C4\u03AE\u03C1\u03B7\u03C3\u03B7: {days} \u03B7\u03BC.",legitimateInterest:"\u0388\u03BD\u03BD\u03BF\u03BC\u03BF \u03C3\u03C5\u03BC\u03C6\u03AD\u03C1\u03BF\u03BD",privacyPolicy:"\u03A0\u03BF\u03BB\u03B9\u03C4\u03B9\u03BA\u03AE \u03B1\u03C0\u03BF\u03C1\u03C1\u03AE\u03C4\u03BF\u03C5",storageDisclosure:"\u0393\u03BD\u03C9\u03C3\u03C4\u03BF\u03C0\u03BF\u03AF\u03B7\u03C3\u03B7 \u03B1\u03C0\u03BF\u03B8\u03AE\u03BA\u03B5\u03C5\u03C3\u03B7\u03C2",requiredNotice:"\u0391\u03C0\u03B1\u03B9\u03C4\u03B5\u03AF\u03C4\u03B1\u03B9 \u03B3\u03B9\u03B1 \u03C4\u03B7 \u03BB\u03B5\u03B9\u03C4\u03BF\u03C5\u03C1\u03B3\u03B9\u03BA\u03CC\u03C4\u03B7\u03C4\u03B1 \u03C4\u03BF\u03C5 \u03B9\u03C3\u03C4\u03CC\u03C4\u03BF\u03C0\u03BF\u03C5, \u03B4\u03B5\u03BD \u03BC\u03C0\u03BF\u03C1\u03B5\u03AF \u03BD\u03B1 \u03B1\u03C0\u03B5\u03BD\u03B5\u03C1\u03B3\u03BF\u03C0\u03BF\u03B9\u03B7\u03B8\u03B5\u03AF"},footer:{consentStorage:'\u039F\u03B9 \u03C0\u03C1\u03BF\u03C4\u03B9\u03BC\u03AE\u03C3\u03B5\u03B9\u03C2 \u03C3\u03C5\u03B3\u03BA\u03B1\u03C4\u03AC\u03B8\u03B5\u03C3\u03B7\u03C2 \u03B1\u03C0\u03BF\u03B8\u03B7\u03BA\u03B5\u03CD\u03BF\u03BD\u03C4\u03B1\u03B9 \u03C3\u03B5 cookie \u03BC\u03B5 \u03C4\u03BF \u03CC\u03BD\u03BF\u03BC\u03B1 "euconsent-v2" \u03B3\u03B9\u03B1 13 \u03BC\u03AE\u03BD\u03B5\u03C2. The storage duration may be refreshed when you update your preferences.'}},common:{acceptAll:"\u0391\u03C0\u03BF\u03B4\u03BF\u03C7\u03AE \u03CC\u03BB\u03C9\u03BD",rejectAll:"\u0391\u03C0\u03CC\u03C1\u03C1\u03B9\u03C8\u03B7 \u03CC\u03BB\u03C9\u03BD",customize:"\u03A0\u03C1\u03BF\u03C3\u03B1\u03C1\u03BC\u03BF\u03B3\u03AE",saveSettings:"\u0391\u03C0\u03BF\u03B8\u03AE\u03BA\u03B5\u03C5\u03C3\u03B7 \u03C1\u03C5\u03B8\u03BC\u03AF\u03C3\u03B5\u03C9\u03BD",loading:"\u03A6\u03CC\u03C1\u03C4\u03C9\u03C3\u03B7...",showingSelectedVendor:"\u0395\u03BC\u03C6\u03AC\u03BD\u03B9\u03C3\u03B7 \u03B5\u03C0\u03B9\u03BB\u03B5\u03B3\u03BC\u03AD\u03BD\u03BF\u03C5 \u03C3\u03C5\u03BD\u03B5\u03C1\u03B3\u03AC\u03C4\u03B7",clearSelection:"\u0395\u03BA\u03BA\u03B1\u03B8\u03AC\u03C1\u03B9\u03C3\u03B7",customPartner:"\u03A0\u03C1\u03BF\u03C3\u03B1\u03C1\u03BC\u03BF\u03C3\u03BC\u03AD\u03BD\u03BF\u03C2 \u03C3\u03C5\u03BD\u03B5\u03C1\u03B3\u03AC\u03C4\u03B7\u03C2 \u03C0\u03BF\u03C5 \u03B4\u03B5\u03BD \u03B5\u03AF\u03BD\u03B1\u03B9 \u03B5\u03B3\u03B3\u03B5\u03B3\u03C1\u03B1\u03BC\u03BC\u03AD\u03BD\u03BF\u03C2 \u03C3\u03C4\u03BF IAB"}}},it={common:{acceptAll:"Accept All",rejectAll:"Reject All",customize:"Customize",save:"Save Settings"},cookieBanner:{title:"We value your privacy",description:"This site uses cookies to improve your browsing experience, analyze site traffic, and show personalized content."},consentManagerDialog:{title:"Privacy Settings",description:"Customize your privacy settings here. You can choose which types of cookies and tracking technologies you allow."},consentTypes:{necessary:{title:"Strictly Necessary",description:"These cookies are essential for the website to function properly and cannot be disabled."},functionality:{title:"Functionality",description:"These cookies enable enhanced functionality and personalization of the website."},marketing:{title:"Marketing",description:"These cookies are used to deliver relevant advertisements and track their effectiveness."},measurement:{title:"Analytics",description:"These cookies help us understand how visitors interact with the website and improve its performance."},experience:{title:"Experience",description:"These cookies help us provide a better user experience and test new features."}},frame:{title:"Accept {category} consent to view this content.",actionButton:"Enable {category} consent"},legalLinks:{privacyPolicy:"Privacy Policy",cookiePolicy:"Cookie Policy",termsOfService:"Terms of Service"},iab:{banner:{title:"Privacy Settings",description:"We and our {partnerCount} partners store and/or access information on your device and process personal data, such as unique identifiers and browsing data, for this website, to:",partnersLink:"{count} partners",andMore:"And {count} more...",legitimateInterestNotice:"Some partners claim a legitimate interest to process your data. You have the right to object to this processing, customize your choices, and withdraw your consent at any time.",scopeServiceSpecific:"Your consent applies only to this website and will not affect other services.",scopeGroup:"Your choice applies across our websites in this group."},preferenceCenter:{title:"Privacy Settings",description:"Customize your privacy settings here. You can choose which types of cookies and tracking technologies you allow.",tabs:{purposes:"Purposes",vendors:"Vendors"},purposeItem:{partners:"{count} partners",vendorsUseLegitimateInterest:"{count} vendors claim legitimate interest",examples:"Examples",partnersUsingPurpose:"Partners Using This Purpose",withYourPermission:"With Your Permission",legitimateInterest:"Legitimate Interest",objectButton:"Object",objected:"Objected",rightToObject:"You have the right to object to processing based on legitimate interest."},specialPurposes:{title:"Essential Functions (Required)",tooltip:"These are required for site functionality and security. Per IAB TCF, you cannot object to these special purposes."},vendorList:{search:"Search vendors...",showingCount:"{filtered} of {total} vendors",iabVendorsHeading:"IAB Registered Vendors",iabVendorsNotice:"These partners are registered with the IAB Transparency & Consent Framework (TCF), an industry standard for managing consent",customVendorsHeading:"Custom Partners",customVendorsNotice:"These are custom partners not registered with IAB Transparency & Consent Framework (TCF). They process data based on your consent and may have different privacy practices than IAB-registered vendors.",purposes:"Purposes",specialPurposes:"Special Purposes",specialFeatures:"Special Features",features:"Features",dataCategories:"Data Categories",usesCookies:"Uses Cookies",nonCookieAccess:"Non-Cookie Access",maxAge:"Max Age: {days}d",retention:"Retention: {days}d",legitimateInterest:"Leg. Interest",privacyPolicy:"Privacy Policy",storageDisclosure:"Storage Disclosure",requiredNotice:"Required for site functionality, cannot be disabled"},footer:{consentStorage:'Consent preferences are stored in a cookie named "euconsent-v2" for 13 months. The storage duration may be refreshed when you update your preferences.'}},common:{acceptAll:"Accept All",rejectAll:"Reject All",customize:"Customize",saveSettings:"Save Settings",loading:"Loading...",showingSelectedVendor:"Showing selected vendor",clearSelection:"Clear",customPartner:"Custom partner not registered with IAB"}}},li={common:{acceptAll:"Aceptar todo",rejectAll:"Rechazar todo",customize:"Personalizar",save:"Guardar ajustes"},cookieBanner:{title:"Valoramos tu privacidad",description:"Este sitio web utiliza cookies para mejorar tu experiencia de navegaci\xF3n, analizar el tr\xE1fico del sitio y mostrar contenido personalizado."},consentManagerDialog:{title:"Configuraci\xF3n de privacidad",description:"Personaliza tus ajustes de privacidad aqu\xED. Puedes elegir qu\xE9 tipos de cookies y tecnolog\xEDas de seguimiento permites."},consentTypes:{necessary:{title:"Necesario",description:"Estas cookies son esenciales para que el sitio web funcione correctamente y no pueden ser deshabilitadas."},functionality:{title:"Funcionalidad",description:"Estas cookies permiten una mejor funcionalidad y personalizaci\xF3n del sitio web."},marketing:{title:"Marketing",description:"Estas cookies se utilizan para ofrecer anuncios relevantes y realizar un seguimiento de su eficacia."},measurement:{title:"Anal\xEDtica",description:"Estas cookies nos ayudan a comprender c\xF3mo los visitantes interact\xFAan con el sitio web y a mejorar su rendimiento."},experience:{title:"Experiencia",description:"Estas cookies nos ayudan a proporcionar una mejor experiencia de usuario y a probar nuevas funciones."}},frame:{title:"Acepta {category} para ver este contenido.",actionButton:"Habilitar consentimiento de {category}"},legalLinks:{privacyPolicy:"Pol\xEDtica de Privacidad",cookiePolicy:"Pol\xEDtica de Cookies",termsOfService:"T\xE9rminos de Servicio"},iab:{banner:{title:"Configuraci\xF3n de privacidad",description:"Nosotros y nuestros {partnerCount} socios almacenamos y/o accedemos a informaci\xF3n en tu dispositivo y procesamos datos personales, como identificadores \xFAnicos y datos de navegaci\xF3n, para este sitio web, con el fin de:",partnersLink:"{count} socios",andMore:"Y {count} m\xE1s...",legitimateInterestNotice:"Algunos socios reclaman un inter\xE9s leg\xEDtimo para procesar tus datos. Tienes derecho a oponerte a este procesamiento, personalizar tus opciones y retirar tu consentimiento en cualquier momento.",scopeServiceSpecific:"Tu consentimiento se aplica solo a este sitio web y no afectar\xE1 a otros servicios.",scopeGroup:"Su elecci\xF3n se aplica a todos nuestros sitios web de este grupo."},preferenceCenter:{title:"Configuraci\xF3n de privacidad",description:"Personaliza tus ajustes de privacidad aqu\xED. Puedes elegir qu\xE9 tipos de cookies y tecnolog\xEDas de seguimiento permites.",tabs:{purposes:"Prop\xF3sitos",vendors:"Proveedores"},purposeItem:{partners:"{count} socios",vendorsUseLegitimateInterest:"{count} proveedores reclaman inter\xE9s leg\xEDtimo",examples:"Ejemplos",partnersUsingPurpose:"Socios que utilizan este prop\xF3sito",withYourPermission:"Con tu permiso",legitimateInterest:"Inter\xE9s leg\xEDtimo",objectButton:"Oponerse",objected:"Opuesto",rightToObject:"Tienes derecho a oponerte al procesamiento basado en inter\xE9s leg\xEDtimo."},specialPurposes:{title:"Funciones esenciales (requeridas)",tooltip:"Estas son necesarias para la funcionalidad y seguridad del sitio. Seg\xFAn el TCF de IAB, no puedes oponerte a estos prop\xF3sitos especiales."},vendorList:{search:"Buscar proveedores...",showingCount:"{filtered} de {total} proveedores",iabVendorsHeading:"Proveedores registrados en IAB",iabVendorsNotice:"Estos socios est\xE1n registrados en el Marco de Transparencia y Consentimiento (TCF) de IAB, un est\xE1ndar de la industria para gestionar el consentimiento",customVendorsHeading:"Socios personalizados",customVendorsNotice:"Estos son socios personalizados no registrados en el Marco de Transparencia y Consentimiento de IAB (TCF). Procesan datos bas\xE1ndose en tu consentimiento y pueden tener pr\xE1cticas de privacidad diferentes a las de los proveedores registrados en IAB.",purposes:"Finalidades",specialPurposes:"Finalidades especiales",specialFeatures:"Caracter\xEDsticas especiales",features:"Caracter\xEDsticas",dataCategories:"Categor\xEDas de datos",usesCookies:"Usa cookies",nonCookieAccess:"Acceso sin cookies",maxAge:"Duraci\xF3n m\xE1xima: {days}d",retention:"Retenci\xF3n: {days}d",legitimateInterest:"Inter\xE9s leg\xEDtimo",privacyPolicy:"Pol\xEDtica de privacidad",storageDisclosure:"Divulgaci\xF3n de almacenamiento",requiredNotice:"Requerido para la funcionalidad del sitio, no se puede desactivar"},footer:{consentStorage:'Las preferencias de consentimiento se almacenan en una cookie llamada "euconsent-v2" durante 13 meses. The storage duration may be refreshed when you update your preferences.'}},common:{acceptAll:"Aceptar todo",rejectAll:"Rechazar todo",customize:"Personalizar",saveSettings:"Guardar ajustes",loading:"Cargando...",showingSelectedVendor:"Mostrando proveedor seleccionado",clearSelection:"Limpiar",customPartner:"Socio personalizado no registrado en IAB"}}},ui={common:{acceptAll:"N\xF5ustu k\xF5igiga",rejectAll:"Keeldu k\xF5igist",customize:"Kohanda",save:"Salvesta seaded"},cookieBanner:{title:"Hindame teie privaatsust",description:"See sait kasutab k\xFCpsiseid, et parandada teie sirvimiskogemust, anal\xFC\xFCsida saidi liiklust ja n\xE4idata isikup\xE4rastatud sisu."},consentManagerDialog:{title:"Privaatsusseaded",description:"Kohandage siin oma privaatsusseadeid. Saate valida, milliseid k\xFCpsiseid ja j\xE4lgimistehnoloogiaid lubate."},consentTypes:{necessary:{title:"H\xE4davajalikud",description:"Need k\xFCpsised on veebisaidi n\xF5uetekohaseks toimimiseks h\xE4davajalikud ja neid ei saa keelata."},functionality:{title:"Funktsionaalsus",description:"Need k\xFCpsised v\xF5imaldavad veebisaidi t\xE4iustatud funktsionaalsust ja isikup\xE4rastamist."},marketing:{title:"Turundus",description:"Neid k\xFCpsiseid kasutatakse asjakohaste reklaamide edastamiseks ja nende t\xF5hususe j\xE4lgimiseks."},measurement:{title:"Anal\xFC\xFCtika",description:"Need k\xFCpsised aitavad meil m\xF5ista, kuidas k\xFClastajad veebisaidiga suhtlevad, ja parandada selle toimivust."},experience:{title:"Kogemus",description:"Need k\xFCpsised aitavad meil pakkuda paremat kasutajakogemust ja testida uusi funktsioone."}},frame:{title:"Selle sisu vaatamiseks n\xF5ustuge kategooria {category} n\xF5usolekuga.",actionButton:"Luba kategooria {category} n\xF5usolek"},legalLinks:{privacyPolicy:"Privaatsuspoliitika",cookiePolicy:"K\xFCpsiste poliitika",termsOfService:"Kasutustingimused"},iab:{banner:{title:"Privaatsusseaded",description:"Meie ja meie {partnerCount} partnerit salvestavad ja/v\xF5i p\xE4\xE4sevad ligi teie seadmes olevatele andmetele ning t\xF6\xF6tlevad isikuandmeid, nagu unikaalsed identifikaatorid ja sirvimisandmed sellel veebilehel, et:",partnersLink:"{count} partnerit",andMore:"Ja veel {count}...",legitimateInterestNotice:"M\xF5ned partnerid v\xE4idavad, et neil on \xF5igustatud huvi teie andmete t\xF6\xF6tlemiseks. Teil on \xF5igus sellele t\xF6\xF6tlemisele vastu vaielda, oma valikuid kohandada ja n\xF5usolek igal ajal tagasi v\xF5tta.",scopeServiceSpecific:"Sinu n\xF5usolek kehtib ainult sellele veebisaidile ega m\xF5juta teisi teenuseid.",scopeGroup:"Teie valik kehtib k\xF5igil meie veebisaitidel selles grupis."},preferenceCenter:{title:"Privaatsusseaded",description:"Kohandage siin oma privaatsusseadeid. Saate valida, milliseid k\xFCpsiseid ja j\xE4lgimistehnoloogiaid lubate.",tabs:{purposes:"Eesm\xE4rgid",vendors:"Teenusepakkujad"},purposeItem:{partners:"{count} partnerit",vendorsUseLegitimateInterest:"{count} teenusepakkujat v\xE4idavad \xF5igustatud huvi",examples:"N\xE4ited",partnersUsingPurpose:"Selle eesm\xE4rgi kasutavad partnerid",withYourPermission:"Teie loal",legitimateInterest:"\xD5igustatud huvi",objectButton:"Vaidle vastu",objected:"Vastu vaieldud",rightToObject:"Teil on \xF5igus vaielda vastu t\xF6\xF6tlemisele, mis p\xF5hineb \xF5igustatud huvil."},specialPurposes:{title:"Olulised funktsioonid (n\xF5utud)",tooltip:"Need on vajalikud saidi toimimiseks ja turvalisuseks. IAB TCF-i kohaselt ei saa nendele erieesm\xE4rkidele vastu vaielda."},vendorList:{search:"Otsi teenusepakkujaid...",showingCount:"Kuvatakse {filtered} / {total} teenusepakkujat",iabVendorsHeading:"IAB registreeritud teenusepakkujad",iabVendorsNotice:"Need partnerid on registreeritud IAB l\xE4bipaistvuse ja n\xF5usoleku raamistikus (TCF), mis on t\xF6\xF6stusstandard n\xF5usoleku haldamiseks",customVendorsHeading:"Kohandatud partnerid",customVendorsNotice:"Need on kohandatud partnerid, kes ei ole registreeritud IAB l\xE4bipaistvuse ja n\xF5usoleku raamistikus (TCF). Nad t\xF6\xF6tlevad andmeid teie n\xF5usoleku alusel ning nende privaatsustavad v\xF5ivad erineda IAB-sertifitseeritud partnerite omadest.",purposes:"Eesm\xE4rgid",specialPurposes:"Eriotstarbed",specialFeatures:"Eriomadused",features:"Omadused",dataCategories:"Andmekategooriad",usesCookies:"Kasutab k\xFCpsiseid",nonCookieAccess:"K\xFCpsisteta juurdep\xE4\xE4s",maxAge:"Maksimaalne vanus: {days}p",retention:"S\xE4ilitamine: {days}p",legitimateInterest:"\xD5igustatud huvi",privacyPolicy:"Privaatsuspoliitika",storageDisclosure:"Salvestamise teave",requiredNotice:"Vajalik saidi toimimiseks, ei saa keelata"},footer:{consentStorage:'N\xF5usoleku eelistused salvestatakse k\xFCpsisesse nimega "euconsent-v2" 13 kuuks. The storage duration may be refreshed when you update your preferences.'}},common:{acceptAll:"N\xF5ustu k\xF5igiga",rejectAll:"Keeldu k\xF5igist",customize:"Kohanda",saveSettings:"Salvesta seaded",loading:"Laadimine...",showingSelectedVendor:"Kuvatakse valitud partner",clearSelection:"T\xFChjenda",customPartner:"Kohandatud partner, kes ei ole IAB-s registreeritud"}}},di={common:{acceptAll:"Hyv\xE4ksy kaikki",rejectAll:"Hylk\xE4\xE4 kaikki",customize:"Mukauta",save:"Tallenna asetukset"},cookieBanner:{title:"Arvostamme yksityisyytt\xE4si",description:"T\xE4m\xE4 sivusto k\xE4ytt\xE4\xE4 ev\xE4steit\xE4 parantaakseen selauskokemustasi, analysoidakseen sivuston liikennett\xE4 ja n\xE4ytt\xE4\xE4kseen yksil\xF6llist\xE4 sis\xE4lt\xF6\xE4."},consentManagerDialog:{title:"Tietosuoja-asetukset",description:"Mukauta yksityisyysasetuksiasi t\xE4\xE4ll\xE4. Voit valita, mink\xE4 tyyppiset ev\xE4steet ja seurantatekniikat sallit."},consentTypes:{necessary:{title:"Ehdottoman tarpeellinen",description:"N\xE4m\xE4 ev\xE4steet ovat v\xE4ltt\xE4m\xE4tt\xF6mi\xE4, jotta verkkosivusto toimisi oikein, eik\xE4 niit\xE4 voi poistaa k\xE4yt\xF6st\xE4."},functionality:{title:"Toiminnallisuus",description:"N\xE4m\xE4 ev\xE4steet mahdollistavat verkkosivuston tehostetun toiminnallisuuden ja personoinnin."},marketing:{title:"Markkinointi",description:"N\xE4it\xE4 ev\xE4steit\xE4 k\xE4ytet\xE4\xE4n relevanttien mainosten l\xE4hett\xE4miseen ja niiden tehokkuuden seurantaan."},measurement:{title:"Analytiikka",description:"N\xE4m\xE4 ev\xE4steet auttavat meit\xE4 ymm\xE4rt\xE4m\xE4\xE4n, miten k\xE4vij\xE4t ovat vuorovaikutuksessa verkkosivuston kanssa, ja parantamaan sen suorituskyky\xE4."},experience:{title:"Kokemus",description:"N\xE4m\xE4 ev\xE4steet auttavat meit\xE4 tarjoamaan paremman k\xE4ytt\xF6kokemuksen ja testaamaan uusia ominaisuuksia."}},frame:{title:"Hyv\xE4ksy {category}, jotta voit tarkastella t\xE4t\xE4 sis\xE4lt\xF6\xE4.",actionButton:"Ota {category} k\xE4ytt\xF6\xF6n"},legalLinks:{privacyPolicy:"Tietosuojak\xE4yt\xE4nt\xF6",cookiePolicy:"Ev\xE4stek\xE4yt\xE4nt\xF6",termsOfService:"K\xE4ytt\xF6ehdot"},iab:{banner:{title:"Tietosuoja-asetukset",description:"Me ja {partnerCount} kumppaniamme tallennamme ja/tai k\xE4yt\xE4mme tietoja laitteellasi ja k\xE4sittelemme henkil\xF6tietoja, kuten yksil\xF6llisi\xE4 tunnisteita ja selaustietoja, t\xE4ll\xE4 verkkosivustolla seuraaviin tarkoituksiin:",partnersLink:"{count} kumppania",andMore:"Ja {count} muuta...",legitimateInterestNotice:"Jotkut kumppanit vetoavat oikeutettuun etuun tietojesi k\xE4sittelyss\xE4. Sinulla on oikeus vastustaa t\xE4t\xE4 k\xE4sittely\xE4, mukauttaa valintojasi ja peruuttaa suostumuksesi milloin tahansa.",scopeServiceSpecific:"Suostumuksesi koskee vain t\xE4t\xE4 verkkosivustoa eik\xE4 vaikuta muihin palveluihin.",scopeGroup:"Valintasi koskee kaikkia verkkosivujamme t\xE4ss\xE4 ryhm\xE4ss\xE4."},preferenceCenter:{title:"Tietosuoja-asetukset",description:"Mukauta yksityisyysasetuksiasi t\xE4\xE4ll\xE4. Voit valita, mink\xE4 tyyppiset ev\xE4steet ja seurantatekniikat sallit.",tabs:{purposes:"K\xE4ytt\xF6tarkoitukset",vendors:"Kumppanit"},purposeItem:{partners:"{count} kumppania",vendorsUseLegitimateInterest:"{count} kumppania vetoaa oikeutettuun etuun",examples:"Esimerkit",partnersUsingPurpose:"T\xE4t\xE4 k\xE4ytt\xF6tarkoitusta k\xE4ytt\xE4v\xE4t kumppanit",withYourPermission:"Luvallasi",legitimateInterest:"Oikeutettu etu",objectButton:"Vastusta",objected:"Vastustettu",rightToObject:"Sinulla on oikeus vastustaa oikeutettuun etuun perustuvaa k\xE4sittely\xE4."},specialPurposes:{title:"V\xE4ltt\xE4m\xE4tt\xF6m\xE4t toiminnot (pakollinen)",tooltip:"N\xE4m\xE4 ovat v\xE4ltt\xE4m\xE4tt\xF6mi\xE4 sivuston toimivuuden ja turvallisuuden kannalta. IAB TCF:n mukaan et voi vastustaa n\xE4it\xE4 erityisi\xE4 k\xE4ytt\xF6tarkoituksia."},vendorList:{search:"Hae kumppaneita...",showingCount:"{filtered}/{total} kumppania",iabVendorsHeading:"IAB-rekister\xF6idyt kumppanit",iabVendorsNotice:"N\xE4m\xE4 kumppanit on rekister\xF6ity IAB Transparency & Consent Framework (TCF) -j\xE4rjestelm\xE4\xE4n, joka on alan standardi suostumusten hallintaan",customVendorsHeading:"Mukautetut kumppanit",customVendorsNotice:"N\xE4m\xE4 ovat mukautettuja kumppaneita, jotka eiv\xE4t ole rekister\xF6ityneet IAB Transparency & Consent Framework (TCF) -j\xE4rjestelm\xE4\xE4n. Ne k\xE4sittelev\xE4t tietoja suostumuksesi perusteella, ja niill\xE4 voi olla erilaiset tietosuojak\xE4yt\xE4nn\xF6t kuin IAB:hen rekister\xF6ityneill\xE4 toimittajilla.",purposes:"Tarkoitukset",specialPurposes:"Erityistarkoitukset",specialFeatures:"Erikoisominaisuudet",features:"Ominaisuudet",dataCategories:"Tietoluokat",usesCookies:"K\xE4ytt\xE4\xE4 ev\xE4steit\xE4",nonCookieAccess:"Muu kuin ev\xE4stepohjainen k\xE4ytt\xF6",maxAge:"Enimm\xE4isik\xE4: {days} pv",retention:"S\xE4ilytys: {days} pv",legitimateInterest:"Oikeutettu etu",privacyPolicy:"Tietosuojak\xE4yt\xE4nt\xF6",storageDisclosure:"Tallennustietojen julkistaminen",requiredNotice:"Vaaditaan sivuston toiminnallisuuden vuoksi, ei voi poistaa k\xE4yt\xF6st\xE4"},footer:{consentStorage:'Suostumusasetukset tallennetaan ev\xE4steeseen nimelt\xE4 "euconsent-v2" 13 kuukaudeksi. The storage duration may be refreshed when you update your preferences.'}},common:{acceptAll:"Hyv\xE4ksy kaikki",rejectAll:"Hylk\xE4\xE4 kaikki",customize:"Mukauta",saveSettings:"Tallenna asetukset",loading:"Ladataan...",showingSelectedVendor:"N\xE4ytet\xE4\xE4n valittu toimittaja",clearSelection:"Tyhjenn\xE4",customPartner:"Mukautettu kumppani, joka ei ole rekister\xF6itynyt IAB:hen"}}},pi={common:{acceptAll:"Accepter tout",rejectAll:"Tout rejeter",customize:"Personnaliser",save:"Enregistrer les param\xE8tres"},cookieBanner:{title:"Nous respectons votre vie priv\xE9e",description:"Ce site utilise des cookies pour am\xE9liorer votre exp\xE9rience de navigation, analyser le trafic du site et afficher du contenu personnalis\xE9."},consentManagerDialog:{title:"Param\xE8tres de confidentialit\xE9",description:"Personnalisez vos param\xE8tres de confidentialit\xE9 ici. Vous pouvez choisir les types de cookies et de technologies de suivi que vous autorisez."},consentTypes:{necessary:{title:"Strictement n\xE9cessaire",description:"Ces cookies sont essentiels pour que le site web fonctionne correctement et ne peuvent pas \xEAtre d\xE9sactiv\xE9s."},functionality:{title:"Fonctionnalit\xE9",description:"Ces cookies permettent d'am\xE9liorer la fonctionnalit\xE9 et la personnalisation du site web."},marketing:{title:"Marketing",description:"Ces cookies sont utilis\xE9s pour offrir des publicit\xE9s pertinentes et suivre leur efficacit\xE9."},measurement:{title:"Analyse",description:"Ces cookies nous permettent de comprendre comment les visiteurs interagissent avec le site web et am\xE9liorent ses performances."},experience:{title:"Exp\xE9rience",description:"Ces cookies nous permettent de fournir une meilleure exp\xE9rience utilisateur et de tester de nouvelles fonctionnalit\xE9s."}},frame:{title:"Acceptez {category} pour afficher ce contenu.",actionButton:"Activer le consentement {category}"},legalLinks:{privacyPolicy:"Politique de Confidentialit\xE9",cookiePolicy:"Politique des Cookies",termsOfService:"Conditions de Service"},iab:{banner:{title:"Param\xE8tres de confidentialit\xE9",description:"Nous et nos {partnerCount} partenaires stockons et/ou acc\xE9dons \xE0 des informations sur votre appareil et traitons des donn\xE9es personnelles, telles que des identifiants uniques et des donn\xE9es de navigation, pour ce site web, afin de :",partnersLink:"{count} partenaires",andMore:"Et {count} de plus...",legitimateInterestNotice:"Certains partenaires revendiquent un int\xE9r\xEAt l\xE9gitime pour traiter vos donn\xE9es. Vous avez le droit de vous opposer \xE0 ce traitement, de personnaliser vos choix et de retirer votre consentement \xE0 tout moment.",scopeServiceSpecific:"Votre consentement s'applique uniquement \xE0 ce site web et n'affecte pas d'autres services.",scopeGroup:"Votre choix s'applique \xE0 tous nos sites web de ce groupe."},preferenceCenter:{title:"Param\xE8tres de confidentialit\xE9",description:"Personnalisez vos param\xE8tres de confidentialit\xE9 ici. Vous pouvez choisir les types de cookies et de technologies de suivi que vous autorisez.",tabs:{purposes:"Finalit\xE9s",vendors:"Fournisseurs"},purposeItem:{partners:"{count} partenaires",vendorsUseLegitimateInterest:"{count} fournisseurs revendiquent un int\xE9r\xEAt l\xE9gitime",examples:"Exemples",partnersUsingPurpose:"Partenaires utilisant cette finalit\xE9",withYourPermission:"Avec votre autorisation",legitimateInterest:"Int\xE9r\xEAt l\xE9gitime",objectButton:"S'opposer",objected:"Opposition enregistr\xE9e",rightToObject:"Vous avez le droit de vous opposer au traitement fond\xE9 sur l'int\xE9r\xEAt l\xE9gitime."},specialPurposes:{title:"Fonctions essentielles (obligatoires)",tooltip:"Ces fonctions sont n\xE9cessaires au fonctionnement et \xE0 la s\xE9curit\xE9 du site. Conform\xE9ment au TCF de l'IAB, vous ne pouvez pas vous opposer \xE0 ces finalit\xE9s sp\xE9ciales."},vendorList:{search:"Rechercher des fournisseurs...",showingCount:"{filtered} sur {total} fournisseurs",iabVendorsHeading:"Fournisseurs enregistr\xE9s IAB",iabVendorsNotice:"Ces partenaires sont enregistr\xE9s aupr\xE8s du Transparency & Consent Framework (TCF) de l'IAB, une norme industrielle pour la gestion du consentement",customVendorsHeading:"Partenaires personnalis\xE9s",customVendorsNotice:"Il s'agit de partenaires personnalis\xE9s non enregistr\xE9s aupr\xE8s de l'IAB Transparency & Consent Framework (TCF). Ils traitent les donn\xE9es sur la base de votre consentement et peuvent avoir des pratiques de confidentialit\xE9 diff\xE9rentes de celles des fournisseurs enregistr\xE9s aupr\xE8s de l'IAB.",purposes:"Finalit\xE9s",specialPurposes:"Finalit\xE9s sp\xE9ciales",specialFeatures:"Fonctionnalit\xE9s sp\xE9ciales",features:"Fonctionnalit\xE9s",dataCategories:"Cat\xE9gories de donn\xE9es",usesCookies:"Utilise des cookies",nonCookieAccess:"Acc\xE8s sans cookie",maxAge:"Dur\xE9e max. : {days} j",retention:"R\xE9tention : {days} j",legitimateInterest:"Int\xE9r\xEAt l\xE9gitime",privacyPolicy:"Politique de confidentialit\xE9",storageDisclosure:"Divulgation du stockage",requiredNotice:"Requis pour le fonctionnement du site, ne peut pas \xEAtre d\xE9sactiv\xE9"},footer:{consentStorage:"Les pr\xE9f\xE9rences de consentement sont stock\xE9es dans un cookie nomm\xE9 \xAB euconsent-v2 \xBB pendant 13 mois. The storage duration may be refreshed when you update your preferences."}},common:{acceptAll:"Accepter tout",rejectAll:"Tout rejeter",customize:"Personnaliser",saveSettings:"Enregistrer les param\xE8tres",loading:"Chargement...",showingSelectedVendor:"Affichage du fournisseur s\xE9lectionn\xE9",clearSelection:"Effacer",customPartner:"Partenaire personnalis\xE9 non enregistr\xE9 aupr\xE8s de l'IAB"}}},gi={common:{acceptAll:"Glac le Gach Rud",rejectAll:"Di\xFAltaigh do Gach Rud",customize:"Saincheap",save:"S\xE1bh\xE1il Socruithe"},cookieBanner:{title:"Tugaimid luach do do phr\xEDobh\xE1ideachas",description:"\xDAs\xE1ideann an su\xEDomh seo fian\xE1in chun do thaith\xED bhrabhs\xE1la a fheabhs\xFA, tr\xE1cht su\xEDmh a anail\xEDsi\xFA, agus \xE1bhar pearsantaithe a thaispe\xE1int."},consentManagerDialog:{title:"Socruithe Pr\xEDobh\xE1ideachais",description:"Saincheap do shocruithe pr\xEDobh\xE1ideachais anseo. Is f\xE9idir leat na cine\xE1lacha fian\xE1n agus teicneola\xEDochta\xED rianaithe a cheada\xEDonn t\xFA a roghn\xFA."},consentTypes:{necessary:{title:"F\xEDor-Riachtanach",description:"T\xE1 na fian\xE1in seo riachtanach chun go bhfeidhmeoidh an su\xEDomh gr\xE9as\xE1in i gceart agus n\xED f\xE9idir iad a dh\xEDchumas\xFA."},functionality:{title:"Feidhmi\xFAlacht",description:"Cumasa\xEDonn na fian\xE1in seo feidhmi\xFAlacht fheabhsaithe agus pearsant\xFA an tsu\xEDmh ghr\xE9as\xE1in."},marketing:{title:"Marga\xEDocht",description:"\xDAs\xE1idtear na fian\xE1in seo chun f\xF3gra\xED \xE1bhartha a sheachadadh agus a n-\xE9ifeachtacht a rian\xFA."},measurement:{title:"Anail\xEDs\xEDocht",description:"Cabhra\xEDonn na fian\xE1in seo linn tuiscint a fh\xE1il ar conas a idirghn\xEDomha\xEDonn cuairteoir\xED leis an su\xEDomh gr\xE9as\xE1in agus a fheidhm\xEDocht a fheabhs\xFA."},experience:{title:"Taith\xED",description:"Cabhra\xEDonn na fian\xE1in seo linn taith\xED \xFAs\xE1ideora n\xEDos fearr a shol\xE1thar agus gn\xE9ithe nua a th\xE1st\xE1il."}},frame:{title:"Glac le toili\xFA {category} chun an t-\xE1bhar seo a fheice\xE1il.",actionButton:"Cumasaigh toili\xFA {category}"},legalLinks:{privacyPolicy:"Beartas Pr\xEDobh\xE1ideachta",cookiePolicy:"Beartas Fian\xE1n",termsOfService:"T\xE9arma\xED Seirbh\xEDse"},iab:{banner:{title:"Socruithe pr\xEDobh\xE1ideachais",description:"St\xF3r\xE1laimid agus/n\xF3 faighimid rochtain ar fhaisn\xE9is ar do ghl\xE9as, muid f\xE9in agus \xE1r {partnerCount} comhph\xE1irt\xED, agus pr\xF3ise\xE1laimid sonra\xED pearsanta, amhail aitheant\xF3ir\xED uath\xFAla agus sonra\xED brabhs\xE1la, don su\xEDomh gr\xE9as\xE1in seo, chun:",partnersLink:"{count} comhph\xE1irt\xED",andMore:"Agus {count} eile...",legitimateInterestNotice:"\xC9il\xEDonn roinnt comhph\xE1irtithe leas dlisteanach chun do shonra\xED a phr\xF3ise\xE1il. T\xE1 an ceart agat cur in aghaidh an phr\xF3ise\xE1la seo, do roghanna a shaincheapadh, agus do thoili\xFA a tharraingt siar am ar bith.",scopeServiceSpecific:"Baineann do thoili\xFA leis an su\xEDomh gr\xE9as\xE1in seo amh\xE1in agus n\xED dh\xE9anfaidh s\xE9 difear do sheirbh\xEDs\xED eile.",scopeGroup:"Baineann do rogha le gach ceann d\xE1r l\xE1ithre\xE1in ghr\xE9as\xE1in sa ghr\xFApa seo."},preferenceCenter:{title:"Socruithe pr\xEDobh\xE1ideachais",description:"Saincheap do shocruithe pr\xEDobh\xE1ideachais anseo. Is f\xE9idir leat na cine\xE1lacha fian\xE1n agus teicneola\xEDochta\xED rianaithe a cheada\xEDonn t\xFA a roghn\xFA.",tabs:{purposes:"Cusp\xF3ir\xED",vendors:"Sol\xE1thr\xF3ir\xED"},purposeItem:{partners:"{count} comhph\xE1irt\xED",vendorsUseLegitimateInterest:"\xC9il\xEDonn {count} sol\xE1thr\xF3ir leas dlisteanach",examples:"Sampla\xED",partnersUsingPurpose:"Comhph\xE1irtithe a \xFAs\xE1ideann an cusp\xF3ir seo",withYourPermission:"Le do chead",legitimateInterest:"Leas dlisteanach",objectButton:"Cuir in aghaidh",objected:"Curtha in aghaidh",rightToObject:"T\xE1 an ceart agat cur in aghaidh pr\xF3ise\xE1la bunaithe ar leas dlisteanach."},specialPurposes:{title:"Feidhmeanna riachtanacha (\xE9igeantach)",tooltip:"T\xE1 siad seo riachtanach d'fheidhmi\xFAlacht agus sl\xE1nd\xE1il an tsu\xEDmh. De r\xE9ir IAB TCF, n\xED f\xE9idir leat cur in aghaidh na gcusp\xF3ir\xED speisialta seo."},vendorList:{search:"Cuardaigh sol\xE1thr\xF3ir\xED...",showingCount:"{filtered} as {total} sol\xE1thr\xF3ir",iabVendorsHeading:"Sol\xE1thr\xF3ir\xED cl\xE1raithe IAB",iabVendorsNotice:"T\xE1 na comhph\xE1irtithe seo cl\xE1raithe le Creat Tr\xE9dhearcachta agus Toilithe IAB (TCF), caighde\xE1n tionscail chun toili\xFA a bhainisti\xFA",customVendorsHeading:"Comhph\xE1irtithe saincheaptha",customVendorsNotice:"Is comhph\xE1irtithe saincheaptha iad seo nach bhfuil cl\xE1raithe le Creat Tr\xE9dhearcachta agus Toilithe IAB (TCF). Pr\xF3ise\xE1lann siad sonra\xED bunaithe ar do thoili\xFA agus d'fh\xE9adfadh cleachtais phr\xEDobh\xE1ideachta \xE9ags\xFAla a bheith acu \xF3 dh\xEDolt\xF3ir\xED cl\xE1raithe IAB.",purposes:"Cusp\xF3ir\xED",specialPurposes:"Cusp\xF3ir\xED speisialta",specialFeatures:"Gn\xE9ithe speisialta",features:"Gn\xE9ithe",dataCategories:"Catag\xF3ir\xED sonra\xED",usesCookies:"\xDAs\xE1ideann fian\xE1in",nonCookieAccess:"Rochtain neamh-fhian\xE1n",maxAge:"Uasaois: {days}l",retention:"Coinne\xE1il: {days}l",legitimateInterest:"Leas dlisteanach",privacyPolicy:"Beartas pr\xEDobh\xE1ideachta",storageDisclosure:"Nochtadh st\xF3r\xE1la",requiredNotice:"Riachtanach d'fheidhmi\xFAlacht an tsu\xEDmh, n\xED f\xE9idir \xE9 a dh\xEDchumas\xFA"},footer:{consentStorage:'St\xF3r\xE1iltear roghanna toilithe i bhfian\xE1n darb ainm "euconsent-v2" ar feadh 13 mh\xED. The storage duration may be refreshed when you update your preferences.'}},common:{acceptAll:"Glac le gach rud",rejectAll:"Di\xFAltaigh do gach rud",customize:"Saincheap",saveSettings:"S\xE1bh\xE1il socruithe",loading:"\xC1 l\xF3d\xE1il...",showingSelectedVendor:"D\xEDolt\xF3ir roghnaithe \xE1 thaispe\xE1int",clearSelection:"Glan",customPartner:"Comhph\xE1irt\xED saincheaptha nach bhfuil cl\xE1raithe le IAB"}}},mi={common:{acceptAll:"\u05D0\u05E4\u05E9\u05E8 \u05D4\u05DB\u05DC",rejectAll:"\u05D3\u05D7\u05D4 \u05D4\u05DB\u05DC",customize:"\u05D4\u05EA\u05D0\u05DE\u05D4 \u05D0\u05D9\u05E9\u05D9\u05EA",save:"\u05E9\u05DE\u05D5\u05E8 \u05D4\u05D2\u05D3\u05E8\u05D5\u05EA"},cookieBanner:{title:"\u05E4\u05E8\u05D8\u05D9\u05D5\u05EA\u05DA \u05D7\u05E9\u05D5\u05D1\u05D4 \u05DC\u05E0\u05D5",description:"\u05D0\u05EA\u05E8 \u05D6\u05D4 \u05DE\u05E9\u05EA\u05DE\u05E9 \u05D1\u05E2\u05D5\u05D2\u05D9\u05D5\u05EA (\u05E7\u05D5\u05E7\u05D9\u05D6) \u05D1\u05DB\u05D3\u05D9 \u05DC\u05E9\u05E4\u05E8 \u05D0\u05EA \u05D7\u05D5\u05D5\u05D9\u05D9\u05EA \u05D4\u05E9\u05D9\u05DE\u05D5\u05E9, \u05DC\u05E0\u05D8\u05E8 \u05D0\u05EA \u05EA\u05E2\u05D1\u05D5\u05E8\u05EA \u05D4\u05D0\u05EA\u05E8 \u05D5\u05DC\u05D4\u05E6\u05D9\u05D2 \u05EA\u05D5\u05DB\u05DF \u05DE\u05D5\u05EA\u05D0\u05DD \u05D0\u05D9\u05E9\u05D9\u05EA."},consentManagerDialog:{title:"\u05D4\u05D2\u05D3\u05E8\u05D5\u05EA \u05E4\u05E8\u05D8\u05D9\u05D5\u05EA",description:"\u05D1\u05D7\u05E8 \u05D0\u05EA \u05D4\u05D2\u05D3\u05E8\u05D5\u05EA \u05D4\u05E4\u05E8\u05D8\u05D9\u05D5\u05EA \u05E9\u05DC\u05DA \u05DB\u05D0\u05DF. \u05D1\u05D0\u05E4\u05E9\u05E8\u05D5\u05EA\u05DA \u05DC\u05D1\u05D7\u05D5\u05E8 \u05D0\u05D9\u05DC\u05D5 \u05E1\u05D5\u05D2\u05D9 \u05E2\u05D5\u05D2\u05D9\u05D5\u05EA \u05D5\u05D8\u05DB\u05E0\u05D5\u05DC\u05D5\u05D2\u05D9\u05D5\u05EA \u05DE\u05E2\u05E7\u05D1 \u05EA\u05E8\u05E6\u05D4 \u05DC\u05D0\u05E4\u05E9\u05E8."},consentTypes:{necessary:{title:"\u05D4\u05DB\u05E8\u05D7\u05D9\u05D5\u05EA",description:"\u05E2\u05D5\u05D2\u05D9\u05D5\u05EA \u05D0\u05DC\u05D5 \u05D3\u05E8\u05D5\u05E9\u05D5\u05EA \u05DC\u05E4\u05E2\u05D5\u05DC\u05EA \u05D4\u05D0\u05EA\u05E8 \u05D5\u05DC\u05D0 \u05E0\u05D9\u05EA\u05DF \u05DC\u05D4\u05E9\u05D1\u05D9\u05EA \u05D0\u05D5\u05EA\u05DF."},functionality:{title:"\u05E4\u05D5\u05E0\u05E7\u05E6\u05D9\u05D5\u05E0\u05DC\u05D9\u05D5\u05EA",description:"\u05E2\u05D5\u05D2\u05D9\u05D5\u05EA \u05D0\u05DC\u05D5 \u05DE\u05D0\u05E4\u05E9\u05E8\u05D5\u05EA \u05E4\u05D5\u05E0\u05E7\u05E6\u05D9\u05D5\u05E0\u05DC\u05D9\u05D5\u05EA \u05DE\u05E9\u05D5\u05E4\u05E8\u05EA \u05D5\u05D4\u05EA\u05D0\u05DE\u05D4 \u05D0\u05D9\u05E9\u05D9\u05EA."},marketing:{title:"\u05E9\u05D9\u05D5\u05D5\u05E7",description:"\u05E2\u05D5\u05D2\u05D9\u05D5\u05EA \u05D0\u05DC\u05D5 \u05DE\u05E9\u05DE\u05E9\u05D5\u05EA \u05DC\u05D4\u05EA\u05D0\u05DE\u05EA \u05E4\u05E8\u05E1\u05D5\u05DE\u05D5\u05EA \u05D5\u05DE\u05E2\u05E7\u05D1 \u05D0\u05D7\u05E8 \u05D9\u05E2\u05D9\u05DC\u05D5\u05EA\u05DF."},measurement:{title:"\u05E0\u05D9\u05EA\u05D5\u05D7",description:"\u05E2\u05D5\u05D2\u05D9\u05D5\u05EA \u05D0\u05DC\u05D5 \u05DE\u05E1\u05D9\u05D9\u05E2\u05D5\u05EA \u05DC\u05D4\u05D1\u05D9\u05DF \u05D0\u05D9\u05DA \u05DE\u05E9\u05EA\u05DE\u05E9\u05D9\u05DD \u05D1\u05D0\u05EA\u05E8 \u05D5\u05DC\u05E9\u05E4\u05E8 \u05D0\u05EA \u05D1\u05D9\u05E6\u05D5\u05E2\u05D9\u05D5."},experience:{title:"\u05D7\u05D5\u05D5\u05D9\u05D9\u05EA \u05DE\u05E9\u05EA\u05DE\u05E9",description:"\u05E2\u05D5\u05D2\u05D9\u05D5\u05EA \u05D0\u05DC\u05D5 \u05DE\u05D0\u05E4\u05E9\u05E8\u05D5\u05EA \u05D7\u05D5\u05D5\u05D9\u05D9\u05EA \u05DE\u05E9\u05EA\u05DE\u05E9 \u05D8\u05D5\u05D1\u05D4 \u05D9\u05D5\u05EA\u05E8 \u05D5\u05D1\u05D3\u05D9\u05E7\u05EA \u05E4\u05D5\u05E0\u05E7\u05E6\u05D9\u05D5\u05E0\u05DC\u05D9\u05D5\u05EA \u05D7\u05D3\u05E9\u05D4 \u05D1\u05D0\u05EA\u05E8."}},frame:{title:"\u05E7\u05D1\u05DC {category} \u05DB\u05D3\u05D9 \u05DC\u05D4\u05E6\u05D9\u05D2 \u05EA\u05D5\u05DB\u05DF \u05D6\u05D4.",actionButton:"\u05D4\u05E4\u05E2\u05DC {category} \u05E8\u05E9\u05D5\u05EA"},legalLinks:{privacyPolicy:"\u05DE\u05D3\u05D9\u05E0\u05D9\u05D5\u05EA \u05E4\u05E8\u05D8\u05D9\u05D5\u05EA",cookiePolicy:"\u05DE\u05D3\u05D9\u05E0\u05D9\u05D5\u05EA \u05E2\u05D5\u05D2\u05D9\u05D5\u05EA",termsOfService:"\u05EA\u05E0\u05D0\u05D9 \u05E9\u05D9\u05E8\u05D5\u05EA"},iab:{banner:{title:"\u05D4\u05D2\u05D3\u05E8\u05D5\u05EA \u05E4\u05E8\u05D8\u05D9\u05D5\u05EA",description:"\u05D0\u05E0\u05D7\u05E0\u05D5 \u05D5-{partnerCount} \u05D4\u05E9\u05D5\u05EA\u05E4\u05D9\u05DD \u05E9\u05DC\u05E0\u05D5 \u05DE\u05D0\u05D7\u05E1\u05E0\u05D9\u05DD \u05D5/\u05D0\u05D5 \u05E0\u05D9\u05D2\u05E9\u05D9\u05DD \u05DC\u05DE\u05D9\u05D3\u05E2 \u05D1\u05DE\u05DB\u05E9\u05D9\u05E8 \u05E9\u05DC\u05DA \u05D5\u05DE\u05E2\u05D1\u05D3\u05D9\u05DD \u05E0\u05EA\u05D5\u05E0\u05D9\u05DD \u05D0\u05D9\u05E9\u05D9\u05D9\u05DD, \u05DB\u05D2\u05D5\u05DF \u05DE\u05D6\u05D4\u05D9\u05DD \u05D9\u05D9\u05D7\u05D5\u05D3\u05D9\u05D9\u05DD \u05D5\u05E0\u05EA\u05D5\u05E0\u05D9 \u05D2\u05DC\u05D9\u05E9\u05D4, \u05E2\u05D1\u05D5\u05E8 \u05D0\u05EA\u05E8 \u05D6\u05D4, \u05DB\u05D3\u05D9:",partnersLink:"{count} \u05E9\u05D5\u05EA\u05E4\u05D9\u05DD",andMore:"\u05D5\u05E2\u05D5\u05D3 {count}...",legitimateInterestNotice:"\u05D7\u05DC\u05E7 \u05DE\u05D4\u05E9\u05D5\u05EA\u05E4\u05D9\u05DD \u05D8\u05D5\u05E2\u05E0\u05D9\u05DD \u05DC\u05D0\u05D9\u05E0\u05D8\u05E8\u05E1 \u05DC\u05D2\u05D9\u05D8\u05D9\u05DE\u05D9 \u05DC\u05E2\u05D1\u05D3 \u05D0\u05EA \u05D4\u05E0\u05EA\u05D5\u05E0\u05D9\u05DD \u05E9\u05DC\u05DA. \u05D9\u05E9 \u05DC\u05DA \u05D6\u05DB\u05D5\u05EA \u05DC\u05D4\u05EA\u05E0\u05D2\u05D3 \u05DC\u05E2\u05D9\u05D1\u05D5\u05D3 \u05D6\u05D4, \u05DC\u05D4\u05EA\u05D0\u05D9\u05DD \u05D0\u05D9\u05E9\u05D9\u05EA \u05D0\u05EA \u05D4\u05D1\u05D7\u05D9\u05E8\u05D5\u05EA \u05E9\u05DC\u05DA \u05D5\u05DC\u05D1\u05D8\u05DC \u05D0\u05EA \u05D4\u05E1\u05DB\u05DE\u05EA\u05DA \u05D1\u05DB\u05DC \u05E2\u05EA.",scopeServiceSpecific:"\u05D4\u05D4\u05E1\u05DB\u05DE\u05D4 \u05E9\u05DC\u05DA \u05D7\u05DC\u05D4 \u05E8\u05E7 \u05E2\u05DC \u05D0\u05EA\u05E8 \u05D6\u05D4 \u05D5\u05DC\u05D0 \u05EA\u05E9\u05E4\u05D9\u05E2 \u05E2\u05DC \u05E9\u05D9\u05E8\u05D5\u05EA\u05D9\u05DD \u05D0\u05D7\u05E8\u05D9\u05DD.",scopeGroup:"\u05D4\u05D1\u05D7\u05D9\u05E8\u05D4 \u05E9\u05DC\u05DA \u05D7\u05DC\u05D4 \u05E2\u05DC \u05DB\u05DC \u05D4\u05D0\u05EA\u05E8\u05D9\u05DD \u05E9\u05DC\u05E0\u05D5 \u05D1\u05E7\u05D1\u05D5\u05E6\u05D4 \u05D6\u05D5."},preferenceCenter:{title:"\u05D4\u05D2\u05D3\u05E8\u05D5\u05EA \u05E4\u05E8\u05D8\u05D9\u05D5\u05EA",description:"\u05D4\u05EA\u05D0\u05DD \u05D0\u05D9\u05E9\u05D9\u05EA \u05D0\u05EA \u05D4\u05D2\u05D3\u05E8\u05D5\u05EA \u05D4\u05E4\u05E8\u05D8\u05D9\u05D5\u05EA \u05E9\u05DC\u05DA \u05DB\u05D0\u05DF. \u05D1\u05D0\u05E4\u05E9\u05E8\u05D5\u05EA\u05DA \u05DC\u05D1\u05D7\u05D5\u05E8 \u05D0\u05D9\u05DC\u05D5 \u05E1\u05D5\u05D2\u05D9 \u05E2\u05D5\u05D2\u05D9\u05D5\u05EA \u05D5\u05D8\u05DB\u05E0\u05D5\u05DC\u05D5\u05D2\u05D9\u05D5\u05EA \u05DE\u05E2\u05E7\u05D1 \u05EA\u05E8\u05E6\u05D4 \u05DC\u05D0\u05E4\u05E9\u05E8.",tabs:{purposes:"\u05DE\u05D8\u05E8\u05D5\u05EA",vendors:"\u05E1\u05E4\u05E7\u05D9\u05DD"},purposeItem:{partners:"{count} \u05E9\u05D5\u05EA\u05E4\u05D9\u05DD",vendorsUseLegitimateInterest:"{count} \u05E1\u05E4\u05E7\u05D9\u05DD \u05D8\u05D5\u05E2\u05E0\u05D9\u05DD \u05DC\u05D0\u05D9\u05E0\u05D8\u05E8\u05E1 \u05DC\u05D2\u05D9\u05D8\u05D9\u05DE\u05D9",examples:"\u05D3\u05D5\u05D2\u05DE\u05D0\u05D5\u05EA",partnersUsingPurpose:"\u05E9\u05D5\u05EA\u05E4\u05D9\u05DD \u05D4\u05DE\u05E9\u05EA\u05DE\u05E9\u05D9\u05DD \u05D1\u05DE\u05D8\u05E8\u05D4 \u05D6\u05D5",withYourPermission:"\u05D1\u05D4\u05E1\u05DB\u05DE\u05EA\u05DA",legitimateInterest:"\u05D0\u05D9\u05E0\u05D8\u05E8\u05E1 \u05DC\u05D2\u05D9\u05D8\u05D9\u05DE\u05D9",objectButton:"\u05D4\u05EA\u05E0\u05D2\u05D3",objected:"\u05D4\u05EA\u05E0\u05D2\u05D3\u05EA",rightToObject:"\u05D9\u05E9 \u05DC\u05DA \u05D6\u05DB\u05D5\u05EA \u05DC\u05D4\u05EA\u05E0\u05D2\u05D3 \u05DC\u05E2\u05D9\u05D1\u05D5\u05D3 \u05D4\u05DE\u05D1\u05D5\u05E1\u05E1 \u05E2\u05DC \u05D0\u05D9\u05E0\u05D8\u05E8\u05E1 \u05DC\u05D2\u05D9\u05D8\u05D9\u05DE\u05D9."},specialPurposes:{title:"\u05E4\u05D5\u05E0\u05E7\u05E6\u05D9\u05D5\u05EA \u05D7\u05D9\u05D5\u05E0\u05D9\u05D5\u05EA (\u05E0\u05D3\u05E8\u05E9)",tooltip:"\u05D0\u05DC\u05D5 \u05E0\u05D3\u05E8\u05E9\u05D5\u05EA \u05DC\u05EA\u05E4\u05E7\u05D5\u05D3 \u05D5\u05D0\u05D1\u05D8\u05D7\u05EA \u05D4\u05D0\u05EA\u05E8. \u05E2\u05DC \u05E4\u05D9 IAB TCF, \u05D0\u05D9\u05E0\u05DA \u05D9\u05DB\u05D5\u05DC \u05DC\u05D4\u05EA\u05E0\u05D2\u05D3 \u05DC\u05DE\u05D8\u05E8\u05D5\u05EA \u05DE\u05D9\u05D5\u05D7\u05D3\u05D5\u05EA \u05D0\u05DC\u05D5."},vendorList:{search:"\u05D7\u05E4\u05E9 \u05E1\u05E4\u05E7\u05D9\u05DD...",showingCount:"{filtered} \u05DE\u05EA\u05D5\u05DA {total} \u05E1\u05E4\u05E7\u05D9\u05DD",iabVendorsHeading:"\u05E1\u05E4\u05E7\u05D9\u05DD \u05E8\u05E9\u05D5\u05DE\u05D9\u05DD \u05D1-IAB",iabVendorsNotice:"\u05E9\u05D5\u05EA\u05E4\u05D9\u05DD \u05D0\u05DC\u05D5 \u05E8\u05E9\u05D5\u05DE\u05D9\u05DD \u05D1\u05DE\u05E1\u05D2\u05E8\u05EA \u05D4\u05E9\u05E7\u05D9\u05E4\u05D5\u05EA \u05D5\u05D4\u05D4\u05E1\u05DB\u05DE\u05D4 \u05E9\u05DC IAB (TCF), \u05EA\u05E7\u05DF \u05EA\u05E2\u05E9\u05D9\u05D9\u05EA\u05D9 \u05DC\u05E0\u05D9\u05D4\u05D5\u05DC \u05D4\u05E1\u05DB\u05DE\u05D4",customVendorsHeading:"\u05E9\u05D5\u05EA\u05E4\u05D9\u05DD \u05DE\u05D5\u05EA\u05D0\u05DE\u05D9\u05DD \u05D0\u05D9\u05E9\u05D9\u05EA",customVendorsNotice:"\u05D0\u05DC\u05D5 \u05D4\u05DD \u05E9\u05D5\u05EA\u05E4\u05D9\u05DD \u05DE\u05D5\u05EA\u05D0\u05DE\u05D9\u05DD \u05D0\u05D9\u05E9\u05D9\u05EA \u05E9\u05D0\u05D9\u05E0\u05DD \u05E8\u05E9\u05D5\u05DE\u05D9\u05DD \u05D1-IAB Transparency & Consent Framework (TCF). \u05D4\u05DD \u05DE\u05E2\u05D1\u05D3\u05D9\u05DD \u05E0\u05EA\u05D5\u05E0\u05D9\u05DD \u05E2\u05DC \u05D1\u05E1\u05D9\u05E1 \u05D4\u05E1\u05DB\u05DE\u05EA\u05DA \u05D5\u05E2\u05E9\u05D5\u05D9\u05D9\u05DD \u05DC\u05D4\u05D9\u05D5\u05EA \u05DC\u05D4\u05DD \u05E0\u05D4\u05DC\u05D9 \u05E4\u05E8\u05D8\u05D9\u05D5\u05EA \u05E9\u05D5\u05E0\u05D9\u05DD \u05DE\u05E9\u05D5\u05EA\u05E4\u05D9\u05DD \u05D4\u05E8\u05E9\u05D5\u05DE\u05D9\u05DD \u05D1-IAB.",purposes:"\u05DE\u05D8\u05E8\u05D5\u05EA",specialPurposes:"\u05DE\u05D8\u05E8\u05D5\u05EA \u05DE\u05D9\u05D5\u05D7\u05D3\u05D5\u05EA",specialFeatures:"\u05EA\u05DB\u05D5\u05E0\u05D5\u05EA \u05DE\u05D9\u05D5\u05D7\u05D3\u05D5\u05EA",features:"\u05EA\u05DB\u05D5\u05E0\u05D5\u05EA",dataCategories:"\u05E7\u05D8\u05D2\u05D5\u05E8\u05D9\u05D5\u05EA \u05E0\u05EA\u05D5\u05E0\u05D9\u05DD",usesCookies:"\u05DE\u05E9\u05EA\u05DE\u05E9 \u05D1\u05E2\u05D5\u05D2\u05D9\u05D5\u05EA",nonCookieAccess:"\u05D2\u05D9\u05E9\u05D4 \u05DC\u05DC\u05D0 \u05E2\u05D5\u05D2\u05D9\u05D5\u05EA",maxAge:"\u05EA\u05D5\u05E7\u05E3 \u05DE\u05E7\u05E1\u05D9\u05DE\u05DC\u05D9: {days} \u05D9\u05DE\u05D9\u05DD",retention:"\u05E9\u05DE\u05D9\u05E8\u05D4: {days} \u05D9\u05DE\u05D9\u05DD",legitimateInterest:"\u05D0\u05D9\u05E0\u05D8\u05E8\u05E1 \u05DC\u05D2\u05D9\u05D8\u05D9\u05DE\u05D9",privacyPolicy:"\u05DE\u05D3\u05D9\u05E0\u05D9\u05D5\u05EA \u05E4\u05E8\u05D8\u05D9\u05D5\u05EA",storageDisclosure:"\u05D2\u05D9\u05DC\u05D5\u05D9 \u05D0\u05D7\u05E1\u05D5\u05DF",requiredNotice:"\u05E0\u05D3\u05E8\u05E9 \u05DC\u05EA\u05E4\u05E2\u05D5\u05DC \u05D4\u05D0\u05EA\u05E8, \u05DC\u05D0 \u05E0\u05D9\u05EA\u05DF \u05DC\u05D4\u05E9\u05D1\u05D9\u05EA"},footer:{consentStorage:'\u05D4\u05E2\u05D3\u05E4\u05D5\u05EA \u05D4\u05E1\u05DB\u05DE\u05D4 \u05E0\u05E9\u05DE\u05E8\u05D5\u05EA \u05D1\u05E2\u05D5\u05D2\u05D9\u05D9\u05D4 \u05D1\u05E9\u05DD "euconsent-v2" \u05DC\u05DE\u05E9\u05DA 13 \u05D7\u05D5\u05D3\u05E9\u05D9\u05DD. The storage duration may be refreshed when you update your preferences.'}},common:{acceptAll:"\u05D0\u05E4\u05E9\u05E8 \u05D4\u05DB\u05DC",rejectAll:"\u05D3\u05D7\u05D4 \u05D4\u05DB\u05DC",customize:"\u05D4\u05EA\u05D0\u05DE\u05D4 \u05D0\u05D9\u05E9\u05D9\u05EA",saveSettings:"\u05E9\u05DE\u05D5\u05E8 \u05D4\u05D2\u05D3\u05E8\u05D5\u05EA",loading:"\u05D8\u05D5\u05E2\u05DF...",showingSelectedVendor:"\u05DE\u05E6\u05D9\u05D2 \u05E9\u05D5\u05EA\u05E3 \u05E0\u05D1\u05D7\u05E8",clearSelection:"\u05E0\u05E7\u05D4",customPartner:"\u05E9\u05D5\u05EA\u05E3 \u05DE\u05D5\u05EA\u05D0\u05DD \u05D0\u05D9\u05E9\u05D9\u05EA \u05E9\u05D0\u05D9\u05E0\u05D5 \u05E8\u05E9\u05D5\u05DD \u05D1-IAB"}}},fi={common:{acceptAll:"Prihvati sve",rejectAll:"Odbij sve",customize:"Prilagodi",save:"Spremi postavke"},cookieBanner:{title:"Cijenimo va\u0161u privatnost",description:"Ova stranica koristi kola\u010Di\u0107e za pobolj\u0161anje va\u0161eg iskustva pregledavanja, analizu prometa na stranici i prikaz personaliziranog sadr\u017Eaja."},consentManagerDialog:{title:"Postavke privatnosti",description:"Ovdje mo\u017Eete prilagoditi svoje postavke privatnosti. Mo\u017Eete odabrati koje vrste kola\u010Di\u0107a i tehnologija pra\u0107enja dopu\u0161tate."},consentTypes:{necessary:{title:"Strogo nu\u017Eno",description:"Ovi kola\u010Di\u0107i su klju\u010Dni za ispravno funkcioniranje web stranice i ne mogu se onemogu\u0107iti."},functionality:{title:"Funkcionalnost",description:"Ovi kola\u010Di\u0107i omogu\u0107uju pobolj\u0161anu funkcionalnost i personalizaciju web stranice."},marketing:{title:"Marketing",description:"Ovi kola\u010Di\u0107i se koriste za prikaz relevantnih oglasa i pra\u0107enje njihove u\u010Dinkovitosti."},measurement:{title:"Analitika",description:"Ovi kola\u010Di\u0107i nam poma\u017Eu razumjeti kako posjetitelji koriste web stranicu i pobolj\u0161ati njezine performanse."},experience:{title:"Iskustvo",description:"Ovi kola\u010Di\u0107i nam poma\u017Eu pru\u017Eiti bolje korisni\u010Dko iskustvo i testirati nove zna\u010Dajke."}},frame:{title:"Prihvatite {category} privolu za prikaz ovog sadr\u017Eaja.",actionButton:"Omogu\u0107i {category} privolu"},legalLinks:{privacyPolicy:"Pravila o privatnosti",cookiePolicy:"Pravila o kola\u010Di\u0107ima",termsOfService:"Uvjeti pru\u017Eanja usluge"},iab:{banner:{title:"Postavke privatnosti",description:"Mi i na\u0161ih {partnerCount} partnera pohranjujemo i/ili pristupamo informacijama na va\u0161em ure\u0111aju i obra\u0111ujemo osobne podatke, kao \u0161to su jedinstveni identifikatori i podaci o pregledavanju, za ovu web stranicu, kako bismo:",partnersLink:"{count} partnera",andMore:"I jo\u0161 {count}...",legitimateInterestNotice:"Neki partneri pola\u017Eu pravo na legitimni interes za obradu va\u0161ih podataka. Imate pravo prigovora na ovu obradu, prilagodbe svojih izbora i povla\u010Denja privole u bilo kojem trenutku.",scopeServiceSpecific:"Va\u0161 pristanak odnosi se samo na ovu web stranicu i ne\u0107e utjecati na druge usluge.",scopeGroup:"Va\u0161 izbor vrijedi za sve na\u0161e web stranice u ovoj grupi."},preferenceCenter:{title:"Postavke privatnosti",description:"Ovdje mo\u017Eete prilagoditi svoje postavke privatnosti. Mo\u017Eete odabrati koje vrste kola\u010Di\u0107a i tehnologija pra\u0107enja dopu\u0161tate.",tabs:{purposes:"Svrhe",vendors:"Prodava\u010Di"},purposeItem:{partners:"{count} partnera",vendorsUseLegitimateInterest:"{count} prodava\u010Da pola\u017Ee pravo na legitimni interes",examples:"Primjeri",partnersUsingPurpose:"Partneri koji koriste ovu svrhu",withYourPermission:"Uz va\u0161e dopu\u0161tenje",legitimateInterest:"Legitimni interes",objectButton:"Prigovori",objected:"Prigovoreno",rightToObject:"Imate pravo prigovora na obradu temeljenu na legitimnom interesu."},specialPurposes:{title:"Osnovne funkcije (obavezno)",tooltip:"Ove su funkcije potrebne za funkcionalnost i sigurnost stranice. Prema IAB TCF-u, ne mo\u017Eete ulo\u017Eiti prigovor na ove posebne svrhe."},vendorList:{search:"Pretra\u017Ei prodava\u010De...",showingCount:"{filtered} od {total} prodava\u010Da",iabVendorsHeading:"IAB registrirani prodava\u010Di",iabVendorsNotice:"Ovi partneri su registrirani u IAB Transparency & Consent Framework (TCF), industrijskom standardu za upravljanje privolama",customVendorsHeading:"Prilago\u0111eni partneri",customVendorsNotice:"Ovo su prilago\u0111eni partneri koji nisu registrirani u IAB Transparency & Consent Framework (TCF). Oni obra\u0111uju podatke na temelju va\u0161e privole i mogu imati druga\u010Dije prakse privatnosti od IAB registriranih prodava\u010Da.",purposes:"Svrhe",specialPurposes:"Posebne svrhe",specialFeatures:"Posebne zna\u010Dajke",features:"Zna\u010Dajke",dataCategories:"Kategorije podataka",usesCookies:"Koristi kola\u010Di\u0107e",nonCookieAccess:"Pristup bez kola\u010Di\u0107a",maxAge:"Maks. starost: {days}d",retention:"Zadr\u017Eavanje: {days}d",legitimateInterest:"Leg. interes",privacyPolicy:"Pravila o privatnosti",storageDisclosure:"Objavljivanje pohrane",requiredNotice:"Potrebno za funkcionalnost stranice, ne mo\u017Ee se onemogu\u0107iti"},footer:{consentStorage:'Postavke privole pohranjuju se u kola\u010Di\u0107u pod nazivom "euconsent-v2" tijekom 13 mjeseci. The storage duration may be refreshed when you update your preferences.'}},common:{acceptAll:"Prihvati sve",rejectAll:"Odbij sve",customize:"Prilagodi",saveSettings:"Spremi postavke",loading:"U\u010Ditavanje...",showingSelectedVendor:"Prikaz odabranog prodava\u010Da",clearSelection:"O\u010Disti",customPartner:"Prilago\u0111eni partner koji nije registriran u IAB-u"}}},hi={common:{acceptAll:"\xD6sszes elfogad\xE1sa",rejectAll:"\xD6sszes elutas\xEDt\xE1sa",customize:"Testreszab\xE1s",save:"Be\xE1ll\xEDt\xE1sok ment\xE9se"},cookieBanner:{title:"\xC9rt\xE9kelj\xFCk az adatv\xE9delmet",description:"Ez a webhely s\xFCtiket haszn\xE1l a b\xF6ng\xE9sz\xE9si \xE9lm\xE9ny jav\xEDt\xE1s\xE1ra, a forgalom elemz\xE9s\xE9re \xE9s szem\xE9lyre szabott tartalom megjelen\xEDt\xE9s\xE9re."},consentManagerDialog:{title:"Adatv\xE9delmi be\xE1ll\xEDt\xE1sok",description:"Testreszabhatja adatv\xE9delmi be\xE1ll\xEDt\xE1sait itt. Kiv\xE1laszthatja, hogy milyen t\xEDpus\xFA s\xFCtiket \xE9s nyomk\xF6vet\u0151 technol\xF3gi\xE1kat enged\xE9lyez."},consentTypes:{necessary:{title:"Felt\xE9tlen\xFCl sz\xFCks\xE9ges",description:"Ezek a s\xFCtik elengedhetetlenek a weboldal megfelel\u0151 m\u0171k\xF6d\xE9s\xE9hez, \xE9s nem kapcsolhat\xF3k ki."},functionality:{title:"Funkcionalit\xE1s",description:"Ezek a s\xFCtik lehet\u0151v\xE9 teszik a weboldal tov\xE1bbfejlesztett funkci\xF3it \xE9s szem\xE9lyre szab\xE1s\xE1t."},marketing:{title:"Marketing",description:"Ezeket a s\xFCtiket relev\xE1ns hirdet\xE9sek megjelen\xEDt\xE9s\xE9re \xE9s hat\xE9konys\xE1guk nyomon k\xF6vet\xE9s\xE9re haszn\xE1ljuk."},measurement:{title:"Analitika",description:"Ezek a s\xFCtik seg\xEDtenek meg\xE9rteni, hogyan l\xE9pnek kapcsolatba a l\xE1togat\xF3k a weboldallal, \xE9s jav\xEDtj\xE1k annak teljes\xEDtm\xE9ny\xE9t."},experience:{title:"Felhaszn\xE1l\xF3i \xE9lm\xE9ny",description:"Ezek a s\xFCtik seg\xEDtenek jobb felhaszn\xE1l\xF3i \xE9lm\xE9nyt ny\xFAjtani \xE9s \xFAj funkci\xF3kat tesztelni."}},frame:{title:"Fogadja el a(z) {category} hozz\xE1j\xE1rul\xE1st a tartalom megtekint\xE9s\xE9hez.",actionButton:"A(z) {category} hozz\xE1j\xE1rul\xE1s enged\xE9lyez\xE9se"},legalLinks:{privacyPolicy:"Adatv\xE9delmi szab\xE1lyzat",cookiePolicy:"S\xFCti szab\xE1lyzat",termsOfService:"Felhaszn\xE1l\xE1si felt\xE9telek"},iab:{banner:{title:"Adatv\xE9delmi be\xE1ll\xEDt\xE1sok",description:"Mi \xE9s a(z) {partnerCount} partner\xFCnk inform\xE1ci\xF3kat t\xE1rolunk az \xD6n eszk\xF6z\xE9n \xE9s/vagy \xE9r\xFCnk el azokhoz, valamint szem\xE9lyes adatokat, p\xE9ld\xE1ul egyedi azonos\xEDt\xF3kat \xE9s b\xF6ng\xE9sz\xE9si adatokat dolgozunk fel ezen a weboldalon a k\xF6vetkez\u0151 c\xE9lokb\xF3l:",partnersLink:"{count} partner",andMore:"\xC9s m\xE9g {count}...",legitimateInterestNotice:"N\xE9h\xE1ny partner jogos \xE9rdekre hivatkozik az \xD6n adatainak feldolgoz\xE1s\xE1hoz. \xD6nnek joga van tiltakozni ez ellen a feldolgoz\xE1s ellen, testreszabni v\xE1laszt\xE1sait, \xE9s b\xE1rmikor visszavonni hozz\xE1j\xE1rul\xE1s\xE1t.",scopeServiceSpecific:"Az \xD6n hozz\xE1j\xE1rul\xE1sa csak erre a webhelyre vonatkozik, \xE9s nem \xE9rinti m\xE1s szolg\xE1ltat\xE1sokat.",scopeGroup:"A v\xE1laszt\xE1sa az ebben a csoportban l\xE9v\u0151 \xF6sszes weboldalunkra vonatkozik."},preferenceCenter:{title:"Adatv\xE9delmi be\xE1ll\xEDt\xE1sok",description:"Testreszabhatja adatv\xE9delmi be\xE1ll\xEDt\xE1sait itt. Kiv\xE1laszthatja, hogy milyen t\xEDpus\xFA s\xFCtiket \xE9s nyomk\xF6vet\u0151 technol\xF3gi\xE1kat enged\xE9lyez.",tabs:{purposes:"C\xE9lok",vendors:"Szolg\xE1ltat\xF3k"},purposeItem:{partners:"{count} partner",vendorsUseLegitimateInterest:"{count} szolg\xE1ltat\xF3 jogos \xE9rdekre hivatkozik",examples:"P\xE9ld\xE1k",partnersUsingPurpose:"Ezt a c\xE9lt haszn\xE1l\xF3 partnerek",withYourPermission:"Az \xD6n enged\xE9ly\xE9vel",legitimateInterest:"Jogos \xE9rdek",objectButton:"Tiltakoz\xE1s",objected:"Tiltakozott",rightToObject:"\xD6nnek joga van tiltakozni a jogos \xE9rdeken alapul\xF3 adatkezel\xE9s ellen."},specialPurposes:{title:"Alapvet\u0151 funkci\xF3k (sz\xFCks\xE9ges)",tooltip:"Ezek a webhely m\u0171k\xF6d\xE9s\xE9hez \xE9s biztons\xE1g\xE1hoz sz\xFCks\xE9gesek. Az IAB TCF szerint \xD6n nem tiltakozhat ezen k\xFCl\xF6nleges c\xE9lok ellen."},vendorList:{search:"Szolg\xE1ltat\xF3k keres\xE9se...",showingCount:"{total} szolg\xE1ltat\xF3b\xF3l {filtered} megjelen\xEDt\xE9se",iabVendorsHeading:"IAB regisztr\xE1lt szolg\xE1ltat\xF3k",iabVendorsNotice:"Ezek a partnerek regisztr\xE1lva vannak az IAB Transparency & Consent Framework (TCF) rendszer\xE9ben, amely a hozz\xE1j\xE1rul\xE1sok kezel\xE9s\xE9nek ipar\xE1gi szabv\xE1nya",customVendorsHeading:"Egyedi partnerek",customVendorsNotice:"Ezek olyan egyedi partnerek, akik nincsenek regisztr\xE1lva az IAB Transparency & Consent Framework (TCF) rendszer\xE9ben. Az \xD6n hozz\xE1j\xE1rul\xE1sa alapj\xE1n kezelik az adatokat, \xE9s az IAB-regisztr\xE1lt szolg\xE1ltat\xF3kt\xF3l elt\xE9r\u0151 adatv\xE9delmi gyakorlatot folytathatnak.",purposes:"C\xE9lok",specialPurposes:"K\xFCl\xF6nleges c\xE9lok",specialFeatures:"K\xFCl\xF6nleges funkci\xF3k",features:"Funkci\xF3k",dataCategories:"Adatkateg\xF3ri\xE1k",usesCookies:"S\xFCtiket haszn\xE1l",nonCookieAccess:"Nem s\xFCti alap\xFA hozz\xE1f\xE9r\xE9s",maxAge:"Max. \xE9lettartam: {days} nap",retention:"Meg\u0151rz\xE9s: {days} nap",legitimateInterest:"Jogos \xE9rdek",privacyPolicy:"Adatv\xE9delmi szab\xE1lyzat",storageDisclosure:"T\xE1rol\xE1si t\xE1j\xE9koztat\xF3",requiredNotice:"A webhely m\u0171k\xF6d\xE9s\xE9hez sz\xFCks\xE9ges, nem kapcsolhat\xF3 ki"},footer:{consentStorage:'A hozz\xE1j\xE1rul\xE1si be\xE1ll\xEDt\xE1sokat egy "euconsent-v2" nev\u0171 s\xFCtiben t\xE1roljuk 13 h\xF3napig. The storage duration may be refreshed when you update your preferences.'}},common:{acceptAll:"\xD6sszes elfogad\xE1sa",rejectAll:"\xD6sszes elutas\xEDt\xE1sa",customize:"Testreszab\xE1s",saveSettings:"Be\xE1ll\xEDt\xE1sok ment\xE9se",loading:"Bet\xF6lt\xE9s...",showingSelectedVendor:"A kiv\xE1lasztott szolg\xE1ltat\xF3 megjelen\xEDt\xE9se",clearSelection:"T\xF6rl\xE9s",customPartner:"IAB-n k\xEDv\xFCli egyedi partner"}}},ki={common:{acceptAll:"Terima Semua",rejectAll:"Tolak Semua",customize:"Sesuaikan",save:"Simpan Pengaturan"},cookieBanner:{title:"Kami menghargai privasi Anda",description:"Situs ini menggunakan cookie untuk meningkatkan pengalaman penelusuran Anda, menganalisis lalu lintas situs, dan menampilkan konten yang dipersonalisasi."},consentManagerDialog:{title:"Pengaturan Privasi",description:"Atur preferensi privasi Anda di sini. Anda dapat memilih jenis cookie dan teknologi pelacakan yang diizinkan."},consentTypes:{necessary:{title:"Sangat Diperlukan",description:"Cookie ini penting agar situs web dapat berfungsi dengan baik dan tidak dapat dinonaktifkan."},functionality:{title:"Fungsionalitas",description:"Cookie ini memungkinkan peningkatan fungsionalitas dan personalisasi situs web."},marketing:{title:"Pemasaran",description:"Cookie ini digunakan untuk menampilkan iklan yang relevan dan melacak efektivitasnya."},measurement:{title:"Analitik",description:"Cookie ini membantu kami memahami bagaimana pengunjung berinteraksi dengan situs web dan meningkatkan kinerjanya."},experience:{title:"Pengalaman",description:"Cookie ini membantu kami memberikan pengalaman pengguna yang lebih baik dan menguji fitur baru."}},frame:{title:"Setujui {category} untuk melihat konten ini.",actionButton:"Aktifkan persetujuan {category}"},legalLinks:{privacyPolicy:"Kebijakan Privasi",cookiePolicy:"Kebijakan Cookie",termsOfService:"Syarat Layanan"},iab:{banner:{title:"Pengaturan Privasi",description:"Kami dan {partnerCount} mitra kami menyimpan dan/atau mengakses informasi pada perangkat Anda dan memproses data pribadi, seperti pengidentifikasi unik dan data penelusuran, untuk situs web ini, untuk:",partnersLink:"{count} mitra",andMore:"Dan {count} lainnya...",legitimateInterestNotice:"Beberapa mitra mengklaim kepentingan sah untuk memproses data Anda. Anda memiliki hak untuk menolak pemrosesan ini, menyesuaikan pilihan Anda, dan menarik persetujuan Anda kapan saja.",scopeServiceSpecific:"Persetujuan Anda hanya berlaku untuk situs web ini dan tidak memengaruhi layanan lainnya.",scopeGroup:"Pilihan Anda berlaku untuk semua situs web kami dalam grup ini."},preferenceCenter:{title:"Pengaturan Privasi",description:"Atur preferensi privasi Anda di sini. Anda dapat memilih jenis cookie dan teknologi pelacakan yang diizinkan.",tabs:{purposes:"Tujuan",vendors:"Vendor"},purposeItem:{partners:"{count} mitra",vendorsUseLegitimateInterest:"{count} vendor mengklaim kepentingan sah",examples:"Contoh",partnersUsingPurpose:"Mitra yang Menggunakan Tujuan Ini",withYourPermission:"Dengan Izin Anda",legitimateInterest:"Kepentingan Sah",objectButton:"Keberatan",objected:"Ditolak",rightToObject:"Anda memiliki hak untuk menolak pemrosesan berdasarkan kepentingan sah."},specialPurposes:{title:"Fungsi Penting (Wajib)",tooltip:"Ini diperlukan untuk fungsionalitas dan keamanan situs. Per IAB TCF, Anda tidak dapat menolak tujuan khusus ini."},vendorList:{search:"Cari vendor...",showingCount:"{filtered} dari {total} vendor",iabVendorsHeading:"Vendor Terdaftar IAB",iabVendorsNotice:"Mitra-mitra ini terdaftar di IAB Transparency & Consent Framework (TCF), standar industri untuk mengelola persetujuan",customVendorsHeading:"Mitra Kustom",customVendorsNotice:"Ini adalah mitra kustom yang tidak terdaftar di IAB Transparency & Consent Framework (TCF). Mereka memproses data berdasarkan persetujuan Anda dan mungkin memiliki praktik privasi yang berbeda dari vendor terdaftar IAB.",purposes:"Tujuan",specialPurposes:"Tujuan Khusus",specialFeatures:"Fitur Khusus",features:"Fitur",dataCategories:"Kategori Data",usesCookies:"Menggunakan Cookie",nonCookieAccess:"Akses Non-Cookie",maxAge:"Usia Maks: {days}h",retention:"Retensi: {days}h",legitimateInterest:"Kepent. Sah",privacyPolicy:"Kebijakan Privasi",storageDisclosure:"Pengungkapan Penyimpanan",requiredNotice:"Diperlukan untuk fungsionalitas situs, tidak dapat dinonaktifkan"},footer:{consentStorage:'Preferensi persetujuan disimpan dalam cookie bernama "euconsent-v2" selama 13 bulan. The storage duration may be refreshed when you update your preferences.'}},common:{acceptAll:"Terima Semua",rejectAll:"Tolak Semua",customize:"Sesuaikan",saveSettings:"Simpan Pengaturan",loading:"Memuat...",showingSelectedVendor:"Menampilkan vendor terpilih",clearSelection:"Bersihkan",customPartner:"Mitra kustom tidak terdaftar di IAB"}}},vi={common:{acceptAll:"Sam\xFEykkja allt",rejectAll:"Hafna \xF6llu",customize:"S\xE9rsn\xED\xF0a",save:"Vista stillingar"},cookieBanner:{title:"Vi\xF0 metum fri\xF0helgi \xFE\xEDna",description:"\xDEessi vefur notar vafrak\xF6kur til a\xF0 b\xE6ta vafraupplifun \xFE\xEDna, greina umfer\xF0 \xE1 vefnum og s\xFDna pers\xF3numi\xF0a\xF0 efni."},consentManagerDialog:{title:"Pers\xF3nuverndastillingar",description:"S\xE9rsn\xED\xF0a\xF0u pers\xF3nuverndastillingar \xFE\xEDnar h\xE9r. \xDE\xFA getur vali\xF0 hva\xF0a tegundir af vafrak\xF6kum og rakningart\xE6kni \xFE\xFA leyfir."},consentTypes:{necessary:{title:"Nau\xF0synlegar",description:"\xDEessar vafrak\xF6kur eru nau\xF0synlegar til a\xF0 vefs\xED\xF0an virki r\xE9tt og ekki er h\xE6gt a\xF0 sl\xF6kkva \xE1 \xFEeim."},functionality:{title:"Virkni",description:"\xDEessar vafrak\xF6kur gera m\xF6gulegt a\xF0 auka virkni og pers\xF3numi\xF0a vefs\xED\xF0una."},marketing:{title:"Marka\xF0ssetning",description:"\xDEessar vafrak\xF6kur eru nota\xF0ar til a\xF0 birta vi\xF0eigandi augl\xFDsingar og fylgjast me\xF0 \xE1rangri \xFEeirra."},measurement:{title:"Greining",description:"\xDEessar vafrak\xF6kur hj\xE1lpa okkur a\xF0 skilja hvernig gestir nota vefs\xED\xF0una og b\xE6ta frammist\xF6\xF0u hennar."},experience:{title:"Upplifun",description:"\xDEessar vafrak\xF6kur hj\xE1lpa okkur a\xF0 veita betri notendaupplifun og pr\xF3fa n\xFDja eiginleika."}},frame:{title:"Sam\xFEykktu {category} sam\xFEykki til a\xF0 sko\xF0a \xFEetta efni.",actionButton:"Virkja {category} sam\xFEykki"},legalLinks:{privacyPolicy:"Pers\xF3nuverndarstefna",cookiePolicy:"Stefna um vafrak\xF6kur",termsOfService:"\xDEj\xF3nustuskilm\xE1lar"},iab:{banner:{title:"Pers\xF3nuverndastillingar",description:"Vi\xF0 og {partnerCount} samstarfsa\xF0ilar okkar geymum og/e\xF0a h\xF6fum a\xF0gang a\xF0 uppl\xFDsingum \xE1 t\xE6kinu \xFE\xEDnu og vinnum pers\xF3nuuppl\xFDsingar, svo sem einst\xF6k au\xF0kenni og vafrauppl\xFDsingar, fyrir \xFEessa vefs\xED\xF0u, til a\xF0:",partnersLink:"{count} samstarfsa\xF0ilar",andMore:"Og {count} til vi\xF0b\xF3tar...",legitimateInterestNotice:"Sumir samstarfsa\xF0ilar krefjast l\xF6gm\xE6tra hagsmuna til a\xF0 vinna g\xF6gnin \xFE\xEDn. \xDE\xFA \xE1tt r\xE9tt \xE1 a\xF0 andm\xE6la \xFEessari vinnslu, s\xE9rsn\xED\xF0a val \xFEitt og draga sam\xFEykki \xFEitt til baka hven\xE6r sem er.",scopeServiceSpecific:"Sam\xFEykki \xFEitt gildir a\xF0eins fyrir \xFEessa vefs\xED\xF0u og hefur ekki \xE1hrif \xE1 a\xF0rar \xFEj\xF3nustur.",scopeGroup:"Val \xFEitt gildir \xE1 \xF6llum vefs\xED\xF0um okkar \xED \xFEessum h\xF3p."},preferenceCenter:{title:"Pers\xF3nuverndastillingar",description:"S\xE9rsn\xED\xF0a\xF0u pers\xF3nuverndastillingar \xFE\xEDnar h\xE9r. \xDE\xFA getur vali\xF0 hva\xF0a tegundir af vafrak\xF6kum og rakningart\xE6kni \xFE\xFA leyfir.",tabs:{purposes:"Tilgangur",vendors:"S\xF6lua\xF0ilar"},purposeItem:{partners:"{count} samstarfsa\xF0ilar",vendorsUseLegitimateInterest:"{count} s\xF6lua\xF0ilar krefjast l\xF6gm\xE6tra hagsmuna",examples:"D\xE6mi",partnersUsingPurpose:"Samstarfsa\xF0ilar sem nota \xFEennan tilgang",withYourPermission:"Me\xF0 \xFE\xEDnu leyfi",legitimateInterest:"L\xF6gm\xE6tir hagsmunir",objectButton:"Andm\xE6la",objected:"Andm\xE6lt",rightToObject:"\xDE\xFA \xE1tt r\xE9tt \xE1 a\xF0 andm\xE6la vinnslu sem byggir \xE1 l\xF6gm\xE6tum hagsmunum."},specialPurposes:{title:"Nau\xF0synleg virkni (krafist)",tooltip:"\xDEetta er nau\xF0synlegt fyrir virkni og \xF6ryggi vefsins. Samkv\xE6mt IAB TCF getur\xF0u ekki andm\xE6lt \xFEessum s\xE9rst\xF6ku markmi\xF0um."},vendorList:{search:"Leita a\xF0 s\xF6lua\xF0ilum...",showingCount:"{filtered} af {total} s\xF6lua\xF0ilum",iabVendorsHeading:"IAB skr\xE1\xF0ir s\xF6lua\xF0ilar",iabVendorsNotice:"\xDEessir samstarfsa\xF0ilar eru skr\xE1\xF0ir hj\xE1 IAB Transparency & Consent Framework (TCF), i\xF0na\xF0arsta\xF0li til a\xF0 stj\xF3rna sam\xFEykki",customVendorsHeading:"S\xE9rsni\xF0nir samstarfsa\xF0ilar",customVendorsNotice:"\xDEetta eru s\xE9rsni\xF0nir samstarfsa\xF0ilar sem eru ekki skr\xE1\xF0ir hj\xE1 IAB Transparency & Consent Framework (TCF). \xDEeir vinna g\xF6gn byggt \xE1 sam\xFEykki \xFE\xEDnu og g\xE6tu haft a\xF0rar pers\xF3nuverndarreglur en IAB-skr\xE1\xF0ir s\xF6lua\xF0ilar.",purposes:"Tilgangur",specialPurposes:"S\xE9rstakur tilgangur",specialFeatures:"S\xE9rstakir eiginleikar",features:"Eiginleikar",dataCategories:"Gagnaflokkar",usesCookies:"Notar vafrak\xF6kur",nonCookieAccess:"A\xF0gangur \xE1n vafrakaka",maxAge:"H\xE1marksaldur: {days}d",retention:"Var\xF0veisla: {days}d",legitimateInterest:"L\xF6gm. hagsmunir",privacyPolicy:"Pers\xF3nuverndarstefna",storageDisclosure:"Uppl\xFDsingar um geymslu",requiredNotice:"Nau\xF0synlegt fyrir virkni vefsins, ekki h\xE6gt a\xF0 sl\xF6kkva \xE1"},footer:{consentStorage:'Sam\xFEykkisstillingar eru geymdar \xED vafrak\xF6ku sem heitir "euconsent-v2" \xED 13 m\xE1nu\xF0i. The storage duration may be refreshed when you update your preferences.'}},common:{acceptAll:"Sam\xFEykkja allt",rejectAll:"Hafna \xF6llu",customize:"S\xE9rsn\xED\xF0a",saveSettings:"Vista stillingar",loading:"Hle\xF0ur...",showingSelectedVendor:"S\xFDnir valdan s\xF6lua\xF0ila",clearSelection:"Hreinsa",customPartner:"S\xE9rsni\xF0inn samstarfsa\xF0ili ekki skr\xE1\xF0ur hj\xE1 IAB"}}},yi={common:{acceptAll:"Accetta tutto",rejectAll:"Rifiuta tutto",customize:"Personalizza",save:"Salva impostazioni"},cookieBanner:{title:"Rispettiamo la tua privacy",description:"Questo sito utilizza cookies per migliorare la tua esperienza di navigazione, analizzare il traffico e mostrare contenuti personalizzati."},consentManagerDialog:{title:"Impostazioni di privacy",description:"Personalizza le tue impostazioni di privacy. Puoi scegliere i tipi di cookies e tecnologie di tracciamento che autorizzi."},consentTypes:{necessary:{title:"Strettamente necessari",description:"Questi cookies sono essenziali per il sito web per funzionare correttamente e non possono essere disabilitati."},functionality:{title:"Funzionalit\xE0",description:"Questi cookies permettono di migliorare la funzionalit\xE0 e la personalizzazione del sito web."},marketing:{title:"Marketing",description:"Questi cookies sono utilizzati per fornire pubblicit\xE0 pertinenti e misurare la loro efficacia."},measurement:{title:"Misurazione",description:"Questi cookies ci aiutano a comprendere come i visitatori interagiscano con il sito web per migliorarne le sue prestazioni."},experience:{title:"Esperienza",description:"Questi cookies ci aiutano a fornire una migliore esperienza utente e per testare nuove funzionalit\xE0."}},frame:{title:"Accetta {category} per visualizzare questo contenuto",actionButton:"Abilita consenso {category}"},legalLinks:{privacyPolicy:"Informativa sulla Privacy",cookiePolicy:"Politica sui Cookie",termsOfService:"Termini di Servizio"},iab:{banner:{title:"Impostazioni di privacy",description:"Noi e i nostri {partnerCount} partner archiviamo e/o accediamo a informazioni su un dispositivo e trattiamo dati personali, come identificatori univoci e informazioni di navigazione, per questo sito web, per:",partnersLink:"{count} partner",andMore:"E altri {count}...",legitimateInterestNotice:"Alcuni partner rivendicano un interesse legittimo per trattare i tuoi dati. Hai il diritto di opporti a questo trattamento, personalizzare le tue scelte e revocare il tuo consenso in qualsiasi momento.",scopeServiceSpecific:"Il tuo consenso si applica solo a questo sito web e non influisce su altri servizi.",scopeGroup:"La tua scelta si applica a tutti i nostri siti web di questo gruppo."},preferenceCenter:{title:"Impostazioni di privacy",description:"Personalizza le tue impostazioni di privacy. Puoi scegliere i tipi di cookies e tecnologie di tracciamento che autorizzi.",tabs:{purposes:"Finalit\xE0",vendors:"Fornitori"},purposeItem:{partners:"{count} partner",vendorsUseLegitimateInterest:"{count} fornitori rivendicano un interesse legittimo",examples:"Esempi",partnersUsingPurpose:"Partner che utilizzano questa finalit\xE0",withYourPermission:"Con la tua autorizzazione",legitimateInterest:"Interesse legittimo",objectButton:"Opponiti",objected:"Opposizione registrata",rightToObject:"Hai il diritto di opporti al trattamento basato sull\u2019interesse legittimo."},specialPurposes:{title:"Funzioni essenziali (obbligatorie)",tooltip:"Queste sono necessarie per la funzionalit\xE0 e la sicurezza del sito. Secondo l\u2019IAB TCF, non puoi opporti a queste finalit\xE0 speciali."},vendorList:{search:"Cerca fornitori...",showingCount:"{filtered} di {total} fornitori",iabVendorsHeading:"Fornitori registrati IAB",iabVendorsNotice:"Questi partner sono registrati presso l\u2019IAB Transparency & Consent Framework (TCF), uno standard industriale per la gestione del consenso",customVendorsHeading:"Partner personalizzati",customVendorsNotice:"Si tratta di partner personalizzati non registrati presso l\u2019IAB Transparency & Consent Framework (TCF). Trattano i dati sulla base del tuo consenso e possono avere pratiche di privacy diverse rispetto ai fornitori registrati IAB.",purposes:"Finalit\xE0",specialPurposes:"Finalit\xE0 speciali",specialFeatures:"Funzionalit\xE0 speciali",features:"Funzionalit\xE0",dataCategories:"Categorie di dati",usesCookies:"Utilizza cookie",nonCookieAccess:"Accesso senza cookie",maxAge:"Durata massima: {days}g",retention:"Conservazione: {days}g",legitimateInterest:"Int. legittimo",privacyPolicy:"Informativa sulla privacy",storageDisclosure:"Informativa sull\u2019archiviazione",requiredNotice:"Richiesto per la funzionalit\xE0 del sito, non pu\xF2 essere disabilitato"},footer:{consentStorage:'Le preferenze di consenso vengono memorizzate in un cookie denominato "euconsent-v2" per 13 mesi. The storage duration may be refreshed when you update your preferences.'}},common:{acceptAll:"Accetta tutto",rejectAll:"Rifiuta tutto",customize:"Personalizza",saveSettings:"Salva impostazioni",loading:"Caricamento...",showingSelectedVendor:"Visualizzazione del fornitore selezionato",clearSelection:"Cancella",customPartner:"Partner personalizzato non registrato presso l\u2019IAB"}}},bi={common:{acceptAll:"All akzept\xE9ieren",rejectAll:"All refus\xE9ieren",customize:"Upassen",save:"Astellunge sp\xE4icheren"},cookieBanner:{title:"Mir sch\xE4tzen \xC4r Privatsph\xE4r",description:"D\xEBs Webs\xE4it benotzt Cookien fir \xC4r Surferfahrung ze verbesseren, Webs\xE4it-Verk\xE9ier ze analys\xE9ieren an personalis\xE9ierten Inhalt unzebidden."},consentManagerDialog:{title:"Privatsph\xE4r Astellungen",description:"Passt \xC4r Privatsph\xE4r Astellungen hei un. Dir k\xEBnnt wielen w\xE9i eng Zorte vu Cookien an Tracking-Technologien Dir erlaabt."},consentTypes:{necessary:{title:"Strikt n\xE9ideg",description:"D\xEBs Cookien si wesentlech fir datt d'Webs\xE4it richteg funktion\xE9iert a k\xEBnnen net desaktiv\xE9iert ginn."},functionality:{title:"Funktionalit\xE9it",description:"D\xEBs Cookien erm\xE9iglechen erweidert Funktionalit\xE9it a Personalis\xE9ierung vun der Webs\xE4it."},marketing:{title:"Marketing",description:"D\xEBs Cookien ginn benotzt fir relevant Reklammen ze liwweren an hir Wierksamkeet ze verfolgen."},measurement:{title:"Analytik",description:"D\xEBs Cookien h\xEBllefen eis ze verstoen w\xE9i d'Besicher mat der Webs\xE4it interag\xE9ieren an hir Leeschtung verbesseren."},experience:{title:"Erfahrung",description:"D\xEBs Cookien h\xEBllefen eis eng besser Benotzererfabrung ze bidden an nei Funktiounen ze testen."}},frame:{title:"Akzept\xE9iert {category} Zoust\xEBmmung fir d\xEBsen Inhalt ze gesinn.",actionButton:"{category} Zoust\xEBmmung aktiv\xE9ieren"},legalLinks:{privacyPolicy:"Dateschutzrichtlinn",cookiePolicy:"Cookie-Politik",termsOfService:"Notzungsbedingungen"},iab:{banner:{title:"Privatsph\xE4r Astellungen",description:"Mir an eis {partnerCount} Partner sp\xE4icheren an/oder gr\xE4ifen op Informatiounen op \xC4rem Apparat zou a veraarbechten pers\xE9inlech Daten, w\xE9i eenzegaarteg Identifiz\xE9ierer a Browserdaten, fir d\xEBs Webs\xE4it, fir:",partnersLink:"{count} Partner",andMore:"An nach {count}...",legitimateInterestNotice:"E puer Partner behaapten e berechtegten Interessi fir \xC4r Daten ze veraarbechten. Dir hutt d\u2019Recht g\xE9int d\xEBs Veraarbechtung ze protest\xE9ieren, \xC4r Wiel unzepassen an \xC4r Zoust\xEBmmung zu all Moment zr\xE9ckzez\xE9ien.",scopeServiceSpecific:"\xC4r Zoust\xEBmmung g\xEBllt n\xEBmme fir d\xEBs Webs\xE4it a w\xE4ert aner Servicer net beaflossen.",scopeGroup:"\xC4r Auswiel g\xEBllt fir all eis Webs\xE4iten an d\xEBser Grupp."},preferenceCenter:{title:"Privatsph\xE4r Astellungen",description:"Passt \xC4r Privatsph\xE4r Astellungen hei un. Dir k\xEBnnt wielen w\xE9i eng Zorte vu Cookien an Tracking-Technologien Dir erlaabt.",tabs:{purposes:"Zwecker",vendors:"Ubidder"},purposeItem:{partners:"{count} Partner",vendorsUseLegitimateInterest:"{count} Ubidder behaapten berechtegten Interessi",examples:"Beispiller",partnersUsingPurpose:"Partner d\xE9i d\xEBsen Zweck benotzen",withYourPermission:"Mat \xC4rer Erlaabnis",legitimateInterest:"Berechtegten Interessi",objectButton:"Protest\xE9ieren",objected:"Protest\xE9iert",rightToObject:"Dir hutt d\u2019Recht g\xE9int d\u2019Veraarbechtung op Basis vu berechtegten Interessi ze protest\xE9ieren."},specialPurposes:{title:"Wichteg Funktiounen (erfuerderlech)",tooltip:"D\xEBs sinn erfuerderlech fir d\u2019Funktionalit\xE9it an d\u2019S\xE9cherheet vum Site. Gem\xE9iss IAB TCF k\xEBnnt Dir net g\xE9int d\xEBs speziell Zwecker protest\xE9ieren."},vendorList:{search:"Ubidder sichen...",showingCount:"{filtered} vun {total} Ubidder",iabVendorsHeading:"IAB registr\xE9iert Ubidder",iabVendorsNotice:"D\xEBs Partner sinn am IAB Transparency & Consent Framework (TCF) registr\xE9iert, en Industriestandard fir d\u2019Gestioun vun der Zoust\xEBmmung",customVendorsHeading:"Benotzerdefin\xE9iert Partner",customVendorsNotice:"D\xEBst si benotzerdefin\xE9iert Partner d\xE9i net am IAB Transparency & Consent Framework (TCF) registr\xE9iert sinn. Si veraarbechten Daten op Basis vun \xC4rer Zoust\xEBmmung a k\xEBnnen aner Dateschutzpraktiken hunn w\xE9i IAB-registr\xE9iert Ubidder.",purposes:"Zwecker",specialPurposes:"Speziell Zwecker",specialFeatures:"Speziell Fonctiounen",features:"Fonctiounen",dataCategories:"Datekategorien",usesCookies:"Benotzt Cookien",nonCookieAccess:"Net-Cookie-Zougang",maxAge:"Max Alter: {days}d",retention:"Bewaaren: {days}d",legitimateInterest:"Ber. Interessi",privacyPolicy:"Dateschutzrichtlinn",storageDisclosure:"Sp\xE4icher-Offenlegung",requiredNotice:"Erfuerderlech fir d\u2019Funktionalit\xE9it vum Site, kann net desaktiv\xE9iert ginn"},footer:{consentStorage:'Zoust\xEBmmungsvirl\xE9iften ginn an engem Cookie mam Numm "euconsent-v2" fir 13 M\xE9int gesp\xE4ichert. The storage duration may be refreshed when you update your preferences.'}},common:{acceptAll:"All akzept\xE9ieren",rejectAll:"All refus\xE9ieren",customize:"Upassen",saveSettings:"Astellunge sp\xE4icheren",loading:"Lueden...",showingSelectedVendor:"Gewielten Ubider g\xEBtt ugewisen",clearSelection:"L\xE4schen",customPartner:"Benotzerdefin\xE9ierte Partner net am IAB registr\xE9iert"}}},wi={common:{acceptAll:"Priimti visus",rejectAll:"Atmesti visus",customize:"Tinkinti",save:"I\u0161saugoti nustatymus"},cookieBanner:{title:"Mes vertiname j\u016Bs\u0173 privatum\u0105",description:"\u0160i svetain\u0117 naudoja slapukus nar\u0161ymo patir\u010Diai gerinti, svetain\u0117s srautui analizuoti ir rodyti jums pritaikyt\u0105 turin\u012F."},consentManagerDialog:{title:"Privatumo nustatymai",description:"\u010Cia galite tinkinti savo privatumo nustatymus. Galite pasirinkti, koki\u0173 tip\u0173 slapukus ir sekimo technologijas leid\u017Eiate naudoti."},consentTypes:{necessary:{title:"B\u016Btinieji",description:"\u0160ie slapukai yra b\u016Btini tinkamam svetain\u0117s veikimui ir negali b\u016Bti i\u0161jungti."},functionality:{title:"Funkcionalumo",description:"\u0160ie slapukai \u012Fgalina i\u0161pl\u0117stin\u012F funkcionalum\u0105 ir svetain\u0117s personalizavim\u0105."},marketing:{title:"Rinkodaros",description:"\u0160ie slapukai naudojami pateikti aktualius skelbimus ir sekti j\u0173 efektyvum\u0105."},measurement:{title:"Analitikos",description:"\u0160ie slapukai padeda mums suprasti, kaip lankytojai s\u0105veikauja su svetaine, ir pagerinti jos veikim\u0105."},experience:{title:"Patirties",description:"\u0160ie slapukai padeda mums u\u017Etikrinti geresn\u0119 vartotojo patirt\u012F ir i\u0161bandyti naujas funkcijas."}},frame:{title:"Priimkite {category} sutikim\u0105, kad gal\u0117tum\u0117te per\u017Ei\u016Br\u0117ti \u0161\u012F turin\u012F.",actionButton:"\u012Egalinti {category} sutikim\u0105"},legalLinks:{privacyPolicy:"Privatumo politika",cookiePolicy:"Slapuk\u0173 politika",termsOfService:"Naudojimosi s\u0105lygos"},iab:{banner:{title:"Privatumo nustatymai",description:"Mes ir m\u016Bs\u0173 {partnerCount} partneriai saugome ir (arba) pasiekiame informacij\u0105 j\u016Bs\u0173 \u012Frenginyje ir tvarkome asmens duomenis, tokius kaip unikal\u016Bs identifikatoriai ir nar\u0161ymo duomenys, \u0161ioje svetain\u0117je, kad gal\u0117tume:",partnersLink:"{count} partneriai",andMore:"Ir dar {count}...",legitimateInterestNotice:"Kai kurie partneriai teigia turintys teis\u0117t\u0105 interes\u0105 tvarkyti j\u016Bs\u0173 duomenis. J\u016Bs turite teis\u0119 nesutikti su tokiu tvarkymu, tinkinti savo pasirinkimus ir bet kada at\u0161aukti sutikim\u0105.",scopeServiceSpecific:"J\u016Bs\u0173 sutikimas taikomas tik \u0161iai svetainei ir netur\u0117s \u012Ftakos kitoms paslaugoms.",scopeGroup:"J\u016Bs\u0173 pasirinkimas taikomas visoms m\u016Bs\u0173 svetain\u0117ms \u0161ioje grup\u0117je."},preferenceCenter:{title:"Privatumo nustatymai",description:"\u010Cia galite tinkinti savo privatumo nustatymus. Galite pasirinkti, koki\u0173 tip\u0173 slapukus ir sekimo technologijas leid\u017Eiate naudoti.",tabs:{purposes:"Tikslai",vendors:"Tiek\u0117jai"},purposeItem:{partners:"{count} partneriai",vendorsUseLegitimateInterest:"{count} tiek\u0117jai teigia turintys teis\u0117t\u0105 interes\u0105",examples:"Pavyzd\u017Eiai",partnersUsingPurpose:"Partneriai, naudojantys \u0161\u012F tiksl\u0105",withYourPermission:"Su j\u016Bs\u0173 leidimu",legitimateInterest:"Teis\u0117tas interesas",objectButton:"Nesutikti",objected:"Prie\u0161tarauta",rightToObject:"J\u016Bs turite teis\u0119 nesutikti su tvarkymu, pagr\u012Fstu teis\u0117tu interesu."},specialPurposes:{title:"Esmin\u0117s funkcijos (privaloma)",tooltip:"Jos reikalingos svetain\u0117s funkcionalumui ir saugumui u\u017Etikrinti. Pagal IAB TCF negalite nesutikti su \u0161iais specialiais tikslais."},vendorList:{search:"Ie\u0161koti tiek\u0117j\u0173...",showingCount:"Rodoma {filtered} i\u0161 {total} tiek\u0117j\u0173",iabVendorsHeading:"IAB registruoti tiek\u0117jai",iabVendorsNotice:"\u0160ie partneriai yra u\u017Eregistruoti IAB Transparency & Consent Framework (TCF) \u2013 pramon\u0117s standarte, skirtame sutikim\u0173 valdymui",customVendorsHeading:"Pasirinktiniai partneriai",customVendorsNotice:"Tai yra pasirinktiniai partneriai, kurie n\u0117ra u\u017Eregistruoti IAB Transparency & Consent Framework (TCF). Jie tvarko duomenis remdamiesi j\u016Bs\u0173 sutikimu ir gali taikyti kitoki\u0105 privatumo praktik\u0105 nei IAB registruoti tiek\u0117jai.",purposes:"Tikslai",specialPurposes:"Special\u016Bs tikslai",specialFeatures:"Specialios funkcijos",features:"Funkcijos",dataCategories:"Duomen\u0173 kategorijos",usesCookies:"Naudoja slapukus",nonCookieAccess:"Prieiga be slapuk\u0173",maxAge:"Maks. am\u017Eius: {days}d",retention:"Saugojimas: {days}d",legitimateInterest:"Teis\u0117tas int.",privacyPolicy:"Privatumo politika",storageDisclosure:"Informacija apie saugojim\u0105",requiredNotice:"Reikalinga svetain\u0117s funkcionalumui, negalima i\u0161jungti"},footer:{consentStorage:"Sutikimo nuostatos saugomos slapuke pavadinimu \u201Eeuconsent-v2\u201C 13 m\u0117nesi\u0173. The storage duration may be refreshed when you update your preferences."}},common:{acceptAll:"Priimti visus",rejectAll:"Atmesti visus",customize:"Tinkinti",saveSettings:"I\u0161saugoti nustatymus",loading:"\u012Ekeliama...",showingSelectedVendor:"Rodomas pasirinktas tiek\u0117jas",clearSelection:"I\u0161valyti",customPartner:"Pasirinktinis partneris, ne\u012Fregistruotas IAB"}}},Ci={common:{acceptAll:"Pie\u0146emt visu",rejectAll:"Noraid\u012Bt visu",customize:"Piel\u0101got",save:"Saglab\u0101t iestat\u012Bjumus"},cookieBanner:{title:"M\u0113s nov\u0113rt\u0113jam j\u016Bsu priv\u0101tumu",description:"\u0160\u012B vietne izmanto s\u012Bkdatnes, lai uzlabotu j\u016Bsu p\u0101rl\u016Bko\u0161anas pieredzi, analiz\u0113tu vietnes datpl\u016Bsmu un r\u0101d\u012Btu personaliz\u0113tu saturu."},consentManagerDialog:{title:"Priv\u0101tuma iestat\u012Bjumi",description:"Piel\u0101gojiet savus priv\u0101tuma iestat\u012Bjumus \u0161eit. J\u016Bs varat izv\u0113l\u0113ties, k\u0101da veida s\u012Bkdatnes un izseko\u0161anas tehnolo\u0123ijas at\u013Caut."},consentTypes:{necessary:{title:"Stingri nepiecie\u0161am\u0101s",description:"\u0160\u012Bs s\u012Bkdatnes ir b\u016Btiskas, lai vietne darbotos pareizi, un t\u0101s nevar atsp\u0113jot."},functionality:{title:"Funkcionalit\u0101te",description:"\u0160\u012Bs s\u012Bkdatnes nodro\u0161ina uzlabotu funkcionalit\u0101ti un vietnes personaliz\u0101ciju."},marketing:{title:"M\u0101rketings",description:"\u0160\u012Bs s\u012Bkdatnes tiek izmantotas, lai pieg\u0101d\u0101tu atbilsto\u0161as rekl\u0101mas un izsekotu to efektivit\u0101ti."},measurement:{title:"Anal\u012Btika",description:"\u0160\u012Bs s\u012Bkdatnes pal\u012Bdz mums saprast, k\u0101 apmekl\u0113t\u0101ji mijiedarbojas ar vietni un uzlabo t\u0101s veiktsp\u0113ju."},experience:{title:"Pieredze",description:"\u0160\u012Bs s\u012Bkdatnes pal\u012Bdz mums nodro\u0161in\u0101t lab\u0101ku lietot\u0101ja pieredzi un test\u0113t jaunas funkcijas."}},frame:{title:"Pie\u0146emiet {category} piekri\u0161anu, lai skat\u012Btu \u0161o saturu.",actionButton:"Iesp\u0113jot {category} piekri\u0161anu"},legalLinks:{privacyPolicy:"Priv\u0101tuma politika",cookiePolicy:"S\u012Bkdat\u0146u politika",termsOfService:"Pakalpojumu snieg\u0161anas noteikumi"},iab:{banner:{title:"Priv\u0101tuma iestat\u012Bjumi",description:"M\u0113s un m\u016Bsu {partnerCount} partneri uzglab\u0101jam un/vai piek\u013C\u016Bstam inform\u0101cijai j\u016Bsu ier\u012Bc\u0113 un apstr\u0101d\u0101jam personas datus, piem\u0113ram, unik\u0101lus identifikatorus un p\u0101rl\u016Bko\u0161anas datus, \u0161ai vietnei, lai:",partnersLink:"{count} partneri",andMore:"Un v\u0113l {count}...",legitimateInterestNotice:"Da\u017Ei partneri pieprasa le\u0123it\u012Bmas intereses j\u016Bsu datu apstr\u0101dei. Jums ir ties\u012Bbas iebilst pret \u0161o apstr\u0101di, piel\u0101got savu izv\u0113li un jebkur\u0101 laik\u0101 atsaukt savu piekri\u0161anu.",scopeServiceSpecific:"J\u016Bsu piekri\u0161ana attiecas tikai uz \u0161o vietni un neietekm\u0113s citus pakalpojumus.",scopeGroup:"J\u016Bsu izv\u0113le attiecas uz vis\u0101m m\u016Bsu vietn\u0113m \u0161aj\u0101 grup\u0101."},preferenceCenter:{title:"Priv\u0101tuma iestat\u012Bjumi",description:"Piel\u0101gojiet savus priv\u0101tuma iestat\u012Bjumus \u0161eit. J\u016Bs varat izv\u0113l\u0113ties, k\u0101da veida s\u012Bkdatnes un izseko\u0161anas tehnolo\u0123ijas at\u013Caut.",tabs:{purposes:"M\u0113r\u0137i",vendors:"Pieg\u0101d\u0101t\u0101ji"},purposeItem:{partners:"{count} partneri",vendorsUseLegitimateInterest:"{count} pieg\u0101d\u0101t\u0101ji pieprasa le\u0123it\u012Bmas intereses",examples:"Piem\u0113ri",partnersUsingPurpose:"Partneri, kas izmanto \u0161o m\u0113r\u0137i",withYourPermission:"Ar j\u016Bsu at\u013Cauju",legitimateInterest:"Le\u0123it\u012Bm\u0101s intereses",objectButton:"Iebilst",objected:"Iebilsts",rightToObject:"Jums ir ties\u012Bbas iebilst pret apstr\u0101di, kuras pamat\u0101 ir le\u0123it\u012Bmas intereses."},specialPurposes:{title:"B\u016Btiskas funkcijas (nepiecie\u0161ams)",tooltip:"T\u0101s ir nepiecie\u0161amas vietnes funkcionalit\u0101tei un dro\u0161\u012Bbai. Saska\u0146\u0101 ar IAB TCF j\u016Bs nevarat iebilst pret \u0161iem \u012Bpa\u0161ajiem m\u0113r\u0137iem."},vendorList:{search:"Mekl\u0113t pieg\u0101d\u0101t\u0101jus...",showingCount:"R\u0101da {filtered} no {total} pieg\u0101d\u0101t\u0101jiem",iabVendorsHeading:"IAB re\u0123istr\u0113tie pieg\u0101d\u0101t\u0101ji",iabVendorsNotice:"\u0160ie partneri ir re\u0123istr\u0113ti IAB Transparency & Consent Framework (TCF) \u2014 nozares standart\u0101 piekri\u0161anas p\u0101rvald\u012Bbai",customVendorsHeading:"Piel\u0101goti partneri",customVendorsNotice:"\u0160ie ir piel\u0101goti partneri, kas nav re\u0123istr\u0113ti IAB Transparency & Consent Framework (TCF). Vi\u0146i apstr\u0101d\u0101 datus, pamatojoties auf j\u016Bsu piekri\u0161anu, un vi\u0146iem var b\u016Bt at\u0161\u0137ir\u012Bga priv\u0101tuma prakse nek\u0101 IAB re\u0123istr\u0113tajiem pieg\u0101d\u0101t\u0101jiem.",purposes:"M\u0113r\u0137i",specialPurposes:"\u012Apa\u0161ie m\u0113r\u0137i",specialFeatures:"\u012Apa\u0161\u0101s funkcijas",features:"Funkcijas",dataCategories:"Datu kategorijas",usesCookies:"Izmanto s\u012Bkdatnes",nonCookieAccess:"Piek\u013Cuve bez s\u012Bkdatn\u0113m",maxAge:"Maks. vecums: {days}d",retention:"Saglab\u0101\u0161ana: {days}d",legitimateInterest:"Le\u0123. intereses",privacyPolicy:"Priv\u0101tuma politika",storageDisclosure:"Inform\u0101cija par glab\u0101\u0161anu",requiredNotice:"Nepiecie\u0161ams vietnes funkcionalit\u0101tei, nevar atsp\u0113jot"},footer:{consentStorage:'Piekri\u0161anas preferences tiek glab\u0101tas s\u012Bkdatn\u0113 ar nosaukumu "euconsent-v2" 13 m\u0113ne\u0161us. The storage duration may be refreshed when you update your preferences.'}},common:{acceptAll:"Pie\u0146emt visu",rejectAll:"Noraid\u012Bt visu",customize:"Piel\u0101got",saveSettings:"Saglab\u0101t iestat\u012Bjumus",loading:"Iel\u0101d\u0113...",showingSelectedVendor:"R\u0101da atlas\u012Bto pieg\u0101d\u0101t\u0101ju",clearSelection:"Not\u012Br\u012Bt",customPartner:"Piel\u0101gots partneris, kas nav re\u0123istr\u0113ts IAB"}}},Ii={common:{acceptAll:"A\u010B\u010Betta kollox",rejectAll:"Irrifjuta kollox",customize:"Personalizza",save:"Issejvja s-settings"},cookieBanner:{title:"Napprezzaw il-privatezza tieg\u0127ek",description:"Dan is-sit ju\u017Ca cookies biex itejjeb l-esperjenza tal-browsing tieg\u0127ek, janalizza t-traffiku tas-sit, u juri kontenut personalizzat."},consentManagerDialog:{title:"Settings tal-privatezza",description:"Personalizza s-settings tal-privatezza tieg\u0127ek hawn. Tista' tag\u0127\u017Cel liema tipi ta' cookies u teknolo\u0121iji ta' tra\u010B\u010Bar tippermetti."},consentTypes:{necessary:{title:"Strettament ne\u010Bessarji",description:"Dawn il-cookies huma essenzjali biex is-sit web ja\u0127dem sew u ma jistg\u0127ux ji\u0121u di\u017Cattivati."},functionality:{title:"Funzjonalit\xE0",description:"Dawn il-cookies jippermettu funzjonalit\xE0 mtejba u personalizzazzjoni tas-sit web."},marketing:{title:"Marketing",description:"Dawn il-cookies jintu\u017Caw biex iwasslu riklami rilevanti u jittra\u010B\u010Baw l-effettivit\xE0 tag\u0127hom."},measurement:{title:"Analitika",description:"Dawn il-cookies jg\u0127inuna nifhmu kif il-vi\u017Citaturi jintera\u0121ixxu mas-sit web u ntejbu l-prestazzjoni tieg\u0127u."},experience:{title:"Esperjenza",description:"Dawn il-cookies jg\u0127inuna nipprovdu esperjenza a\u0127jar g\u0127all-utent u nittestjaw karatteristi\u010Bi \u0121odda."}},frame:{title:"A\u010B\u010Betta l-kunsens ta' {category} biex tara dan il-kontenut.",actionButton:"Attiva l-kunsens ta' {category}"},legalLinks:{privacyPolicy:"Politika tal-Privatezza",cookiePolicy:"Politika tal-Cookies",termsOfService:"Termini tas-Servizz"},iab:{banner:{title:"Settings tal-privatezza",description:"A\u0127na u l-{partnerCount} sie\u0127eb tag\u0127na na\u0127\u017Cnu u/jew na\u010B\u010Bessaw informazzjoni fuq apparat u nippro\u010Bessaw data personali, b\u0127al identifikaturi uni\u010Bi u data tal-browsing, g\u0127al dan is-sit web, biex:",partnersLink:"{count} sie\u0127eb",andMore:"U {count} o\u0127ra...",legitimateInterestNotice:"Xi s\u0127ab jitolbu interess le\u0121ittimu biex jippro\u010Bessaw id-data tieg\u0127ek. G\u0127andek id-dritt li to\u0121\u0121ezzjona g\u0127al dan il-pro\u010Bessar, tippersonalizza l-g\u0127a\u017Cliet tieg\u0127ek, u tirtira l-kunsens tieg\u0127ek fi kwalunkwe \u0127in.",scopeServiceSpecific:"Il-kunsens tieg\u0127ek japplika biss g\u0127al dan is-sit web u ma jaffettwax servizzi o\u0127ra.",scopeGroup:"L-g\u0127a\u017Cla tieg\u0127ek tapplika g\u0127al kull sit web tag\u0127na f'din il-grupp."},preferenceCenter:{title:"Settings tal-privatezza",description:"Personalizza s-settings tal-privatezza tieg\u0127ek hawn. Tista' tag\u0127\u017Cel liema tipi ta' cookies u teknolo\u0121iji ta' tra\u010B\u010Bar tippermetti.",tabs:{purposes:"G\u0127anijiet",vendors:"Bejjieg\u0127a"},purposeItem:{partners:"{count} sie\u0127eb",vendorsUseLegitimateInterest:"{count} bejjieg\u0127 jitolbu interess le\u0121ittimu",examples:"E\u017Cempji",partnersUsingPurpose:"S\u0127ab li Ju\u017Caw dan l-G\u0127an",withYourPermission:"Bil-Permess Tieg\u0127ek",legitimateInterest:"Interess Le\u0121ittimu",objectButton:"O\u0121\u0121ezzjona",objected:"O\u0121\u0121ezzjonat",rightToObject:"G\u0127andek id-dritt li to\u0121\u0121ezzjona g\u0127all-ippro\u010Bessar ibba\u017Cat fuq interess le\u0121ittimu."},specialPurposes:{title:"Funzjonijiet Essenzjali (Me\u0127tie\u0121a)",tooltip:"Dawn huma me\u0127tie\u0121a g\u0127all-funzjonalit\xE0 u s-sigurt\xE0 tas-sit. Skont l-IAB TCF, ma tistax to\u0121\u0121ezzjona g\u0127al dawn l-g\u0127anijiet spe\u010Bjali."},vendorList:{search:"Fittex bejjieg\u0127a...",showingCount:"Qed jintwerew {filtered} minn {total} bejjieg\u0127",iabVendorsHeading:"Bejjieg\u0127a Re\u0121istrati fl-IAB",iabVendorsNotice:"Dawn is-s\u0127ab huma re\u0121istrati mal-IAB Transparency & Consent Framework (TCF), standard tal-industrija g\u0127all-immani\u0121\u0121jar tal-kunsens",customVendorsHeading:"S\u0127ab Personalizzati",customVendorsNotice:"Dawn huma s\u0127ab personalizzati mhux re\u0121istrati mal-IAB Transparency & Consent Framework (TCF). Huma jippro\u010Bessaw id-data abba\u017Ci tal-kunsens tieg\u0127ek u jista\u2019 jkollhom prattiki ta\u2019 privatezza differenti minn bejjieg\u0127a re\u0121istrati fl-IAB.",purposes:"G\u0127anijiet",specialPurposes:"G\u0127anijiet Spe\u010Bjali",specialFeatures:"Karatteristi\u010Bi Spe\u010Bjali",features:"Karatteristi\u010Bi",dataCategories:"Kategoriji tad-Data",usesCookies:"Ju\u017Ca l-Cookies",nonCookieAccess:"A\u010B\u010Bess Mhux tal-Cookie",maxAge:"Et\xE0 Massima: {days}j",retention:"\u017Bamma: {days}j",legitimateInterest:"Int. Le\u0121ittimu",privacyPolicy:"Politika tal-Privatezza",storageDisclosure:"\u017Bvelar tal-\u0126a\u017Cna",requiredNotice:"Me\u0127tie\u0121 g\u0127all-funzjonalit\xE0 tas-sit, ma jistax ji\u0121i di\u017Cattivat"},footer:{consentStorage:'Il-preferenzi tal-kunsens huma ma\u0127\u017Cuna f\u2019cookie msemmija "euconsent-v2" g\u0127al 13-il xahar. The storage duration may be refreshed when you update your preferences.'}},common:{acceptAll:"A\u010B\u010Betta kollox",rejectAll:"Irrifjuta kollox",customize:"Personalizza",saveSettings:"Issejvja s-settings",loading:"Qed jillowdja...",showingSelectedVendor:"Qed jintwera l-bejjieg\u0127 mag\u0127\u017Cul",clearSelection:"Ikklerja",customPartner:"Sie\u0127eb personalizzat mhux re\u0121istrat mal-IAB"}}},ji={common:{acceptAll:"Godta alle",rejectAll:"Avsl\xE5 alle",customize:"Tilpass",save:"Lagre innstillinger"},cookieBanner:{title:"Vi verdsetter ditt personvern",description:"Dette nettstedet bruker informasjonskapsler for \xE5 forbedre din nettopplevelse, analysere trafikk og vise personlig tilpasset innhold."},consentManagerDialog:{title:"Personverninnstillinger",description:"Tilpass personverninnstillingene dine her. Du kan velge hvilke typer informasjonskapsler og sporingsteknologier du vil tillate."},consentTypes:{necessary:{title:"Strengt n\xF8dvendige",description:"Disse informasjonskapslene er essensielle for at nettstedet skal fungere riktig og kan ikke deaktiveres."},functionality:{title:"Funksjonalitet",description:"Disse informasjonskapslene muliggj\xF8r forbedret funksjonalitet og personalisering av nettstedet."},marketing:{title:"Markedsf\xF8ring",description:"Disse informasjonskapslene brukes til \xE5 levere relevante annonser og spore deres effektivitet."},measurement:{title:"Analyse",description:"Disse informasjonskapslene hjelper oss med \xE5 forst\xE5 hvordan bes\xF8kende samhandler med nettstedet og forbedre ytelsen."},experience:{title:"Opplevelse",description:"Disse informasjonskapslene hjelper oss med \xE5 gi en bedre brukeropplevelse og teste nye funksjoner."}},frame:{title:"Godta {category}-samtykke for \xE5 se dette innholdet.",actionButton:"Aktiver {category}-samtykke"},legalLinks:{privacyPolicy:"Personvernerkl\xE6ring",cookiePolicy:"Retningslinjer for informasjonskapsler",termsOfService:"Vilk\xE5r for bruk"},iab:{banner:{title:"Personverninnstillinger",description:"Vi og v\xE5re {partnerCount} partnere lagrer og/eller har tilgang til informasjon p\xE5 enheten din og behandler personopplysninger, som unike identifikatorer og nettleserdata, for dette nettstedet, for \xE5:",partnersLink:"{count} partnere",andMore:"Og {count} til...",legitimateInterestNotice:"Noen partnere krever legitim interesse for \xE5 behandle dataene dine. Du har rett til \xE5 protestere mot denne behandlingen, tilpasse valgene dine og trekke tilbake samtykket ditt n\xE5r som helst.",scopeServiceSpecific:"Samtykket ditt gjelder bare for dette nettstedet og p\xE5virker ikke andre tjenester.",scopeGroup:"Valget ditt gjelder p\xE5 tvers av v\xE5re nettsider i denne gruppen."},preferenceCenter:{title:"Personverninnstillinger",description:"Tilpass personverninnstillingene dine her. Du kan velge hvilke typer informasjonskapsler og sporingsteknologier du vil tillate.",tabs:{purposes:"Form\xE5l",vendors:"Leverand\xF8rer"},purposeItem:{partners:"{count} partnere",vendorsUseLegitimateInterest:"{count} leverand\xF8rer krever legitim interesse",examples:"Eksempler",partnersUsingPurpose:"Partnere som bruker dette form\xE5let",withYourPermission:"Med din tillatelse",legitimateInterest:"Legitim interesse",objectButton:"Protester",objected:"Protestert",rightToObject:"Du har rett til \xE5 protestere mot behandling basert p\xE5 legitim interesse."},specialPurposes:{title:"Viktige funksjoner (p\xE5krevd)",tooltip:"Disse er n\xF8dvendige for nettstedets funksjonalitet og sikkerhet. I henhold til IAB TCF kan du ikke protestere mot disse spesielle form\xE5lene."},vendorList:{search:"S\xF8k etter leverand\xF8rer...",showingCount:"{filtered} av {total} leverand\xF8rer",iabVendorsHeading:"IAB-registrerte leverand\xF8rer",iabVendorsNotice:"Disse partnerne er registrert i IAB Transparency & Consent Framework (TCF), en bransjestandard for administrasjon av samtykke",customVendorsHeading:"Egendefinerte partnere",customVendorsNotice:"Dette er egendefinerte partnere som ikke er registrert i IAB Transparency & Consent Framework (TCF). De behandler data basert p\xE5 ditt samtykke og kan ha annen personvernpraksis enn IAB-registrerte leverand\xF8rer.",purposes:"Form\xE5l",specialPurposes:"Spesielle form\xE5l",specialFeatures:"Spesielle funksjoner",features:"Funksjoner",dataCategories:"Datakategorier",usesCookies:"Bruker informasjonskapsler",nonCookieAccess:"Ikke-informasjonskapsel-tilgang",maxAge:"Maks alder: {days}d",retention:"Oppbevaring: {days}d",legitimateInterest:"Leg. interesse",privacyPolicy:"Personvernerkl\xE6ring",storageDisclosure:"Lagringsinformasjon",requiredNotice:"P\xE5krevd for nettstedets funksjonalitet, kan ikke deaktiveres"},footer:{consentStorage:'Samtykkepreferanser lagres i en informasjonskapsel kalt "euconsent-v2" i 13 m\xE5neder. The storage duration may be refreshed when you update your preferences.'}},common:{acceptAll:"Godta alle",rejectAll:"Avsl\xE5 alle",customize:"Tilpass",saveSettings:"Lagre innstillinger",loading:"Laster...",showingSelectedVendor:"Viser valgt leverand\xF8r",clearSelection:"T\xF8m",customPartner:"Egendefinert partner ikke registrert i IAB"}}},Si={common:{acceptAll:"Alles accepteren",rejectAll:"Alles weigeren",customize:"Aanpassen",save:"Instellingen opslaan"},cookieBanner:{title:"Wij hechten waarde aan uw privacy",description:"Deze site gebruikt cookies om uw surfervaring te verbeteren, het verkeer op de site te analyseren en gepersonaliseerde inhoud te tonen"},consentManagerDialog:{title:"Privacy-instellingen",description:"Pas hier uw privacyinstellingen aan. U kunt kiezen welke soorten cookies en trackingtechnologie\xEBn u toestaat."},consentTypes:{necessary:{title:"Strikt noodzakelijk",description:"Deze cookies zijn essentieel voor het goed functioneren van de website en kunnen niet worden uitgeschakeld"},functionality:{title:"Functionaliteit",description:"Deze cookies maken verbeterde functionaliteit en personalisatie van de website mogelijk."},marketing:{title:"Marketing",description:"Deze cookies worden gebruikt om relevante advertenties aan te bieden en de effectiviteit ervan bij te houden"},measurement:{title:"Analytics",description:"Deze cookies helpen ons te begrijpen hoe bezoekers omgaan met de website en de prestaties ervan te verbeteren"},experience:{title:"Ervaring",description:"Deze cookies helpen ons om een betere gebruikerservaring te bieden en nieuwe functies te testen"}},frame:{title:"Accepteer {category} om deze inhoud te bekijken",actionButton:"Schakel {category} toestemming in"},legalLinks:{privacyPolicy:"Privacybeleid",cookiePolicy:"Cookiebeleid",termsOfService:"Servicevoorwaarden"},iab:{banner:{title:"Privacy-instellingen",description:"Wij en onze {partnerCount} partners slaan informatie op een apparaat op en/of openen deze en verwerken persoonlijke gegevens, zoals unieke identificatoren en browsegegevens, voor deze website, om:",partnersLink:"{count} partners",andMore:"En nog {count}...",legitimateInterestNotice:"Sommige partners maken aanspraak op een gerechtvaardigd belang om uw gegevens te verwerken. U heeft het recht om bezwaar te maken tegen deze verwerking, uw keuzes aan te passen en uw toestemming op elk moment in te trekken.",scopeServiceSpecific:"Je toestemming geldt alleen voor deze website en heeft geen invloed op andere diensten.",scopeGroup:"Uw keuze geldt voor al onze websites in deze groep."},preferenceCenter:{title:"Privacy-instellingen",description:"Pas hier uw privacyinstellingen aan. U kunt kiezen welke soorten cookies en trackingtechnologie\xEBn u toestaat.",tabs:{purposes:"Doeleinden",vendors:"Leveranciers"},purposeItem:{partners:"{count} partners",vendorsUseLegitimateInterest:"{count} leveranciers maken aanspraak op gerechtvaardigd belang",examples:"Voorbeelden",partnersUsingPurpose:"Partners die dit doeleinde gebruiken",withYourPermission:"Met uw toestemming",legitimateInterest:"Gerechtvaardigd belang",objectButton:"Bezwaar maken",objected:"Bezwaar gemaakt",rightToObject:"U heeft het recht om bezwaar te maken tegen verwerking op basis van gerechtvaardigd belang."},specialPurposes:{title:"Essenti\xEBle functies (vereist)",tooltip:"Deze zijn vereist voor de functionaliteit en beveiliging van de site. Volgens IAB TCF kunt u geen bezwaar maken tegen deze speciale doeleinden."},vendorList:{search:"Zoek leveranciers...",showingCount:"{filtered} van {total} leveranciers",iabVendorsHeading:"IAB-geregistreerde leveranciers",iabVendorsNotice:"Deze partners zijn geregistreerd bij het IAB Transparency & Consent Framework (TCF), een industriestandaard voor het beheren van toestemming",customVendorsHeading:"Aangepaste partners",customVendorsNotice:"Dit zijn aangepaste partners die niet zijn geregistreerd bij het IAB Transparency & Consent Framework (TCF). Ze verwerken gegevens op basis van uw toestemming en kunnen andere privacypraktijken hebben dan IAB-geregistreerde leveranciers.",purposes:"Doeleinden",specialPurposes:"Speciale doeleinden",specialFeatures:"Speciale functies",features:"Functies",dataCategories:"Datacategorie\xEBn",usesCookies:"Gebruikt cookies",nonCookieAccess:"Toegang zonder cookies",maxAge:"Max. leeftijd: {days}d",retention:"Bewaartermijn: {days}d",legitimateInterest:"Gerechtv. belang",privacyPolicy:"Privacybeleid",storageDisclosure:"Openbaarmaking van opslag",requiredNotice:"Vereist voor websitefunctionaliteit, kan niet worden uitgeschakeld"},footer:{consentStorage:'Toestemmingsvoorkeuren worden gedurende 13 maanden opgeslagen in een cookie genaamd "euconsent-v2". The storage duration may be refreshed when you update your preferences.'}},common:{acceptAll:"Alles accepteren",rejectAll:"Alles weigeren",customize:"Aanpassen",saveSettings:"Instellingen opslaan",loading:"Laden...",showingSelectedVendor:"Geselecteerde leverancier wordt getoond",clearSelection:"Wissen",customPartner:"Aangepaste partner niet geregistreerd bij het IAB"}}},Ai={common:{acceptAll:"Godta alle",rejectAll:"Avvis alle",customize:"Tilpass",save:"Lagre innstillingar"},cookieBanner:{title:"Vi verdset personvernet ditt",description:"Denne nettstaden brukar informasjonskapslar for \xE5 forbetre nettopplevinga di, analysere nettstadtrafikk og vise personleg tilpassa innhald."},consentManagerDialog:{title:"Personverninnstillingar",description:"Tilpass personverninnstillingane dine her. Du kan velje kva typar informasjonskapslar og sporingsteknologiar du till\xE8t."},consentTypes:{necessary:{title:"Strengt n\xF8dvendige",description:"Desse informasjonskapslane er n\xF8dvendige for at nettstaden skal fungere riktig og kan ikkje deaktiverast."},functionality:{title:"Funksjonalitet",description:"Desse informasjonskapslane gjer det mogleg med forbetra funksjonalitet og personleggjering av nettstaden."},marketing:{title:"Marknadsf\xF8ring",description:"Desse informasjonskapslane blir brukte til \xE5 levere relevante annonsar og spore effektiviteten deira."},measurement:{title:"Analyse",description:"Desse informasjonskapslane hjelper oss \xE5 forst\xE5 korleis bes\xF8kande samhandlar med nettstaden og forbetre ytinga."},experience:{title:"Oppleving",description:"Desse informasjonskapslane hjelper oss \xE5 gi ei betre brukaroppleving og teste nye funksjonar."}},frame:{title:"Godta {category}-samtykke for \xE5 sj\xE5 dette innhaldet.",actionButton:"Aktiver {category}-samtykke"},legalLinks:{privacyPolicy:"Personvernerkl\xE6ring",cookiePolicy:"Retningslinjer for informasjonskapslar",termsOfService:"Brukarvilk\xE5r"},iab:{banner:{title:"Personverninnstillingar",description:"Vi og v\xE5re {partnerCount} partnarar lagrar og/eller har tilgang til informasjon p\xE5 eininga di og behandlar personopplysningar, som unike identifikatorar og nettlesardata, for denne nettstaden, for \xE5:",partnersLink:"{count} partnarar",andMore:"Og {count} til...",legitimateInterestNotice:"Nokre partnarar krev legitim interesse for \xE5 behandle dataa dine. Du har rett til \xE5 protestere mot denne behandlinga, tilpasse vala dine og trekkje tilbake samtykket ditt n\xE5r som helst.",scopeServiceSpecific:"Samtykket ditt gjeld berre for denne nettstaden og p\xE5verkar ikkje andre tenester.",scopeGroup:"Valet ditt gjeld p\xE5 tvers av nettsidene v\xE5re i denne gruppa."},preferenceCenter:{title:"Personverninnstillingar",description:"Tilpass personverninnstillingane dine her. Du kan velje kva typar informasjonskapslar og sporingsteknologiar du till\xE8t.",tabs:{purposes:"F\xF8rem\xE5l",vendors:"Leverand\xF8rar"},purposeItem:{partners:"{count} partnarar",vendorsUseLegitimateInterest:"{count} leverand\xF8rar krev legitim interesse",examples:"D\xF8me",partnersUsingPurpose:"Partnarar som brukar dette f\xF8rem\xE5let",withYourPermission:"Med di tillating",legitimateInterest:"Legitim interesse",objectButton:"Protester",objected:"Protestert",rightToObject:"Du har rett til \xE5 protestere mot behandling basert p\xE5 legitim interesse."},specialPurposes:{title:"Viktige funksjonar (p\xE5kravd)",tooltip:"Desse er n\xF8dvendige for funksjonaliteten og tryggleiken til nettstaden. I f\xF8lgje IAB TCF kan du ikkje protestere mot desse spesielle f\xF8rem\xE5la."},vendorList:{search:"S\xF8k etter leverand\xF8rar...",showingCount:"{filtered} av {total} leverand\xF8rar",iabVendorsHeading:"IAB-registrerte leverand\xF8rar",iabVendorsNotice:"Disse partnarane er registrerte i IAB Transparency & Consent Framework (TCF), ein bransjestandard for administrasjon av samtykke",customVendorsHeading:"Eigendefinerte partnarar",customVendorsNotice:"Dette er eigendefinerte partnarar som ikkje er registrerte i IAB Transparency & Consent Framework (TCF). Dei behandlar data basert p\xE5 ditt samtykke og kan ha annan personvernpraksis enn IAB-registrerte leverand\xF8rar.",purposes:"F\xF8rem\xE5l",specialPurposes:"Spesielle f\xF8rem\xE5l",specialFeatures:"Spesielle funksjonar",features:"Funksjonar",dataCategories:"Datakategoriar",usesCookies:"Brukar informasjonskapslar",nonCookieAccess:"Ikkje-informasjonskapsel-tilgang",maxAge:"Maks alder: {days}d",retention:"Lagring: {days}d",legitimateInterest:"Leg. interesse",privacyPolicy:"Personvernerkl\xE6ring",storageDisclosure:"Lagringsinformasjon",requiredNotice:"P\xE5kravd for funksjonaliteten til nettstaden, kan ikkje deaktiverast"},footer:{consentStorage:'Samtykkepreferansar blir lagra i ein informasjonskapsel kalla "euconsent-v2" i 13 m\xE5nader. The storage duration may be refreshed when you update your preferences.'}},common:{acceptAll:"Godta alle",rejectAll:"Avvis alle",customize:"Tilpass",saveSettings:"Lagre innstillingar",loading:"Lastar...",showingSelectedVendor:"Viser vald leverand\xF8r",clearSelection:"T\xF8m",customPartner:"Eigendefinert partnar ikkje registrert i IAB"}}},zi={common:{acceptAll:"Zaakceptuj wszystkie",rejectAll:"Odrzu\u0107 wszystkie",customize:"Dostosuj",save:"Zapisz ustawienia"},cookieBanner:{title:"Cenimy Twoj\u0105 prywatno\u015B\u0107",description:"Ta strona u\u017Cywa plik\xF3w cookie, aby poprawi\u0107 Twoje wra\u017Cenia z przegl\u0105dania, analizowa\u0107 ruch na stronie i wy\u015Bwietla\u0107 spersonalizowane tre\u015Bci."},consentManagerDialog:{title:"Ustawienia prywatno\u015Bci",description:"Dostosuj tutaj swoje ustawienia prywatno\u015Bci. Mo\u017Cesz wybra\u0107, kt\xF3re rodzaje plik\xF3w cookie i technologii \u015Bledzenia chcesz zaakceptowa\u0107."},consentTypes:{necessary:{title:"\u015Aci\u015Ble niezb\u0119dne",description:"Te pliki cookie s\u0105 niezb\u0119dne do prawid\u0142owego funkcjonowania strony internetowej i nie mo\u017Cna ich wy\u0142\u0105czy\u0107."},functionality:{title:"Funkcjonalno\u015B\u0107",description:"Te pliki cookie umo\u017Cliwiaj\u0105 ulepszon\u0105 funkcjonalno\u015B\u0107 i personalizacj\u0119 strony internetowej."},marketing:{title:"Marketing",description:"Te pliki cookie s\u0105 u\u017Cywane do dostarczania odpowiednich reklam i \u015Bledzenia ich skuteczno\u015Bci."},measurement:{title:"Analityka",description:"Te pliki cookie pomagaj\u0105 nam zrozumie\u0107, jak odwiedzaj\u0105cy korzystaj\u0105 ze strony internetowej, i poprawi\u0107 jej wydajno\u015B\u0107."},experience:{title:"Do\u015Bwiadczenie",description:"Te pliki cookie pomagaj\u0105 nam zapewni\u0107 lepsze wra\u017Cenia u\u017Cytkownika i testowa\u0107 nowe funkcje."}},frame:{title:"Zaakceptuj zgod\u0119 na {category}, aby wy\u015Bwietli\u0107 t\u0119 tre\u015B\u0107.",actionButton:"W\u0142\u0105cz zgod\u0119 na {category}"},legalLinks:{privacyPolicy:"Polityka prywatno\u015Bci",cookiePolicy:"Polityka plik\xF3w cookie",termsOfService:"Regulamin"},iab:{banner:{title:"Ustawienia prywatno\u015Bci",description:"My i nasi {partnerCount} partnerzy przechowujemy i/lub uzyskujemy dost\u0119p do informacji na urz\u0105dzeniu oraz przetwarzamy dane osobowe, takie jak unikalne identyfikatory i dane dotycz\u0105ce przegl\u0105dania, w tej witrynie, aby:",partnersLink:"{count} partner\xF3w",andMore:"I {count} wi\u0119cej...",legitimateInterestNotice:"Niekt\xF3rzy partnerzy powo\u0142uj\u0105 si\u0119 na uzasadniony interes w przetwarzaniu Twoich danych. Masz prawo sprzeciwi\u0107 si\u0119 temu przetwarzaniu, dostosowa\u0107 swoje wybory i wycofa\u0107 zgod\u0119 w dowolnym momencie.",scopeServiceSpecific:"Twoja zgoda dotyczy tylko tej strony internetowej i nie wp\u0142ywa na inne us\u0142ugi.",scopeGroup:"Tw\xF3j wyb\xF3r ma zastosowanie do wszystkich naszych stron w tej grupie."},preferenceCenter:{title:"Ustawienia prywatno\u015Bci",description:"Dostosuj tutaj swoje ustawienia prywatno\u015Bci. Mo\u017Cesz wybra\u0107, kt\xF3re rodzaje plik\xF3w cookie i technologii \u015Bledzenia chcesz zaakceptowa\u0107.",tabs:{purposes:"Cele",vendors:"Dostawcy"},purposeItem:{partners:"{count} partner\xF3w",vendorsUseLegitimateInterest:"{count} dostawc\xF3w powo\u0142uje si\u0119 na uzasadniony interes",examples:"Przyk\u0142ady",partnersUsingPurpose:"Partnerzy korzystaj\u0105cy z tego celu",withYourPermission:"Za Twoj\u0105 zgod\u0105",legitimateInterest:"Uzasadniony interes",objectButton:"Sprzeciw",objected:"Zg\u0142oszono sprzeciw",rightToObject:"Masz prawo sprzeciwi\u0107 si\u0119 przetwarzaniu opartemu na uzasadnionym interesie."},specialPurposes:{title:"Funkcje niezb\u0119dne (wymagane)",tooltip:"S\u0105 one wymagane dla funkcjonalno\u015Bci i bezpiecze\u0144stwa witryny. Zgodnie z IAB TCF nie mo\u017Cna sprzeciwi\u0107 si\u0119 tym celom specjalnym."},vendorList:{search:"Szukaj dostawc\xF3w...",showingCount:"{filtered} z {total} dostawc\xF3w",iabVendorsHeading:"Dostawcy zarejestrowani w IAB",iabVendorsNotice:"Ci partnerzy s\u0105 zarejestrowani w IAB Transparency & Consent Framework (TCF), standardzie bran\u017Cowym dotycz\u0105cym zarz\u0105dzania zgodami",customVendorsHeading:"Partnerzy niestandardowi",customVendorsNotice:"S\u0105 to partnerzy niestandardowi, kt\xF3rzy nie s\u0105 zarejestrowani w IAB Transparency & Consent Framework (TCF). Przetwarzaj\u0105 dane na podstawie Twojej zgody i mog\u0105 stosowa\u0107 inne praktyki prywatno\u015Bci ni\u017C dostawcy zarejestrowani w IAB.",purposes:"Cele",specialPurposes:"Cele specjalne",specialFeatures:"Funkcje specjalne",features:"Funkcje",dataCategories:"Kategorie danych",usesCookies:"U\u017Cywa plik\xF3w cookie",nonCookieAccess:"Dost\u0119p bez plik\xF3w cookie",maxAge:"Maks. wiek: {days}d",retention:"Przechowywanie: {days}d",legitimateInterest:"Uzasadn. interes",privacyPolicy:"Polityka prywatno\u015Bci",storageDisclosure:"Ujawnienie informacji o przechowywaniu",requiredNotice:"Wymagane dla funkcjonalno\u015Bci witryny, nie mo\u017Cna wy\u0142\u0105czy\u0107"},footer:{consentStorage:"Preferencje dotycz\u0105ce zgody s\u0105 przechowywane w pliku cookie o nazwie \u201Eeuconsent-v2\u201D przez 13 miesi\u0119cy. The storage duration may be refreshed when you update your preferences."}},common:{acceptAll:"Zaakceptuj wszystkie",rejectAll:"Odrzu\u0107 wszystkie",customize:"Dostosuj",saveSettings:"Zapisz ustawienia",loading:"\u0141adowanie...",showingSelectedVendor:"Pokazywanie wybranego dostawcy",clearSelection:"Wyczy\u015B\u0107",customPartner:"Partner niestandardowy niezarejestrowany w IAB"}}},Pi={common:{acceptAll:"Aceitar todos",rejectAll:"Rejeitar todos",customize:"Personalizar",save:"Salvar configura\xE7\xF5es"},cookieBanner:{title:"Respeitamos a sua privacidade",description:"Este site utiliza cookies para melhorar a sua experi\xEAncia de navega\xE7\xE3o, analisar o tr\xE1fego do site e mostrar conte\xFAdos personalizados."},consentManagerDialog:{title:"Configura\xE7\xF5es",description:"Personalize suas configura\xE7\xF5es de privacidade aqui. Voc\xEA pode escolher quais tipos de cookies e tecnologias de rastreamento voc\xEA permite."},consentTypes:{necessary:{title:"Estritamente necess\xE1rio",description:"Estes cookies s\xE3o essenciais para o site funcionar corretamente e n\xE3o podem ser desativados."},functionality:{title:"Funcionalidade",description:"Estes cookies permitem funcionalidades aprimoradas e personaliza\xE7\xE3o do site."},marketing:{title:"Marketing",description:"Estes cookies s\xE3o utilizados para fornecer publicidade relevante e rastrear a sua efic\xE1cia."},measurement:{title:"An\xE1lise",description:"Estes cookies nos ajudam a compreender como os visitantes interagem com o site e melhoram o seu desempenho."},experience:{title:"Experi\xEAncia",description:"Estes cookies nos ajudam a fornecer uma experi\xEAncia de usu\xE1rio melhor e testar novas funcionalidades."}},frame:{title:"Aceite {category} para ver este conte\xFAdo",actionButton:"Ativar consentimento {category}"},legalLinks:{privacyPolicy:"Pol\xEDtica de Privacidade",cookiePolicy:"Pol\xEDtica de Cookies",termsOfService:"Termos de Servi\xE7o"},iab:{banner:{title:"Configura\xE7\xF5es de privacidade",description:"N\xF3s e os nossos {partnerCount} parceiros armazenamos e/ou acedemos a informa\xE7\xF5es num dispositivo e processamos dados pessoais, tais como identificadores \xFAnicos e informa\xE7\xF5es de navega\xE7\xE3o, para este website, para:",partnersLink:"{count} parceiros",andMore:"E mais {count}...",legitimateInterestNotice:"Alguns parceiros alegam um interesse leg\xEDtimo para processar os seus dados. Tem o direito de se opor a este processamento, personalizar as suas escolhas e retirar o seu consentimento a qualquer momento.",scopeServiceSpecific:"O seu consentimento aplica-se apenas a este site e n\xE3o afetar\xE1 outros servi\xE7os.",scopeGroup:"A sua escolha aplica-se a todos os nossos sites neste grupo."},preferenceCenter:{title:"Configura\xE7\xF5es de privacidade",description:"Personalize suas configura\xE7\xF5es de privacidade aqui. Voc\xEA pode escolher quais tipos de cookies e tecnologias de rastreamento voc\xEA permite.",tabs:{purposes:"Finalidades",vendors:"Fornecedores"},purposeItem:{partners:"{count} parceiros",vendorsUseLegitimateInterest:"{count} fornecedores alegam interesse leg\xEDtimo",examples:"Exemplos",partnersUsingPurpose:"Parceiros que utilizam esta finalidade",withYourPermission:"Com a sua permiss\xE3o",legitimateInterest:"Interesse leg\xEDtimo",objectButton:"Opor-se",objected:"Oposi\xE7\xE3o registada",rightToObject:"Tem o direito de se opor ao processamento baseado no interesse leg\xEDtimo."},specialPurposes:{title:"Fun\xE7\xF5es essenciais (obrigat\xF3rias)",tooltip:"Estas s\xE3o necess\xE1rias para a funcionalidade e seguran\xE7a do site. De acordo com o IAB TCF, n\xE3o pode opor-se a estas finalidades especiais."},vendorList:{search:"Procurar fornecedores...",showingCount:"{filtered} de {total} fornecedores",iabVendorsHeading:"Fornecedores registados no IAB",iabVendorsNotice:"Estes parceiros est\xE3o registados no IAB Transparency & Consent Framework (TCF), um padr\xE3o da ind\xFAstria para gerir o consentimento",customVendorsHeading:"Parceiros personalizados",customVendorsNotice:"Estes s\xE3o parceiros personalizados n\xE3o registados no IAB Transparency & Consent Framework (TCF). Processam dados com base no seu consentimento e podem ter pr\xE1ticas de privacidade diferentes das dos fornecedores registados no IAB.",purposes:"Finalidades",specialPurposes:"Finalidades especiais",specialFeatures:"Funcionalidades especiais",features:"Funcionalidades",dataCategories:"Categorias de dados",usesCookies:"Utiliza cookies",nonCookieAccess:"Acesso sem cookies",maxAge:"Idade m\xE1x.: {days}d",retention:"Reten\xE7\xE3o: {days}d",legitimateInterest:"Int. leg\xEDtimo",privacyPolicy:"Pol\xEDtica de privacidade",storageDisclosure:"Divulga\xE7\xE3o de armazenamento",requiredNotice:"Necess\xE1rio para a funcionalidade do site, n\xE3o pode ser desativado"},footer:{consentStorage:'As prefer\xEAncias de consentimento s\xE3o armazenadas num cookie chamado "euconsent-v2" durante 13 meses. The storage duration may be refreshed when you update your preferences.'}},common:{acceptAll:"Aceitar todos",rejectAll:"Rejeitar todos",customize:"Personalizar",saveSettings:"Salvar configura\xE7\xF5es",loading:"A carregar...",showingSelectedVendor:"A mostrar o fornecedor selecionado",clearSelection:"Limpar",customPartner:"Parceiro personalizado n\xE3o registado no IAB"}}},Ti={common:{acceptAll:"Acceptar tut",rejectAll:"Refusar tut",customize:"Persunalisar",save:"Memorisar las configuraziuns"},cookieBanner:{title:"Nus stimain vossa sfera privata",description:"Questa pagina d'internet dovra cookies per meglierar vossa experientscha da navigaziun, analisar il traffic da la pagina e mussar cuntegns persunalisads."},consentManagerDialog:{title:"Configuraziuns da la sfera privata",description:"Persunalisai vossas configuraziuns da la sfera privata qua. Vus pudais tscherner tge tips da cookies e tecnologias da tracking che vus lubis."},consentTypes:{necessary:{title:"Absolutamain necessari",description:"Quests cookies \xE8n essenzials per il funcziunament da la pagina d'internet e na pon betg vegnir deactivads."},functionality:{title:"Funcziunalitad",description:"Quests cookies permettan funcziunalitads avanzadas e la persunalisaziun da la pagina d'internet."},marketing:{title:"Marketing",description:"Quests cookies vegnan duvrads per mussar reclamas relevantas e per evaluar lur efficacitad."},measurement:{title:"Analisa",description:"Quests cookies ans gidan a chapir co ils visitaders interageschan cun la pagina d'internet e meglierar sia prestaziun."},experience:{title:"Experientscha",description:"Quests cookies ans gidan a porscher ina meglra experientscha d'utilisader e testar novas funcziuns."}},frame:{title:"Acceptai il consentiment da {category} per vesair quest cuntegn.",actionButton:"Activar il consentiment da {category}"},legalLinks:{privacyPolicy:"Directivas da protecziun da datas",cookiePolicy:"Directivas da cookies",termsOfService:"Cundiziuns d'utilisaziun"},iab:{banner:{title:"Configuraziuns da la sfera privata",description:"Nus ed noss {partnerCount} partunaris memorisain e/u accessain ad infurmaziuns sin voss apparat e processain datas persunalas, sco identificaturs unics e datas da navigaziun, per questa pagina d\u2019internet, per:",partnersLink:"{count} partunaris",andMore:"Ed anc {count}...",legitimateInterestNotice:"Inscunter partunaris pretendan in interess legitim per processar vossas datas. Vus avais il dretg da far opposiziun cunter quest processament, persunalisar vossas tschernas e revocar voss consentiment en mintga mument.",scopeServiceSpecific:"Voss consent vala be per questa pagina web e na pertutga betg auters servetschs.",scopeGroup:"Vossa tscherna vala per tut nossas websites en quest gruppa."},preferenceCenter:{title:"Configuraziuns da la sfera privata",description:"Persunalisai vossas configuraziuns da la sfera privata qua. Vus pudais tscherner tge tips da cookies e tecnologias da tracking che vus lubis.",tabs:{purposes:"Finamiras",vendors:"Proveders"},purposeItem:{partners:"{count} partunaris",vendorsUseLegitimateInterest:"{count} proveders pretendan in interess legitim",examples:"Exempels",partnersUsingPurpose:"Partunaris che duvran questa finamira",withYourPermission:"Cun vossa permissiun",legitimateInterest:"Interess legitim",objectButton:"Far opposiziun",objected:"Opposiziun fatta",rightToObject:"Vus avais il dretg da far opposiziun cunter il processament sa basond sin in interess legitim."},specialPurposes:{title:"Funcziuns essenzialas (necessari)",tooltip:"Questas \xE8n necessarias per la funcziunalitad e la segirezza da la pagina. Tenor IAB TCF na pudais vus betg far opposiziun cunter questas finamiras spezialas."},vendorList:{search:"Tscherchar proveders...",showingCount:"Mussa {filtered} da {total} proveders",iabVendorsHeading:"Proveders registrads tar l\u2019IAB",iabVendorsNotice:"Quests partunaris \xE8n registrads tar l\u2019IAB Transparency & Consent Framework (TCF), in standard industrial per la gestiun dal consentiment",customVendorsHeading:"Partunaris persunalisads",customVendorsNotice:"Quai \xE8n partunaris persunalisads che n\u2019\xE8n betg registrads tar l\u2019IAB Transparency & Consent Framework (TCF). Els processan datas sa basond sin voss consentiment e pon avair autras praticas da protecziun da datas che proveders registrads tar l\u2019IAB.",purposes:"Finamiras",specialPurposes:"Finamiras spezialas",specialFeatures:"Funcziuns spezialas",features:"Funcziuns",dataCategories:"Categorias da datas",usesCookies:"Dovra cookies",nonCookieAccess:"Access betg tras cookies",maxAge:"Gradi maximal: {days}d",retention:"Retegnida: {days}d",legitimateInterest:"Int. legitim",privacyPolicy:"Directivas da protecziun da datas",storageDisclosure:"Infurmaziun davart la memorisaziun",requiredNotice:"Necessari per la funcziunalitad da la pagina, na po betg vegnir deactiv\xE0"},footer:{consentStorage:'Las preferenzas da consentiment vegnan memorisadas en in cookie numn\xE0 "euconsent-v2" per 13 mais. The storage duration may be refreshed when you update your preferences.'}},common:{acceptAll:"Acceptar tut",rejectAll:"Refusar tut",customize:"Persunalisar",saveSettings:"Memorisar las configuraziuns",loading:"Chargia...",showingSelectedVendor:"Mussa il proveder tschern\xEC",clearSelection:"Stizzar",customPartner:"Partunari persunalis\xE0 betg registr\xE0 tar l\u2019IAB"}}},Li={common:{acceptAll:"Accept\u0103 toate",rejectAll:"Respinge toate",customize:"Personalizeaz\u0103",save:"Salveaz\u0103 set\u0103rile"},cookieBanner:{title:"Pre\u021Buim confiden\u021Bialitatea ta",description:"Acest site folose\u0219te cookie-uri pentru a \xEEmbun\u0103t\u0103\u021Bi experien\u021Ba de navigare, a analiza traficul site-ului \u0219i a afi\u0219a con\u021Binut personalizat."},consentManagerDialog:{title:"Set\u0103ri de confiden\u021Bialitate",description:"Personalizeaz\u0103 set\u0103rile de confiden\u021Bialitate aici. Po\u021Bi alege ce tipuri de cookie-uri \u0219i tehnologii de urm\u0103rire permi\u021Bi."},consentTypes:{necessary:{title:"Strict necesare",description:"Aceste cookie-uri sunt esen\u021Biale pentru func\u021Bionarea corect\u0103 a site-ului \u0219i nu pot fi dezactivate."},functionality:{title:"Func\u021Bionalitate",description:"Aceste cookie-uri permit func\u021Bionalit\u0103\u021Bi avansate \u0219i personalizarea site-ului."},marketing:{title:"Marketing",description:"Aceste cookie-uri sunt utilizate pentru a livra reclame relevante \u0219i pentru a urm\u0103ri eficien\u021Ba acestora."},measurement:{title:"Analitice",description:"Aceste cookie-uri ne ajut\u0103 s\u0103 \xEEn\u021Belegem cum interac\u021Bioneaz\u0103 vizitatorii cu site-ul \u0219i s\u0103 \xEEi \xEEmbun\u0103t\u0103\u021Bim performan\u021Ba."},experience:{title:"Experien\u021B\u0103",description:"Aceste cookie-uri ne ajut\u0103 s\u0103 oferim o experien\u021B\u0103 mai bun\u0103 utilizatorilor \u0219i s\u0103 test\u0103m func\u021Bionalit\u0103\u021Bi noi."}},frame:{title:"Accept\u0103 consim\u021B\u0103m\xE2ntul pentru {category} pentru a vizualiza acest con\u021Binut.",actionButton:"Activeaz\u0103 consim\u021B\u0103m\xE2ntul pentru {category}"},legalLinks:{privacyPolicy:"Politica de confiden\u021Bialitate",cookiePolicy:"Politica privind cookie-urile",termsOfService:"Termeni \u0219i condi\u021Bii"},iab:{banner:{title:"Set\u0103ri de confiden\u021Bialitate",description:"Noi \u0219i cei {partnerCount} parteneri ai no\u0219tri stoc\u0103m \u0219i/sau acces\u0103m informa\u021Bii pe dispozitivul t\u0103u \u0219i proces\u0103m date personale, cum ar fi identificatori unici \u0219i date de navigare, pentru acest site web, pentru:",partnersLink:"{count} parteneri",andMore:"\u0218i \xEEnc\u0103 {count}...",legitimateInterestNotice:"Unii parteneri invoc\u0103 un interes legitim pentru a procesa datele tale. Ai dreptul de a te opune acestei proces\u0103ri, de a-\u021Bi personaliza alegerile \u0219i de a-\u021Bi retrage consim\u021B\u0103m\xE2ntul \xEEn orice moment.",scopeServiceSpecific:"Consim\u021B\u0103m\xE2ntul t\u0103u se aplic\u0103 doar acestui site web \u0219i nu va afecta alte servicii.",scopeGroup:"Alegerea dvs. se aplic\u0103 tuturor site-urilor noastre din acest grup."},preferenceCenter:{title:"Set\u0103ri de confiden\u021Bialitate",description:"Personalizeaz\u0103 set\u0103rile de confiden\u021Bialitate aici. Po\u021Bi alege ce tipuri de cookie-uri \u0219i tehnologii de urm\u0103rire permi\u021Bi.",tabs:{purposes:"Scopuri",vendors:"Furnizori"},purposeItem:{partners:"{count} parteneri",vendorsUseLegitimateInterest:"{count} furnizori invoc\u0103 interes legitim",examples:"Exemple",partnersUsingPurpose:"Parteneri care utilizeaz\u0103 acest scop",withYourPermission:"Cu permisiunea ta",legitimateInterest:"Interes legitim",objectButton:"Opunere",objected:"Opozi\u021Bie exprimat\u0103",rightToObject:"Ai dreptul de a te opune proces\u0103rii bazate pe interesul legitim."},specialPurposes:{title:"Func\u021Bii esen\u021Biale (obligatorii)",tooltip:"Acestea sunt necesare pentru func\u021Bionalitatea \u0219i securitatea site-ului. Conform IAB TCF, nu te po\u021Bi opune acestor scopuri speciale."},vendorList:{search:"Caut\u0103 furnizori...",showingCount:"Se afi\u0219eaz\u0103 {filtered} din {total} furnizori",iabVendorsHeading:"Furnizori \xEEnregistra\u021Bi IAB",iabVendorsNotice:"Ace\u0219ti parteneri sunt \xEEnregistra\u021Bi \xEEn cadrul IAB Transparency & Consent Framework (TCF), un standard industrial pentru gestionarea consim\u021B\u0103m\xE2ntului",customVendorsHeading:"Parteneri personaliza\u021Bi",customVendorsNotice:"Ace\u0219tia sunt parteneri personaliza\u021Bi care nu sunt \xEEnregistra\u021Bi \xEEn IAB Transparency & Consent Framework (TCF). Ei proceseaz\u0103 datele pe baza consim\u021B\u0103m\xE2ntului t\u0103u \u0219i pot avea practici de confiden\u021Bialitate diferite de cele ale furnizorilor \xEEnregistra\u021Bi IAB.",purposes:"Scopuri",specialPurposes:"Scopuri speciale",specialFeatures:"Func\u021Bionalit\u0103\u021Bi speciale",features:"Func\u021Bionalit\u0103\u021Bi",dataCategories:"Categorii de date",usesCookies:"Utilizeaz\u0103 cookie-uri",nonCookieAccess:"Acces non-cookie",maxAge:"V\xE2rst\u0103 max.: {days}z",retention:"Reten\u021Bie: {days}z",legitimateInterest:"Int. legitim",privacyPolicy:"Politic\u0103 de confiden\u021Bialitate",storageDisclosure:"Prezentarea stoc\u0103rii",requiredNotice:"Necesar pentru func\u021Bionalitatea site-ului, nu poate fi dezactivat"},footer:{consentStorage:"Preferin\u021Bele de consim\u021B\u0103m\xE2nt sunt stocate \xEEntr-un cookie numit \u201Eeuconsent-v2\u201D timp de 13 luni. The storage duration may be refreshed when you update your preferences."}},common:{acceptAll:"Accept\u0103 toate",rejectAll:"Respinge toate",customize:"Personalizeaz\u0103",saveSettings:"Salveaz\u0103 set\u0103rile",loading:"Se \xEEncarc\u0103...",showingSelectedVendor:"Se afi\u0219eaz\u0103 furnizorul selectat",clearSelection:"\u0218terge",customPartner:"Partener personalizat ne\xEEnregistrat \xEEn IAB"}}},Ei={common:{acceptAll:"Prija\u0165 v\u0161etko",rejectAll:"Odmietnu\u0165 v\u0161etko",customize:"Prisp\xF4sobi\u0165",save:"Ulo\u017Ei\u0165 nastavenia"},cookieBanner:{title:"V\xE1\u017Eime si va\u0161e s\xFAkromie",description:"T\xE1to str\xE1nka pou\u017E\xEDva cookies na zlep\u0161enie v\xE1\u0161ho prehliadania, anal\xFDzu n\xE1v\u0161tevnosti a zobrazovanie personalizovan\xE9ho obsahu."},consentManagerDialog:{title:"Nastavenia s\xFAkromia",description:"Prisp\xF4sobte si nastavenia s\xFAkromia tu. M\xF4\u017Eete si vybra\u0165, ktor\xE9 typy cookies a sledovac\xEDch technol\xF3gi\xED povol\xEDte."},consentTypes:{necessary:{title:"Nevyhnutn\xE9",description:"Tieto cookies s\xFA nevyhnutn\xE9 pre spr\xE1vne fungovanie webovej str\xE1nky a nemo\u017Eno ich deaktivova\u0165."},functionality:{title:"Funk\u010Dnos\u0165",description:"Tieto cookies umo\u017E\u0148uj\xFA roz\u0161\xEDren\xFA funk\u010Dnos\u0165 a personaliz\xE1ciu webovej str\xE1nky."},marketing:{title:"Marketing",description:"Tieto cookies sa pou\u017E\xEDvaj\xFA na doru\u010Dovanie relevantn\xFDch rekl\xE1m a sledovanie ich \xFA\u010Dinnosti."},measurement:{title:"Analytika",description:"Tieto cookies n\xE1m pom\xE1haj\xFA pochopi\u0165, ako n\xE1v\u0161tevn\xEDci interaguj\xFA s webovou str\xE1nkou a zlep\u0161i\u0165 jej v\xFDkon."},experience:{title:"Pou\u017E\xEDvate\u013Esk\xE1 sk\xFAsenos\u0165",description:"Tieto cookies n\xE1m pom\xE1haj\xFA poskytova\u0165 lep\u0161iu pou\u017E\xEDvate\u013Esk\xFA sk\xFAsenos\u0165 a testova\u0165 nov\xE9 funkcie."}},frame:{title:"Prijmite s\xFAhlas pre kateg\xF3riu {category} na zobrazenie tohto obsahu.",actionButton:"Povoli\u0165 s\xFAhlas pre {category}"},legalLinks:{privacyPolicy:"Z\xE1sady ochrany osobn\xFDch \xFAdajov",cookiePolicy:"Z\xE1sady pou\u017E\xEDvania s\xFAborov cookie",termsOfService:"Podmienky pou\u017E\xEDvania slu\u017Eby"},iab:{banner:{title:"Nastavenia s\xFAkromia",description:"My a na\u0161i {partnerCount} partneri uklad\xE1me a/alebo pristupujeme k inform\xE1ci\xE1m vo va\u0161om zariaden\xED a sprac\xFAvame osobn\xE9 \xFAdaje, ako s\xFA jedine\u010Dn\xE9 identifik\xE1tory a \xFAdaje o prehliadan\xED, pre t\xFAto webov\xFA str\xE1nku s cie\u013Eom:",partnersLink:"{count} partneri",andMore:"A \u010Fal\u0161\xEDch {count}...",legitimateInterestNotice:"Niektor\xED partneri si uplat\u0148uj\xFA opr\xE1vnen\xFD z\xE1ujem na sprac\xFAvanie va\u0161ich \xFAdajov. M\xE1te pr\xE1vo vznies\u0165 n\xE1mietku proti tomuto sprac\xFAvaniu, prisp\xF4sobi\u0165 svoje vo\u013Eby a kedyko\u013Evek odvola\u0165 svoj s\xFAhlas.",scopeServiceSpecific:"V\xE1\u0161 s\xFAhlas plat\xED len pre t\xFAto webov\xFA str\xE1nku a neovplyvn\xED in\xE9 slu\u017Eby.",scopeGroup:"Va\u0161a vo\u013Eba plat\xED pre v\u0161etky na\u0161e weby v tejto skupine."},preferenceCenter:{title:"Nastavenia s\xFAkromia",description:"Prisp\xF4sobte si nastavenia s\xFAkromia tu. M\xF4\u017Eete si vybra\u0165, ktor\xE9 typy cookies a sledovac\xEDch technol\xF3gi\xED povol\xEDte.",tabs:{purposes:"\xDA\u010Dely",vendors:"Dod\xE1vatelia"},purposeItem:{partners:"{count} partneri",vendorsUseLegitimateInterest:"{count} dod\xE1vatelia si uplat\u0148uj\xFA opr\xE1vnen\xFD z\xE1ujem",examples:"Pr\xEDklady",partnersUsingPurpose:"Partneri vyu\u017E\xEDvaj\xFAci tento \xFA\u010Del",withYourPermission:"S va\u0161\xEDm povolen\xEDm",legitimateInterest:"Opr\xE1vnen\xFD z\xE1ujem",objectButton:"Vznies\u0165 n\xE1mietku",objected:"N\xE1mietka vznesen\xE1",rightToObject:"M\xE1te pr\xE1vo vznies\u0165 n\xE1mietku proti sprac\xFAvaniu zalo\u017Een\xE9mu na opr\xE1vnenom z\xE1ujme."},specialPurposes:{title:"Z\xE1kladn\xE9 funkcie (povinn\xE9)",tooltip:"Tieto s\xFA potrebn\xE9 pre funk\u010Dnos\u0165 a bezpe\u010Dnos\u0165 str\xE1nky. Pod\u013Ea IAB TCF nem\xF4\u017Eete vznies\u0165 n\xE1mietku proti t\xFDmto osobitn\xFDm \xFA\u010Delom."},vendorList:{search:"H\u013Eada\u0165 dod\xE1vate\u013Eov...",showingCount:"Zobrazuje sa {filtered} z {total} dod\xE1vate\u013Eov",iabVendorsHeading:"Dod\xE1vatelia registrovan\xED v IAB",iabVendorsNotice:"T\xEDto partneri s\xFA registrovan\xED v r\xE1mci IAB Transparency & Consent Framework (TCF), priemyseln\xE9ho \u0161tandardu pre spr\xE1vu s\xFAhlasu",customVendorsHeading:"Vlastn\xED partneri",customVendorsNotice:"Toto s\xFA vlastn\xED partneri, ktor\xED nie s\xFA registrovan\xED v r\xE1mci IAB Transparency & Consent Framework (TCF). Sprac\xFAvaj\xFA \xFAdaje na z\xE1klade v\xE1\u0161ho s\xFAhlasu a m\xF4\u017Eu ma\u0165 in\xE9 postupy ochrany s\xFAkromia ako dod\xE1vatelia registrovan\xED v IAB.",purposes:"\xDA\u010Dely",specialPurposes:"Osobitn\xE9 \xFA\u010Dely",specialFeatures:"Osobitn\xE9 funkcie",features:"Funkcie",dataCategories:"Kateg\xF3rie \xFAdajov",usesCookies:"Pou\u017E\xEDva cookies",nonCookieAccess:"Pr\xEDstup bez cookies",maxAge:"Max. vek: {days}d",retention:"Uchov\xE1vanie: {days}d",legitimateInterest:"Opr\xE1v. z\xE1ujem",privacyPolicy:"Z\xE1sady ochrany s\xFAkromia",storageDisclosure:"Zverejnenie inform\xE1ci\xED o ukladan\xED",requiredNotice:"Vy\u017Eaduje sa pre funk\u010Dnos\u0165 str\xE1nky, nemo\u017Eno zak\xE1za\u0165"},footer:{consentStorage:'Predvo\u013Eby s\xFAhlasu s\xFA ulo\u017Een\xE9 v cookie s n\xE1zvom "euconsent-v2" po dobu 13 mesiacov. The storage duration may be refreshed when you update your preferences.'}},common:{acceptAll:"Prija\u0165 v\u0161etko",rejectAll:"Odmietnu\u0165 v\u0161etko",customize:"Prisp\xF4sobi\u0165",saveSettings:"Ulo\u017Ei\u0165 nastavenia",loading:"Na\u010D\xEDtava sa...",showingSelectedVendor:"Zobrazenie vybran\xE9ho dod\xE1vate\u013Ea",clearSelection:"Vymaza\u0165",customPartner:"Vlastn\xFD partner neregistrovan\xFD v IAB"}}},Vi={common:{acceptAll:"Sprejmi vse",rejectAll:"Zavrni vse",customize:"Prilagodi",save:"Shrani nastavitve"},cookieBanner:{title:"Cenimo va\u0161o zasebnost",description:"Ta spletna stran uporablja pi\u0161kotke za izbolj\u0161anje va\u0161e uporabni\u0161ke izku\u0161nje, analizo prometa na strani in prikaz personaliziranih vsebin."},consentManagerDialog:{title:"Nastavitve zasebnosti",description:"Tukaj prilagodite svoje nastavitve zasebnosti. Izberete lahko, katere vrste pi\u0161kotkov in tehnologij sledenja dovolite."},consentTypes:{necessary:{title:"Nujno potrebni",description:"Ti pi\u0161kotki so bistveni za pravilno delovanje spletne strani in jih ni mogo\u010De onemogo\u010Diti."},functionality:{title:"Funkcionalnost",description:"Ti pi\u0161kotki omogo\u010Dajo izbolj\u0161ano funkcionalnost in personalizacijo spletne strani."},marketing:{title:"Tr\u017Eenje",description:"Ti pi\u0161kotki se uporabljajo za prikazovanje relevantnih oglasov in spremljanje njihove u\u010Dinkovitosti."},measurement:{title:"Analitika",description:"Ti pi\u0161kotki nam pomagajo razumeti, kako obiskovalci uporabljajo spletno stran, in izbolj\u0161ati njeno delovanje."},experience:{title:"Izku\u0161nja",description:"Ti pi\u0161kotki nam pomagajo zagotoviti bolj\u0161o uporabni\u0161ko izku\u0161njo in testirati nove funkcije."}},frame:{title:"Za ogled te vsebine sprejmite soglasje za kategorijo {category}.",actionButton:"Omogo\u010Di soglasje za {category}"},legalLinks:{privacyPolicy:"Pravilnik o zasebnosti",cookiePolicy:"Pravilnik o pi\u0161kotkih",termsOfService:"Pogoji uporabe"},iab:{banner:{title:"Nastavitve zasebnosti",description:"Mi in na\u0161ih {partnerCount} partnerjev shranjujemo in/ali dostopamo do informacij na va\u0161i napravi ter obdelujemo osebne podatke, kot so edinstveni identifikatorji in podatki o brskanju, za to spletno mesto, da bi:",partnersLink:"{count} partnerjev",andMore:"In \u0161e {count}...",legitimateInterestNotice:"Nekateri partnerji uveljavljajo zakoniti interes za obdelavo va\u0161ih podatkov. Imate pravico do ugovora tej obdelavi, prilagoditve svojih izbir in preklica soglasja kadar koli.",scopeServiceSpecific:"Va\u0161e soglasje velja samo za to spletno mesto in ne bo vplivalo na druge storitve.",scopeGroup:"Va\u0161a izbira velja za vse na\u0161e spletne strani v tej skupini."},preferenceCenter:{title:"Nastavitve zasebnosti",description:"Tukaj prilagodite svoje nastavitve zasebnosti. Izberete lahko, katere vrste pi\u0161kotkov in tehnologij sledenja dovolite.",tabs:{purposes:"Nameni",vendors:"Ponudniki"},purposeItem:{partners:"{count} partnerjev",vendorsUseLegitimateInterest:"{count} ponudnikov uveljavlja zakoniti interes",examples:"Primeri",partnersUsingPurpose:"Partnerji, ki uporabljajo ta namen",withYourPermission:"Z va\u0161im dovoljenjem",legitimateInterest:"Zakoniti interes",objectButton:"Ugovarjaj",objected:"Ugovarjano",rightToObject:"Imate pravico do ugovora obdelavi, ki temelji na zakonitem interesu."},specialPurposes:{title:"Bistvene funkcije (obvezno)",tooltip:"Te so potrebne for funkcionalnost in varnost spletnega mesta. V skladu z IAB TCF ne morete ugovarjati tem posebnim namenom."},vendorList:{search:"I\u0161\u010Di ponudnike...",showingCount:"Prikazano {filtered} od {total} ponudnikov",iabVendorsHeading:"Ponudniki, registrirani v IAB",iabVendorsNotice:"Ti partnerji so registrirani v okviru IAB Transparency & Consent Framework (TCF), industrijskega standarda za upravljanje soglasij",customVendorsHeading:"Partnerji po meri",customVendorsNotice:"To so partnerji po meri, ki niso registrirani v okviru IAB Transparency & Consent Framework (TCF). Podatke obdelujejo na podlagi va\u0161ega soglasja in imajo lahko druga\u010Dne prakse zasebnosti kot ponudniki, registrirani v IAB.",purposes:"Nameni",specialPurposes:"Posebni nameni",specialFeatures:"Posebne funkcije",features:"Funkcije",dataCategories:"Kategorije podatkov",usesCookies:"Uporablja pi\u0161kotke",nonCookieAccess:"Dostop brez pi\u0161kotkov",maxAge:"Najv. starost: {days}d",retention:"Hramba: {days}d",legitimateInterest:"Zakoniti int.",privacyPolicy:"Pravilnik o zasebnosti",storageDisclosure:"Razkritje shranjevanja",requiredNotice:"Zahtevano za delovanje spletnega mesta, ni mogo\u010De onemogo\u010Diti"},footer:{consentStorage:'Preference glede soglasja so shranjene v pi\u0161kotku z imenom "euconsent-v2" 13 mesecev. The storage duration may be refreshed when you update your preferences.'}},common:{acceptAll:"Sprejmi vse",rejectAll:"Zavrni vse",customize:"Prilagodi",saveSettings:"Shrani nastavitve",loading:"Nalaganje...",showingSelectedVendor:"Prikaz izbranega ponudnika",clearSelection:"Po\u010Disti",customPartner:"Partner po meri, ki ni registriran v IAB"}}},xi={common:{acceptAll:"Acceptera alla",rejectAll:"Avvisa alla",customize:"Anpassa",save:"Spara inst\xE4llningar"},cookieBanner:{title:"Vi v\xE4rdes\xE4tter din integritet",description:"Den h\xE4r webbplatsen anv\xE4nder cookies f\xF6r att f\xF6rb\xE4ttra din surfupplevelse, analysera webbplatstrafik och visa personligt anpassat inneh\xE5ll."},consentManagerDialog:{title:"Integritetsinst\xE4llningar",description:"Anpassa dina integritetsinst\xE4llningar h\xE4r. Du kan v\xE4lja vilka typer av cookies och sp\xE5rningstekniker du till\xE5ter."},consentTypes:{necessary:{title:"Absolut n\xF6dv\xE4ndiga",description:"Dessa cookies \xE4r n\xF6dv\xE4ndiga f\xF6r att webbplatsen ska fungera korrekt och kan inte inaktiveras."},functionality:{title:"Funktionalitet",description:"Dessa cookies m\xF6jligg\xF6r f\xF6rb\xE4ttrad funktionalitet och personalisering av webbplatsen."},marketing:{title:"Marknadsf\xF6ring",description:"Dessa cookies anv\xE4nds f\xF6r att leverera relevanta annonser och sp\xE5ra deras effektivitet."},measurement:{title:"Analys",description:"Dessa cookies hj\xE4lper oss att f\xF6rst\xE5 hur bes\xF6kare interagerar med webbplatsen och f\xF6rb\xE4ttra dess prestanda."},experience:{title:"Upplevelse",description:"Dessa cookies hj\xE4lper oss att ge en b\xE4ttre anv\xE4ndarupplevelse och testa nya funktioner."}},frame:{title:"Acceptera {category}-samtycke f\xF6r att visa detta inneh\xE5ll.",actionButton:"Aktivera {category}-samtycke"},legalLinks:{privacyPolicy:"Integritetspolicy",cookiePolicy:"Cookiepolicy",termsOfService:"Anv\xE4ndarvillkor"},iab:{banner:{title:"Integritetsinst\xE4llningar",description:"Vi och v\xE5ra {partnerCount} partner lagrar och/eller f\xE5r tillg\xE5ng till information p\xE5 din enhet och behandlar personuppgifter, s\xE5som unika identifierare och webbl\xE4sardata, f\xF6r denna webbplats, f\xF6r att:",partnersLink:"{count} partner",andMore:"Och {count} till...",legitimateInterestNotice:"Vissa partner h\xE4vdar ett ber\xE4ttigat intresse f\xF6r att behandla dina uppgifter. Du har r\xE4tt att inv\xE4nda mot denna behandling, anpassa dina val och n\xE4r som helst \xE5terkalla ditt samtycke.",scopeServiceSpecific:"Ditt samtycke g\xE4ller endast f\xF6r den h\xE4r webbplatsen och p\xE5verkar inte andra tj\xE4nster.",scopeGroup:"Ditt val g\xE4ller f\xF6r alla v\xE5ra webbplatser i denna grupp."},preferenceCenter:{title:"Integritetsinst\xE4llningar",description:"Anpassa dina integritetsinst\xE4llningar h\xE4r. Du kan v\xE4lja vilka typer av cookies och sp\xE5rningstekniker du till\xE5ter.",tabs:{purposes:"\xC4ndam\xE5l",vendors:"Leverant\xF6rer"},purposeItem:{partners:"{count} partner",vendorsUseLegitimateInterest:"{count} leverant\xF6rer h\xE4vdar ber\xE4ttigat intresse",examples:"Exempel",partnersUsingPurpose:"Partner som anv\xE4nder detta \xE4ndam\xE5l",withYourPermission:"Med ditt tillst\xE5nd",legitimateInterest:"Ber\xE4ttigat intresse",objectButton:"Inv\xE4nd",objected:"Inv\xE4nt",rightToObject:"Du har r\xE4tt att inv\xE4nda mot behandling baserad p\xE5 ber\xE4ttigat intresse."},specialPurposes:{title:"Viktiga funktioner (kr\xE4vs)",tooltip:"Dessa kr\xE4vs f\xF6r webbplatsens funktionalitet och s\xE4kerhet. Enligt IAB TCF kan du inte inv\xE4nda mot dessa speciella \xE4ndam\xE5l."},vendorList:{search:"S\xF6k leverant\xF6rer...",showingCount:"{filtered} av {total} leverant\xF6rer",iabVendorsHeading:"IAB-registrerade leverant\xF6rer",iabVendorsNotice:"Dessa partner \xE4r registrerade i IAB Transparency & Consent Framework (TCF), en branschstandard f\xF6r hantering av samtycke",customVendorsHeading:"Anpassade partner",customVendorsNotice:"Dessa \xE4r anpassade partner som inte \xE4r registrerade i IAB Transparency & Consent Framework (TCF). De behandlar data baserat p\xE5 ditt samtycke och kan ha andra integritetspraxis \xE4n IAB-registrerade leverant\xF6rer.",purposes:"\xC4ndam\xE5l",specialPurposes:"Speciella \xE4ndam\xE5l",specialFeatures:"Speciella funktioner",features:"Funktioner",dataCategories:"Datakategorier",usesCookies:"Anv\xE4nder cookies",nonCookieAccess:"Icke-cookie-\xE5tkomst",maxAge:"Max \xE5lder: {days}d",retention:"Lagring: {days}d",legitimateInterest:"Ber\xE4tt. intresse",privacyPolicy:"Integritetspolicy",storageDisclosure:"Lagringsinformation",requiredNotice:"Kr\xE4vs f\xF6r webbplatsens funktionalitet, kan inte inaktiveras"},footer:{consentStorage:'Samtyckesinst\xE4llningar lagras i en cookie med namnet "euconsent-v2" i 13 m\xE5nader. The storage duration may be refreshed when you update your preferences.'}},common:{acceptAll:"Acceptera alla",rejectAll:"Avvisa alla",customize:"Anpassa",saveSettings:"Spara inst\xE4llningar",loading:"Laddar...",showingSelectedVendor:"Visar vald leverant\xF6r",clearSelection:"Rensa",customPartner:"Anpassad partner som inte \xE4r registrerad i IAB"}}},Fi={common:{acceptAll:"\u5168\u90E8\u540C\u610F",rejectAll:"\u5168\u90E8\u62D2\u7EDD",customize:"\u81EA\u5B9A\u4E49\u8BBE\u7F6E",save:"\u4FDD\u5B58\u8BBE\u7F6E"},cookieBanner:{title:"\u6211\u4EEC\u91CD\u89C6\u60A8\u7684\u9690\u79C1",description:"\u672C\u7F51\u7AD9\u4F7F\u7528cookies\u6765\u63D0\u5347\u60A8\u7684\u6D4F\u89C8\u4F53\u9A8C\u3001\u5206\u6790\u7F51\u7AD9\u6D41\u91CF\u5E76\u5C55\u793A\u4E2A\u6027\u5316\u5185\u5BB9\u3002"},consentManagerDialog:{title:"\u9690\u79C1\u8BBE\u7F6E",description:"\u5728\u6B64\u81EA\u5B9A\u4E49\u60A8\u7684\u9690\u79C1\u8BBE\u7F6E\u3002\u60A8\u53EF\u4EE5\u9009\u62E9\u5141\u8BB8\u54EA\u4E9B\u7C7B\u578B\u7684cookies\u548C\u8DDF\u8E2A\u6280\u672F\u3002"},consentTypes:{necessary:{title:"\u4E25\u683C\u5FC5\u8981\u7C7B",description:"\u8FD9\u4E9Bcookies\u662F\u7F51\u7AD9\u6B63\u5E38\u8FD0\u884C\u6240\u5FC5\u9700\u7684\uFF0C\u65E0\u6CD5\u88AB\u7981\u7528\u3002"},functionality:{title:"\u529F\u80FD\u7C7B",description:"\u8FD9\u4E9Bcookies\u53EF\u589E\u5F3A\u7F51\u7AD9\u7684\u529F\u80FD\u548C\u4E2A\u6027\u5316\u4F53\u9A8C\u3002"},marketing:{title:"\u8425\u9500\u7C7B",description:"\u8FD9\u4E9Bcookies\u7528\u4E8E\u6295\u653E\u76F8\u5173\u5E7F\u544A\u5E76\u8DDF\u8E2A\u5E7F\u544A\u6548\u679C\u3002"},measurement:{title:"\u5206\u6790\u7C7B",description:"\u8FD9\u4E9Bcookies\u5E2E\u52A9\u6211\u4EEC\u4E86\u89E3\u8BBF\u5BA2\u5982\u4F55\u4E0E\u7F51\u7AD9\u4E92\u52A8\u5E76\u6539\u8FDB\u5176\u6027\u80FD\u3002"},experience:{title:"\u4F53\u9A8C\u7C7B",description:"\u8FD9\u4E9Bcookies\u5E2E\u52A9\u6211\u4EEC\u63D0\u4F9B\u66F4\u597D\u7684\u7528\u6237\u4F53\u9A8C\u5E76\u6D4B\u8BD5\u65B0\u529F\u80FD\u3002"}},frame:{title:"\u63A5\u53D7 {category} \u4EE5\u67E5\u770B\u6B64\u5185\u5BB9\u3002",actionButton:"\u542F\u7528 {category} \u540C\u610F"},legalLinks:{privacyPolicy:"\u9690\u79C1\u653F\u7B56",cookiePolicy:"Cookie\u653F\u7B56",termsOfService:"\u670D\u52A1\u6761\u6B3E"},iab:{banner:{title:"\u9690\u79C1\u8BBE\u7F6E",description:"\u6211\u4EEC\u548C\u6211\u4EEC\u7684 {partnerCount} \u4E2A\u5408\u4F5C\u4F19\u4F34\u5728\u60A8\u7684\u8BBE\u5907\u4E0A\u5B58\u50A8\u548C/\u6216\u8BBF\u95EE\u4FE1\u606F\uFF0C\u5E76\u4E3A\u6B64\u7F51\u7AD9\u5904\u7406\u4E2A\u4EBA\u6570\u636E\uFF08\u5982\u552F\u4E00\u6807\u8BC6\u7B26\u548C\u6D4F\u89C8\u6570\u636E\uFF09\uFF0C\u4EE5\u4FBF\uFF1A",partnersLink:"{count} \u4E2A\u5408\u4F5C\u4F19\u4F34",andMore:"\u8FD8\u6709 {count} \u4E2A...",legitimateInterestNotice:"\u67D0\u4E9B\u5408\u4F5C\u4F19\u4F34\u58F0\u79F0\u5BF9\u5904\u7406\u60A8\u7684\u6570\u636E\u5177\u6709\u6B63\u5F53\u5229\u76CA\u3002\u60A8\u6709\u6743\u53CD\u5BF9\u8FD9\u79CD\u5904\u7406\u3001\u81EA\u5B9A\u4E49\u60A8\u7684\u9009\u62E9\u5E76\u968F\u65F6\u64A4\u56DE\u60A8\u7684\u540C\u610F\u3002",scopeServiceSpecific:"\u60A8\u7684\u540C\u610F\u4EC5\u9002\u7528\u4E8E\u672C\u7F51\u7AD9\uFF0C\u4E0D\u4F1A\u5F71\u54CD\u5176\u4ED6\u670D\u52A1\u3002",scopeGroup:"\u60A8\u7684\u9009\u62E9\u9002\u7528\u4E8E\u672C\u7EC4\u5185\u7684\u6240\u6709\u7F51\u7AD9\u3002"},preferenceCenter:{title:"\u9690\u79C1\u8BBE\u7F6E",description:"\u5728\u6B64\u81EA\u5B9A\u4E49\u60A8\u7684\u9690\u79C1\u8BBE\u7F6E\u3002\u60A8\u53EF\u4EE5\u9009\u62E9\u5141\u8BB8\u54EA\u4E9B\u7C7B\u578B\u7684 cookies \u548C\u8DDF\u8E2A\u6280\u672F\u3002",tabs:{purposes:"\u76EE\u7684",vendors:"\u4F9B\u5E94\u5546"},purposeItem:{partners:"{count} \u4E2A\u5408\u4F5C\u4F19\u4F34",vendorsUseLegitimateInterest:"{count} \u4E2A\u4F9B\u5E94\u5546\u58F0\u79F0\u5177\u6709\u6B63\u5F53\u5229\u76CA",examples:"\u793A\u4F8B",partnersUsingPurpose:"\u4F7F\u7528\u6B64\u76EE\u7684\u7684\u5408\u4F5C\u4F19\u4F34",withYourPermission:"\u5F81\u5F97\u60A8\u7684\u8BB8\u53EF",legitimateInterest:"\u6B63\u5F53\u5229\u76CA",objectButton:"\u53CD\u5BF9",objected:"\u5DF2\u53CD\u5BF9",rightToObject:"\u60A8\u6709\u6743\u53CD\u5BF9\u57FA\u4E8E\u6B63\u5F53\u5229\u76CA\u7684\u5904\u7406\u3002"},specialPurposes:{title:"\u57FA\u672C\u529F\u80FD\uFF08\u5FC5\u9700\uFF09",tooltip:"\u8FD9\u4E9B\u662F\u7F51\u7AD9\u529F\u80FD\u548C\u5B89\u5168\u6240\u5FC5\u9700\u7684\u3002\u6839\u636E IAB TCF\uFF0C\u60A8\u4E0D\u80FD\u53CD\u5BF9\u8FD9\u4E9B\u7279\u6B8A\u76EE\u7684\u3002"},vendorList:{search:"\u641C\u7D22\u4F9B\u5E94\u5546...",showingCount:"\u663E\u793A {total} \u4E2A\u4F9B\u5E94\u5546\u4E2D\u7684 {filtered} \u4E2A",iabVendorsHeading:"IAB \u6CE8\u518C\u4F9B\u5E94\u5546",iabVendorsNotice:"\u8FD9\u4E9B\u5408\u4F5C\u4F19\u4F34\u5DF2\u5728 IAB \u900F\u660E\u5EA6\u4E0E\u540C\u610F\u6846\u67B6 (TCF) \u6CE8\u518C\uFF0C\u8FD9\u662F\u7BA1\u7406\u540C\u610F\u7684\u884C\u4E1A\u6807\u51C6",customVendorsHeading:"\u81EA\u5B9A\u4E49\u5408\u4F5C\u4F19\u4F34",customVendorsNotice:"\u8FD9\u4E9B\u662F\u672A\u5728 IAB \u900F\u660E\u5EA6\u4E0E\u540C\u610F\u6846\u67B6 (TCF) \u6CE8\u518C\u7684\u81EA\u5B9A\u4E49\u5408\u4F5C\u4F19\u4F34\u3002\u4ED6\u4EEC\u6839\u636E\u60A8\u7684\u540C\u610F\u5904\u7406\u6570\u636E\uFF0C\u5E76\u4E14\u53EF\u80FD\u5177\u6709\u4E0E IAB \u6CE8\u518C\u4F9B\u5E94\u5546\u4E0D\u540C\u7684\u9690\u79C1\u60EF\u4F8B\u3002",purposes:"\u76EE\u7684",specialPurposes:"\u7279\u6B8A\u76EE\u7684",specialFeatures:"\u7279\u6B8A\u529F\u80FD",features:"\u529F\u80FD",dataCategories:"\u6570\u636E\u7C7B\u522B",usesCookies:"\u4F7F\u7528 Cookies",nonCookieAccess:"\u975E Cookie \u8BBF\u95EE",maxAge:"\u6700\u957F\u671F\u9650\uFF1A{days}\u5929",retention:"\u4FDD\u7559\u671F\u9650\uFF1A{days}\u5929",legitimateInterest:"\u6B63\u5F53\u5229\u76CA",privacyPolicy:"\u9690\u79C1\u653F\u7B56",storageDisclosure:"\u5B58\u50A8\u62AB\u9732",requiredNotice:"\u7F51\u7AD9\u529F\u80FD\u5FC5\u9700\uFF0C\u65E0\u6CD5\u7981\u7528"},footer:{consentStorage:'\u540C\u610F\u504F\u597D\u5B58\u50A8\u5728\u540D\u4E3A "euconsent-v2" \u7684 cookie \u4E2D\uFF0C\u6709\u6548\u671F\u4E3A 13 \u4E2A\u6708\u3002 The storage duration may be refreshed when you update your preferences.'}},common:{acceptAll:"\u5168\u90E8\u540C\u610F",rejectAll:"\u5168\u90E8\u62D2\u7EDD",customize:"\u81EA\u5B9A\u4E49\u8BBE\u7F6E",saveSettings:"\u4FDD\u5B58\u8BBE\u7F6E",loading:"\u52A0\u8F7D\u4E2D...",showingSelectedVendor:"\u663E\u793A\u9009\u5B9A\u7684\u4F9B\u5E94\u5546",clearSelection:"\u6E05\u9664",customPartner:"\u672A\u5728 IAB \u6CE8\u518C\u7684\u81EA\u5B9A\u4E49\u5408\u4F5C\u4F19\u4F34"}}},Bi={bg:ii,cs:si,da:oi,de:ai,el:ci,en:it,es:li,et:ui,fi:di,fr:pi,ga:gi,he:mi,hr:fi,hu:hi,id:ki,it:yi,lt:wi,lv:Ci,mt:Ii,nl:Si,pl:zi,pt:Pi,ro:Li,sk:Ei,sl:Vi,sv:xi,zh:Fi,is:vi,nb:ji,nn:Ai,lb:bi,rm:Ti,cy:ri};function _t(n){return!(!n||typeof n!="object"||Array.isArray(n))}function Ot(n,e){if(!n&&!e)return{};let t={};if(n)for(let i of Object.keys(n))t[i]=n[i];if(!e)return t;for(let i of Object.keys(e)){let s=e[i];if(s===void 0)continue;let r=n?n[i]:void 0;_t(r)&&_t(s)?t[i]=Ot(r,s):t[i]=s}return t}function Rt(n,e){let t=["cookieBanner","consentManagerDialog","common","consentTypes","frame","legalLinks","iab"],i={};for(let s of t){let r=n[s],o=e[s];(r||o)&&(i[s]=Ot(r,o))}return i}function Mt(n){return n?n.split(",").map(e=>e.split(";")[0]?.trim().toLowerCase()).filter(e=>!!e).map(e=>e.split("-")[0]??e):[]}function Di(n,e){let t=e?.fallback??"en";if(!n.length)return t;let i=Mt(e?.header);for(let s of i)if(n.includes(s))return s;return t}function Ut(n,e){let t={en:it},i=[n.translations,e?.translations];for(let s of i)if(s)for(let[r,o]of Object.entries(s)){if(!o)continue;let a=t[r]||t.en;t[r]=Rt(a,o)}return{...n,...e,translations:t}}function Gt(n,e,t=!1){if(t||typeof window>"u")return e||"en";let i=window.navigator.language?.split("-")[0]||"";return i&&i in n?i:e||"en"}function Ni(n,e){let t=Ut(n,e),i=Gt(t.translations,t.defaultLanguage,t.disableAutoLanguageSwitch);return{...t,defaultLanguage:i}}var Ht=n=>{let e,t=new Set,i=(u,p)=>{let d=typeof u=="function"?u(e):u;if(!Object.is(d,e)){let l=e;e=p??(typeof d!="object"||d===null)?d:Object.assign({},e,d),t.forEach(y=>y(e,l))}},s=()=>e,a={setState:i,getState:s,getInitialState:()=>c,subscribe:u=>(t.add(u),()=>t.delete(u))},c=e=n(i,s,a);return a},qt=(n=>n?Ht(n):Ht);var _i={"./src/libs/cookie/index.ts"(n,e,t){t.d(e,{_y:()=>D,If:()=>O,TV:()=>y,Yj:()=>b,Xk:()=>s,jD:()=>X,Ri:()=>m});function i(g){return{expiryDays:g?.defaultExpiryDays??365,crossSubdomain:g?.crossSubdomain??!1,domain:g?.defaultDomain??"",path:"/",secure:typeof window<"u"&&window.location.protocol==="https:",sameSite:"Lax"}}function s(){if(typeof window>"u")return"";let g=window.location.hostname;if(g==="localhost"||/^\d+\.\d+\.\d+\.\d+$/.test(g))return g;let f=g.split(".");return f.length>=2?`.${f.slice(-2).join(".")}`:g}let r={consents:"c",consentInfo:"i",timestamp:"ts",iabCustomVendorConsents:"icv",iabCustomVendorLegitimateInterests:"icvli",time:"t",type:"y",id:"id",subjectId:"sid",externalId:"eid",identityProvider:"idp"},o=Object.entries(r).reduce((g,[f,k])=>(g[k]=f,g),{});function a(g){let f={};for(let[k,j]of Object.entries(g)){let z=k.split(".").map(T=>r[T]||T);f[z.join(".")]=j}return f}function c(g){let f={};for(let[k,j]of Object.entries(g)){let z=k.split(".").map(T=>o[T]||T);f[z.join(".")]=j}return f}function u(g,f=""){let k={};for(let[j,I]of Object.entries(g)){let z=f?`${f}.${j}`:j;I==null?k[z]="":typeof I=="boolean"?I&&(k[z]="1"):typeof I!="object"||Array.isArray(I)?k[z]=String(I):Object.assign(k,u(I,z))}return k}function p(g){let f={};for(let[k,j]of Object.entries(g)){let I=k.split(".");if(I.length===0)continue;let z=f;for(let E=0;E`${f}:${k}`).join(",")}function l(g){if(!g)return{};let f={},k=g.split(",");for(let j of k){let I=j.indexOf(":");if(I===-1)continue;let z=j.substring(0,I),T=j.substring(I+1);f[z]=T}return f}function y(g,f,k,j){if(typeof document>"u")return;let I={...i(j),...k};I.crossSubdomain&&!k?.domain&&(I.domain=s());try{let z;if(typeof f=="string")z=f;else{let _=u(f),R=a(_);z=d(R)}let T=new Date;T.setTime(T.getTime()+24*I.expiryDays*36e5);let E=`expires=${T.toUTCString()}`,V=[`${g}=${z}`,E,`path=${I.path}`];I.domain&&V.push(`domain=${I.domain}`),I.secure&&V.push("secure"),I.sameSite&&V.push(`SameSite=${I.sameSite}`),document.cookie=V.join("; ")}catch(z){console.warn(`Failed to set cookie "${g}":`,z)}}function m(g){if(typeof document>"u")return null;try{let f=`${g}=`,k=document.cookie.split(";");for(let j of k){let I=j;for(;I.charAt(0)===" ";)I=I.substring(1);if(I.indexOf(f)===0){let z=I.substring(f.length);if(z.includes(":")){let T=l(z),E=c(T);return p(E)}return z}}return null}catch(f){return console.warn(`Failed to get cookie "${g}":`,f),null}}function b(g,f,k){if(typeof document>"u")return;let j={...i(k),...f};j.crossSubdomain&&!f?.domain&&(j.domain=s());try{let I=[`${g}=`,"expires=Thu, 01 Jan 1970 00:00:00 GMT",`path=${j.path}`];j.domain&&I.push(`domain=${j.domain}`),document.cookie=I.join("; ")}catch(I){console.warn(`Failed to delete cookie "${g}":`,I)}}var h=t("./src/store/initial-state.ts"),w=t("./src/types/consent-types.ts"),C=t("./src/libs/debug.ts");function A(g){if(typeof g!="object"||g===null)return!1;let k=g.consentInfo;if(!k||typeof k!="object")return!1;let j=typeof k.id=="string",I=typeof k.subjectId=="string";return j&&!I}function x(g){let f=g?.storageKey||h.ln,k=h.AQ;if(f!==k)try{if(typeof window<"u"&&window.localStorage){if(window.localStorage.getItem(f))return void window.localStorage.removeItem(k);let I=window.localStorage.getItem(k);I&&(window.localStorage.setItem(f,I),window.localStorage.removeItem(k),(0,C.YA)().log(`Migrated consent data from "${k}" to "${f}"`))}}catch(j){console.warn("[c15t] Failed to migrate legacy storage:",j)}}function D(g,f,k){let j=!1,I=!1,z=k?.storageKey||h.ln,T=O(k),V={...{...T,...g,iabCustomVendorConsents:g.iabCustomVendorConsents??T?.iabCustomVendorConsents,iabCustomVendorLegitimateInterests:g.iabCustomVendorLegitimateInterests??T?.iabCustomVendorLegitimateInterests}};(!V.iabCustomVendorConsents||Object.keys(V.iabCustomVendorConsents).length===0)&&delete V.iabCustomVendorConsents,(!V.iabCustomVendorLegitimateInterests||Object.keys(V.iabCustomVendorLegitimateInterests).length===0)&&delete V.iabCustomVendorLegitimateInterests;try{typeof window<"u"&&window.localStorage&&(window.localStorage.setItem(z,JSON.stringify(V)),j=!0)}catch(_){console.warn("Failed to save consent to localStorage:",_)}try{y(z,V,f,k),I=!0}catch(_){console.warn("Failed to save consent to cookie:",_)}if(!j&&!I)throw new Error("Failed to save consent to any storage method")}function F(g){let f=g.consents||{},k={...f};for(let j of w.W)k[j]=f[j]??!1;return{...g,consents:k}}function O(g){x(g);let f=g?.storageKey||h.ln,k=null,j=null;try{if(typeof window<"u"&&window.localStorage){let T=window.localStorage.getItem(f);T&&(k=JSON.parse(T))}}catch(T){console.warn("Failed to read consent from localStorage:",T)}try{j=m(f)}catch(T){console.warn("Failed to read consent from cookie:",T)}let I=null,z=null;if(j?(I=j,z="cookie"):k&&(I=k,z="localStorage"),I&&z){let T=g?.crossSubdomain===!0||!!g?.defaultDomain;if(z!=="localStorage"||j){if(z==="cookie")try{if(typeof window<"u"&&window.localStorage){let E=I;typeof E=="object"&&E!==null&&"consents"in E&&(E=F(E));let V=null;try{let ue=window.localStorage.getItem(f);if(ue){let re=JSON.parse(ue);V=typeof re=="object"&&re!==null&&"consents"in re?F(re):re}}catch{V=null}let _=JSON.stringify(E),R=JSON.stringify(V);_!==R&&(window.localStorage.setItem(f,_),V?T?(0,C.YA)().log("Updated localStorage with consent from cookie (cross-subdomain mode)"):(0,C.YA)().log("Updated localStorage with consent from cookie"):(0,C.YA)().log("Synced consent from cookie to localStorage"))}}catch(E){console.warn("[c15t] Failed to sync consent to localStorage:",E)}}else try{y(f,I,void 0,g),(0,C.YA)().log("Synced consent from localStorage to cookie")}catch(E){console.warn("[c15t] Failed to sync consent to cookie:",E)}}return I&&A(I)?((0,C.YA)().log("Detected legacy consent format (v1.x). Re-consent required for v2.0."),X(void 0,g),null):I&&typeof I=="object"?F(I):I}function X(g,f){let k=f?.storageKey||h.ln;try{typeof window<"u"&&window.localStorage&&(window.localStorage.removeItem(k),k!==h.AQ&&window.localStorage.removeItem(h.AQ))}catch(j){console.warn("Failed to remove consent from localStorage:",j)}try{b(k,g,f),k!==h.AQ&&b(h.AQ,g,f)}catch(j){console.warn("Failed to remove consent cookie:",j)}}},"./src/libs/debug.ts"(n,e,t){t.d(e,{YA:()=>o,tJ:()=>a});let i=()=>{};function s(c){return c?{log:(...u)=>console.log("[c15t]",...u),debug:(...u)=>console.debug("[c15t]",...u)}:{log:i,debug:i}}let r=s(!1);function o(){return r}function a(c){r=s(c)}},"./src/libs/generate-subject-id.ts"(n,e,t){t.d(e,{L:()=>o,U:()=>a});let i="123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";function s(c){let u=BigInt(58),p=BigInt(0);for(let l of c)p=p*BigInt(256)+BigInt(l);let d=[];for(;p>0;){let l=p%u;d.unshift(i.charAt(Number(l))),p/=u}for(let l of c)if(l===0)d.unshift(i.charAt(0));else break;return d.join("")||i.charAt(0)}let r=17e11;function o(){let c=crypto.getRandomValues(new Uint8Array(20)),u=Date.now()-r,p=Math.floor(u/4294967296),d=u>>>0;return c[0]=p>>>24&255,c[1]=p>>>16&255,c[2]=p>>>8&255,c[3]=255&p,c[4]=d>>>24&255,c[5]=d>>>16&255,c[6]=d>>>8&255,c[7]=255&d,`sub_${s(c)}`}function a(c){if(!c.startsWith("sub_"))return!1;let u=c.slice(4);if(u.length===0)return!1;for(let p of u)if(!i.includes(p))return!1;return!0}},"./src/libs/iab-tcf/cmp-defaults.ts"(n,e,t){t.d(e,{D:()=>s,I:()=>r});var i=t("./src/version.ts");let s=0,r=i.r},"./src/libs/iab-tcf/fetch-gvl.ts"(n,e,t){t.d(e,{Ww:()=>a,ix:()=>o,wL:()=>p,xe:()=>c});var i=t("./src/libs/iab-tcf/types.ts");let s=new Map,r;async function o(d,l={}){let y=typeof window<"u"?window.__c15t_mock_gvl:void 0;if(y!==void 0)return r=y,y;if(u!==void 0)return r=u,u;let{endpoint:m=i.w,headers:b}=l,h=d?[...d].sort((F,O)=>F-O):[],w=b?JSON.stringify(b):"",C=`${m}|${h.join(",")}|${w}`,A=s.get(C);if(A)return A;let x=new URL(m);h.length>0&&x.searchParams.set("vendorIds",h.join(","));let D=(async()=>{try{let F=await fetch(x.toString(),{headers:b});if(F.status===204)return r=null,null;if(!F.ok)throw new Error(`Failed to fetch GVL: ${F.status} ${F.statusText}`);let O=await F.json();if(!O.vendorListVersion||!O.purposes||!O.vendors)throw new Error("Invalid GVL response: missing required fields");return r=O,O}finally{s.delete(C)}})();return s.set(C,D),D}function a(){return r}function c(){s.clear(),r=void 0,u=void 0}let u;function p(d){u=d,d!==void 0&&(r=d)}},"./src/libs/iab-tcf/index.ts"(n,e,t){t.d(e,{generateTCString:()=>h,initializeIABStub:()=>p,wL:()=>F.wL,Ww:()=>F.Ww,decodeTCString:()=>w,xe:()=>F.xe,fetchGVL:()=>F.ix,iabPurposesToC15tConsents:()=>X,createCMPApi:()=>D});var i=t("./src/libs/iab-tcf/cmp-defaults.ts"),s=t("./src/version.ts");let r=!1;function o(){return{gdprApplies:void 0,cmpLoaded:!1,cmpStatus:"stub",displayStatus:"hidden",apiVersion:"2.3",cmpVersion:s.r,cmpId:0,gvlVersion:0,tcfPolicyVersion:5}}function a(){let g=[],f=(k,j,I,z)=>{k==="ping"?I(o(),!0):g.push([k,j,I,z])};return f.queue=g,f}function c(){if(typeof document>"u"||document.querySelector('iframe[name="__tcfapiLocator"]'))return null;let g=document.createElement("iframe");return g.name="__tcfapiLocator",g.style.display="none",g.setAttribute("aria-hidden","true"),g.tabIndex=-1,(document.body??document.documentElement).appendChild(g),g}function u(g){if(typeof window>"u"||!window.__tcfapi)return;let{data:f}=g;if(!f||typeof f!="object"||!("__tcfapiCall"in f))return;let k=f.__tcfapiCall;!k||!k.command||!k.callId||window.__tcfapi(k.command,k.version,(j,I)=>{let z={__tcfapiReturn:{returnValue:j,success:I,callId:k.callId}};g.source&&typeof g.source.postMessage=="function"&&g.source.postMessage(z,"*")},k.parameter)}function p(){typeof window>"u"||r||(window.__tcfapi||(window.__tcfapi=a()),c(),window.addEventListener("message",u),r=!0)}function d(){return typeof window>"u"||!window.__tcfapi?[]:window.__tcfapi.queue??[]}function l(){typeof window<"u"&&window.__tcfapi?.queue&&(window.__tcfapi.queue=[])}let y=null,m=null;async function b(){return y||m||(m=Promise.resolve().then(()=>(An(),Sn)).then(g=>(y=g,m=null,g)).catch(g=>{throw m=null,new Error(`Failed to load @iabtechlabtcf/core: ${g instanceof Error?g.message:"Unknown error"}. Make sure it is installed as a dependency.`)}),m)}async function h(g,f,k){let{TCModel:j,TCString:I,GVL:z}=await b(),T=new z(f),E=new j(T);E.cmpId=k.cmpId,E.cmpVersion=typeof k.cmpVersion=="number"?k.cmpVersion:Number.parseInt(String(k.cmpVersion??"1"),10)||1,E.consentScreen=k.consentScreen??1,E.consentLanguage=k.consentLanguage??"EN",E.publisherCountryCode=k.publisherCountryCode??"US",E.isServiceSpecific=k.isServiceSpecific??!0;for(let[V,_]of Object.entries(g.purposeConsents))_&&E.purposeConsents.set(Number(V));for(let[V,_]of Object.entries(g.purposeLegitimateInterests))_&&E.purposeLegitimateInterests.set(Number(V));for(let[V,_]of Object.entries(g.vendorConsents)){let R=Number(V);_&&Number.isFinite(R)&&E.vendorConsents.set(R)}for(let[V,_]of Object.entries(g.vendorLegitimateInterests)){let R=Number(V);_&&Number.isFinite(R)&&E.vendorLegitimateInterests.set(R)}for(let[V,_]of Object.entries(g.specialFeatureOptIns))_&&E.specialFeatureOptins.set(Number(V));for(let[V,_]of Object.entries(g.vendorsDisclosed))_&&E.vendorsDisclosed.set(Number(V));return I.encode(E)}async function w(g){let{TCString:f}=await b(),k=f.decode(g),j=(I,z)=>{let T={};for(let E=1;E<=z;E++)I.has(E)&&(T[E]=!0);return T};return{cmpId:k.cmpId,cmpVersion:k.cmpVersion,consentLanguage:k.consentLanguage,isServiceSpecific:k.isServiceSpecific,purposeConsents:j(k.purposeConsents,11),purposeLegitimateInterests:j(k.purposeLegitimateInterests,11),vendorConsents:j(k.vendorConsents,1e3),vendorLegitimateInterests:j(k.vendorLegitimateInterests,1e3),specialFeatureOptIns:j(k.specialFeatureOptins,2),vendorsDisclosed:j(k.vendorsDisclosed,1e3),created:k.created,lastUpdated:k.lastUpdated,vendorListVersion:k.vendorListVersion,policyVersion:k.policyVersion}}var C=t("./src/libs/iab-tcf/types.ts");function A(g,f,k){if(typeof document>"u")return;let j=24*k*3600;document.cookie=`${g}=${encodeURIComponent(f)}; max-age=${j}; path=/; SameSite=Lax`}function x(g){if(typeof document>"u")return null;let f=document.cookie.match(new RegExp(`(^| )${g}=([^;]+)`));return f?.[2]?decodeURIComponent(f[2]):null}function D(g){let{cmpId:f=i.D,cmpVersion:k=i.I,gvl:j,gdprApplies:I=!0}=g,z="",T="loading",E="hidden",V=new Map,_=0,R=null;async function ue(B,M){if(R&&R.tcString===z&&!B)return R;let se={},ie={},Ie={},Ft={},Bt={};if(z)try{let Le=await w(z);se=Le.purposeConsents,ie=Le.purposeLegitimateInterests,Ie=Le.vendorConsents,Ft=Le.vendorLegitimateInterests,Bt=Le.specialFeatureOptIns}catch{}let ti=typeof k=="number"?k:Number.parseInt(String(k),10)||1,Dt={tcString:z,tcfPolicyVersion:j.tcfPolicyVersion,cmpId:f,cmpVersion:ti,gdprApplies:I,listenerId:M,eventStatus:B,cmpStatus:T,isServiceSpecific:!0,useNonStandardTexts:!1,publisherCC:"US",purposeOneTreatment:!1,purpose:{consents:se,legitimateInterests:ie},vendor:{consents:Ie,legitimateInterests:Ft},specialFeatureOptins:Bt,publisher:{consents:{},legitimateInterests:{},customPurpose:{consents:{},legitimateInterests:{}},restrictions:{}}};return B||(R=Dt),Dt}function re(B){let M={gdprApplies:I,cmpLoaded:T==="loaded",cmpStatus:T,displayStatus:E,apiVersion:"2.3",cmpVersion:typeof k=="string"?k:String(k),cmpId:f,gvlVersion:j.vendorListVersion,tcfPolicyVersion:j.tcfPolicyVersion};B(M,!0)}async function ee(B,M){let se=await ue();B(se,!0)}async function Ce(B){return ee(B)}function Zn(B,M){B(j,!0)}async function Qn(B){let M=_++;V.set(M,B);let se=await ue("tcloaded",M);B(se,!0)}function Xn(B,M){let se=V.has(M);V.delete(M),B(se,!0)}async function Ue(B){for(let[M,se]of V){let ie=await ue(B,M);se(ie,!0)}}function ei(){if(typeof window>"u")return;let B=d();window.__tcfapi=(M,se,ie,Ie)=>{switch(M){case"ping":re(ie);break;case"getTCData":ee(ie,Ie);break;case"getInAppTCData":Ce(ie);break;case"getVendorList":Zn(ie,Ie);break;case"addEventListener":Qn(ie);break;case"removeEventListener":Xn(ie,Ie);break;default:ie(null,!1)}},l();for(let M of B)window.__tcfapi?.(...M);T="loaded"}return ei(),{updateConsent:B=>{z=B,R=null,T="loaded",Ue("useractioncomplete")},setDisplayStatus:B=>{E=B,B==="visible"&&Ue("cmpuishown")},loadFromStorage:()=>{let B=x(C.Y.TC_STRING_COOKIE);if(B)return z=B,R=null,Ue("tcloaded"),B;if(typeof localStorage<"u")try{let M=localStorage.getItem(C.Y.TC_STRING_LOCAL);if(M)return z=M,R=null,Ue("tcloaded"),M}catch{}return null},saveToStorage:B=>{if(A(C.Y.TC_STRING_COOKIE,B,395),typeof localStorage<"u")try{localStorage.setItem(C.Y.TC_STRING_LOCAL,B)}catch{}},getTcString:()=>z,destroy:()=>{V.clear(),R=null,typeof window<"u"&&delete window.__tcfapi}}}var F=t("./src/libs/iab-tcf/fetch-gvl.ts");let O={necessary:[1],marketing:[2,3,4],experience:[5,6],measurement:[7,8,9],functionality:[10,11]};function X(g){let f={necessary:!1,marketing:!1,experience:!1,measurement:!1,functionality:!1};for(let[k,j]of Object.entries(O)){let I=j.every(z=>g[z]===!0);f[k]=I}return f}t("./src/libs/iab-tcf/store.ts")},"./src/libs/iab-tcf/store.ts"(n,e,t){t.d(e,{yx:()=>c});var i=t("./src/libs/cookie/index.ts"),s=t("./src/libs/generate-subject-id.ts"),r=t("./src/libs/iab-tcf/cmp-defaults.ts");function o(l){return{config:l,gvl:null,isLoadingGVL:!1,nonIABVendors:[],tcString:null,vendorConsents:{},vendorLegitimateInterests:{},purposeConsents:{},purposeLegitimateInterests:{},specialFeatureOptIns:{},vendorsDisclosed:{},cmpApi:null,preferenceCenterTab:"purposes"}}function a(l,y,m){let b=h=>{let{iab:w}=l();w&&y({iab:{...w,...h}})};return{_updateState:b,setPurposeConsent:(h,w)=>{let{iab:C}=l();C&&b({purposeConsents:{...C.purposeConsents,[h]:w}})},setPurposeLegitimateInterest:(h,w)=>{let{iab:C}=l();C&&b({purposeLegitimateInterests:{...C.purposeLegitimateInterests,[h]:w}})},setVendorConsent:(h,w)=>{let{iab:C}=l();C&&b({vendorConsents:{...C.vendorConsents,[String(h)]:w}})},setVendorLegitimateInterest:(h,w)=>{let{iab:C}=l();C&&b({vendorLegitimateInterests:{...C.vendorLegitimateInterests,[String(h)]:w}})},setSpecialFeatureOptIn:(h,w)=>{let{iab:C}=l();C&&b({specialFeatureOptIns:{...C.specialFeatureOptIns,[h]:w}})},setPreferenceCenterTab:h=>{b({preferenceCenterTab:h})},acceptAll:()=>{let{iab:h}=l();if(!h?.gvl)return;let{purposeConsents:w,purposeLegitimateInterests:C}=u(h.gvl,!0),{vendorConsents:A,vendorLegitimateInterests:x}=p(h.gvl,h.nonIABVendors,!0),D=d(h.gvl,!0);b({purposeConsents:w,purposeLegitimateInterests:C,vendorConsents:A,vendorLegitimateInterests:x,specialFeatureOptIns:D})},rejectAll:()=>{let{iab:h}=l();if(!h?.gvl)return;let w={1:!0},C={};for(let F of Object.keys(h.gvl.purposes))Number(F)!==1&&(w[Number(F)]=!1,C[Number(F)]=!1);let{vendorConsents:A,vendorLegitimateInterests:x}=p(h.gvl,h.nonIABVendors,!1),D=d(h.gvl,!1);b({purposeConsents:w,purposeLegitimateInterests:C,vendorConsents:A,vendorLegitimateInterests:x,specialFeatureOptIns:D})},save:async()=>{let{iab:h,locationInfo:w,user:C,callbacks:A}=l();if(!h?.cmpApi||!h.gvl)return;let{config:x,gvl:D,cmpApi:F,purposeConsents:O,purposeLegitimateInterests:X,vendorConsents:g,vendorLegitimateInterests:f,specialFeatureOptIns:k}=h,{generateTCString:j,iabPurposesToC15tConsents:I}=await Promise.resolve().then(t.bind(t,"./src/libs/iab-tcf/index.ts")),z={};for(let ee of Object.keys(D.vendors))z[Number(ee)]=!0;let T=await j({purposeConsents:O,purposeLegitimateInterests:X,vendorConsents:g,vendorLegitimateInterests:f,specialFeatureOptIns:k,vendorsDisclosed:z},D,{cmpId:x.cmpId??r.D,cmpVersion:x.cmpVersion??r.I,publisherCountryCode:x.publisherCountryCode??"GB",isServiceSpecific:x.isServiceSpecific??!0});F.saveToStorage(T),F.updateConsent(T);let E=I(O),V=Date.now();b({tcString:T,vendorsDisclosed:z});let _=l().consentInfo?.subjectId;_||(_=(0,s.L)()),y({consents:E,selectedConsents:E,activeUI:"none",consentInfo:{time:V,subjectId:_,externalId:C?.id,identityProvider:C?.identityProvider}});let R={},ue={};for(let ee of h.nonIABVendors){let Ce=String(ee.id);ee.purposes&&ee.purposes.length>0&&(R[Ce]=g[Ce]??!1),ee.legIntPurposes&&ee.legIntPurposes.length>0&&(ue[Ce]=f[Ce]??!0)}(0,i._y)({consents:E,consentInfo:{time:V,subjectId:_,externalId:C?.id,identityProvider:C?.identityProvider},iabCustomVendorConsents:R,iabCustomVendorLegitimateInterests:ue},void 0,l().storageConfig),l().updateScripts();let re=await m.setConsent({body:{subjectId:_,givenAt:V,type:"cookie_banner",domain:typeof window<"u"?window.location.hostname:"",preferences:E,externalSubjectId:C?.id,identityProvider:C?.identityProvider,tcString:T,jurisdiction:w?.jurisdiction??void 0,jurisdictionModel:"iab",metadata:{source:"iab_tcf",acceptanceMethod:"iab"}}});if(!re.ok){let ee=re.error?.message??"Failed to save IAB consents";A.onError?.({error:ee}),A.onError||console.error(ee)}}}}function c(l,y,m,b){let h=o(l),w=a(y,m,b);return{...h,...w}}function u(l,y){let m={},b={};for(let h of Object.keys(l.purposes))m[Number(h)]=y,b[Number(h)]=y;return{purposeConsents:m,purposeLegitimateInterests:b}}function p(l,y,m){let b={},h={};for(let[w,C]of Object.entries(l.vendors)){let A=String(w);C.purposes&&C.purposes.length>0&&(b[A]=m),C.legIntPurposes&&C.legIntPurposes.length>0&&(h[A]=m)}return y.forEach(w=>{let C=String(w.id);w.purposes&&w.purposes.length>0&&(b[C]=m),w.legIntPurposes&&w.legIntPurposes.length>0&&(h[C]=m)}),{vendorConsents:b,vendorLegitimateInterests:h}}function d(l,y){let m={};for(let b of Object.keys(l.specialFeatures))m[Number(b)]=y;return m}},"./src/libs/iab-tcf/types.ts"(n,e,t){t.d(e,{Y:()=>i,w:()=>s});let i={TC_STRING_COOKIE:"euconsent-v2",TC_STRING_LOCAL:"euconsent-v2"},s="https://gvl.consent.io"},"./src/store/initial-state.ts"(n,e,t){t.d(e,{AQ:()=>a,ln:()=>o,ue:()=>c});var i=t("./src/translations/index.ts"),s=t("./src/types/consent-types.ts"),r=t("./src/version.ts");let o="c15t",a="privacy-consent-storage",c={debug:!1,config:{pkg:"c15t",version:r.r,mode:"Unknown"},consents:s.y.reduce((u,p)=>(u[p.name]=p.defaultValue,u),{}),selectedConsents:s.y.reduce((u,p)=>(u[p.name]=p.defaultValue,u),{}),consentInfo:null,branding:"c15t",activeUI:"none",isLoadingConsentInfo:!1,hasFetchedBanner:!1,lastBannerFetchData:null,consentCategories:["necessary"],callbacks:{},locationInfo:null,overrides:void 0,legalLinks:{},translationConfig:i.Z,user:void 0,networkBlocker:void 0,storageConfig:void 0,includeNonDisplayedConsents:!1,consentTypes:s.y,iframeBlockerConfig:{disableAutomaticBlocking:!1},scripts:[],loadedScripts:{},scriptIdMap:{},model:"opt-in",iab:null,reloadOnConsentRevoked:!0,ssrDataUsed:!1,ssrSkippedReason:null}},"./src/translations/index.ts"(n,e,t){t.d(e,{Z:()=>s});var i=t("@c15t/translations");let s={translations:{en:i.enTranslations},defaultLanguage:"en",disableAutoLanguageSwitch:!1}},"./src/types/consent-types.ts"(n,e,t){t.d(e,{W:()=>s,y:()=>i});let i=[{defaultValue:!0,description:"These trackers are used for activities that are strictly necessary to operate or deliver the service you requested from us and, therefore, do not require you to consent.",disabled:!0,display:!0,gdprType:1,name:"necessary"},{defaultValue:!1,description:"These trackers enable basic interactions and functionalities that allow you to access selected features of our service and facilitate your communication with us.",display:!1,gdprType:2,name:"functionality"},{defaultValue:!1,description:"These trackers help us to measure traffic and analyze your behavior to improve our service.",display:!1,gdprType:4,name:"measurement"},{defaultValue:!1,description:"These trackers help us to improve the quality of your user experience and enable interactions with external content, networks, and platforms.",display:!1,gdprType:3,name:"experience"},{defaultValue:!1,description:"These trackers help us to deliver personalized ads or marketing content to you, and to measure their performance.",display:!1,gdprType:5,name:"marketing"}],s=i.map(r=>r.name)},"./src/version.ts"(n,e,t){t.d(e,{r:()=>i});let i="2.0.0-rc.3"},"@c15t/translations"(n){n.exports=st}},zn={};function U(n){var e=zn[n];if(e!==void 0)return e.exports;var t=zn[n]={exports:{}};return _i[n](t,t.exports,U),t.exports}U.d=(n,e)=>{for(var t in e)U.o(e,t)&&!U.o(n,t)&&Object.defineProperty(n,t,{enumerable:!0,get:e[t]})};U.o=(n,e)=>Object.prototype.hasOwnProperty.call(n,e);var le=U("@c15t/translations"),_n=/^\/+/;function Lt(n,e=null,t=null,i=null){return{data:e,error:t,ok:n,response:i}}function Oi(n,e=500,t="ERROR",i){return Lt(!1,null,{message:n,status:e,code:t,cause:i},null)}var de={maxRetries:3,initialDelayMs:100,backoffFactor:2,retryableStatusCodes:[500,502,503,504],nonRetryableStatusCodes:[400,401,403,404],retryOnNetworkError:!0,shouldRetry:void 0},Ri=/^(?:[a-z+]+:)?\/\//i,Pn=_n,K=U("./src/libs/debug.ts"),Je=n=>new Promise(e=>setTimeout(e,n));function Mi(){return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,n=>{let e=16*Math.random()|0;return(n==="x"?e:3&e|8).toString(16)})}function Tn(n){let e=n.length;for(;e>0&&n[e-1]==="/";)e--;return n.slice(0,e)}function Ui(n,e){if(Ri.test(n)){let s=new URL(n),r=Tn(s.pathname),o=e.replace(Pn,""),a=`${r}/${o}`;return s.pathname=a,s.toString()}let t=Tn(n),i=e.replace(Pn,"");return`${t}/${i}`}var me=Lt;async function _e(n,e,t){let i={...n.retryConfig,...t?.retryConfig||{},retryableStatusCodes:t?.retryConfig?.retryableStatusCodes??n.retryConfig.retryableStatusCodes??de.retryableStatusCodes,nonRetryableStatusCodes:t?.retryConfig?.nonRetryableStatusCodes??n.retryConfig.nonRetryableStatusCodes??de.nonRetryableStatusCodes},{maxRetries:s,initialDelayMs:r,backoffFactor:o,retryableStatusCodes:a,nonRetryableStatusCodes:c,retryOnNetworkError:u}=i,p=0,d=r,l=null;for(;p<=(s??0);){let m=Mi(),b=n.customFetch||globalThis.fetch,h=Ui(n.backendURL,e),w;try{w=new URL(h)}catch{w=new URL(h,window.location.origin)}if(t?.query)for(let[A,x]of Object.entries(t.query))x!==void 0&&w.searchParams.append(A,String(x));let C={method:t?.method||"GET",mode:n.corsMode,credentials:"include",headers:{...n.headers,"X-Request-ID":m,...t?.headers},...t?.fetchOptions};t?.body&&C.method!=="GET"&&(C.body=JSON.stringify(t.body));try{let A=await b(w.toString(),C),x=null,D=null;try{A.headers.get("content-type")?.includes("application/json")&&A.status!==204&&A.headers.get("content-length")!=="0"?x=await A.json():A.status===204&&(x=null)}catch(f){D=f}if(D){let f=me(!1,null,{message:"Failed to parse response",status:A.status,code:"PARSE_ERROR",cause:D},A);if(t?.onError?.(f,e),t?.throw)throw new Error("Failed to parse response");return f}if(A.status>=200&&A.status<300){let f=me(!0,x,null,A);return t?.onSuccess?.(f),f}let O=x,X=me(!1,null,{message:O?.message||`Request failed with status ${A.status}`,status:A.status,code:O?.code||"API_ERROR",details:O?.details||null},A);l=X;let g=!1;if(c?.includes(A.status))(0,K.YA)().debug(`Not retrying request to ${e} with status ${A.status} (nonRetryableStatusCodes)`),g=!1;else if(typeof i.shouldRetry=="function")try{g=i.shouldRetry(A,{attemptsMade:p,url:w.toString(),method:C.method||"GET"}),(0,K.YA)().debug(`Custom retry strategy for ${e} with status ${A.status}: ${g}`)}catch{g=a?.includes(A.status)??!1,(0,K.YA)().debug(`Custom retry strategy failed, falling back to status code check: ${g}`)}else g=a?.includes(A.status)??!1,(0,K.YA)().debug(`Standard retry check for ${e} with status ${A.status}: ${g}`);if(!g||p>=(s??0)){if(t?.onError?.(X,e),t?.throw)throw new Error(X.error?.message||"Request failed");return X}p++,await Je(d??0),d=(d??0)*(o??2)}catch(A){if(A&&A.message==="Failed to parse response")throw A;let x=!(A instanceof Response),D=me(!1,null,{message:A instanceof Error?A.message:String(A),status:0,code:"NETWORK_ERROR",cause:A},null);if(l=D,!(x&&u)||p>=(s??0)){if(t?.onError?.(D,e),t?.throw)throw A;return D}p++,await Je(d??0),d=(d??0)*(o??2)}}let y=l||me(!1,null,{message:`Request failed after ${s} retries`,status:0,code:"MAX_RETRIES_EXCEEDED"},null);if(t?.onError?.(y,e),t?.throw)throw new Error(`Request failed after ${s} retries`);return y}var Y=U("./src/libs/cookie/index.ts"),Oe={INIT:"/init",POST_SUBJECT:"/subjects",GET_SUBJECT:"/subjects",PATCH_SUBJECT:"/subjects",CHECK_CONSENT:"/consents/check",LIST_SUBJECTS:"/subjects"};async function On(n,e,t,i,s){try{let r=await _e(n,e,{method:t,...i});return r.ok?r:(console.warn(`API request failed, falling back to offline mode for ${e}`),s(i))}catch(r){return console.warn(`Error calling ${e}, falling back to offline mode:`,r),s(i)}}async function Gi(n){let e="c15t-pending-identify-submissions";try{if(typeof window<"u"&&n?.body&&window.localStorage){let i=[];try{let o=window.localStorage.getItem(e);o&&(i=JSON.parse(o))}catch(o){console.warn("Error parsing pending identify submissions:",o),i=[]}let s=n.body;i.some(o=>o.id===s.id&&o.externalId===s.externalId)||(i.push(s),window.localStorage.setItem(e,JSON.stringify(i)),(0,K.YA)().log("Queued identify user submission for retry on next page load"))}}catch(i){console.warn("Failed to write to localStorage in identify offline fallback:",i)}let t=me(!0,null,null,null);return n?.onSuccess&&await n.onSuccess(t),t}async function Hi(n,e,t){let{body:i,...s}=t;if(!i?.id)return{ok:!1,data:null,response:null,error:{message:"Subject ID is required to identify user",status:400,code:"MISSING_SUBJECT_ID"}};let r=(0,Y.If)(e);(0,Y._y)({consents:r?.consents||{},consentInfo:{...r?.consentInfo,time:r?.consentInfo?.time||Date.now(),subjectId:i.id,externalId:i.externalId,identityProvider:i.identityProvider}},void 0,e);let o=`${Oe.PATCH_SUBJECT}/${i.id}`,{id:a,...c}=i;return On(n,o,"PATCH",{...s,body:c},async u=>{let p={id:i.id,...u?.body};return Gi({...u,body:p})})}var Rn=U("./src/libs/iab-tcf/fetch-gvl.ts");async function Ln(n,e){let t=null;if(e?.enabled)try{t=await(0,Rn.ix)(e.vendorIds)}catch(s){console.warn("Failed to fetch GVL in offline fallback:",s)}let i=me(!0,{jurisdiction:"NONE",location:{countryCode:null,regionCode:null},translations:{language:"en",translations:le.enTranslations},branding:"c15t",gvl:t},null,null);return n?.onSuccess&&await n.onSuccess(i),i}async function qi(n,e,t){try{let i=await _e(n,Oe.INIT,{method:"GET",...e});return i.ok?i:(console.warn("API request failed, falling back to offline mode for consent banner"),Ln(e,t))}catch(i){return console.warn("Error fetching consent banner info, falling back to offline mode:",i),Ln(e,t)}}var Mn="c15t-pending-consent-submissions",Ze="c15t-pending-identify-submissions";function $i(n,e){let t=Mn;if(!(typeof window>"u"||!window.localStorage))try{window.localStorage.setItem("c15t-storage-test-key","test"),window.localStorage.removeItem("c15t-storage-test-key");let i=window.localStorage.getItem(t);if(!i)return;let s=JSON.parse(i);if(!s.length)return void window.localStorage.removeItem(t);(0,K.YA)().log(`Found ${s.length} pending consent submission(s) to retry`),setTimeout(()=>{e(s)},2e3)}catch(i){console.warn("Failed to check for pending consent submissions:",i)}}async function Ki(n,e){let t=Mn,i=3,s=[...e];for(let r=0;r0;r++){let o=[];for(let a=0;a=0;a--){let c=o[a];c!==void 0&&s.splice(c,1)}if(s.length===0)break;r0?(window.localStorage.setItem(t,JSON.stringify(s)),(0,K.YA)().log(`${s.length} consent submissions still pending for future retry`)):(window.localStorage.removeItem(t),(0,K.YA)().log("All pending consent submissions processed successfully")))}catch(r){console.warn("Error updating pending submissions storage:",r)}}function Yi(n,e){if(!(typeof window>"u"||!window.localStorage))try{let t=window.localStorage.getItem(Ze);if(!t)return;let i=JSON.parse(t);if(!i.length)return void window.localStorage.removeItem(Ze);(0,K.YA)().log(`Found ${i.length} pending identify user submission(s) to retry`),setTimeout(()=>{e(i)},2500)}catch(t){console.warn("Failed to check for pending identify submissions:",t)}}async function Wi(n,e){let i=[...e];for(let s=0;s<3&&i.length>0;s++){let r=[];for(let o=0;o=0;o--){let a=r[o];a!==void 0&&i.splice(a,1)}if(i.length===0)break;s<2&&await Je(1e3*(s+1))}try{typeof window<"u"&&window.localStorage&&(i.length>0?(window.localStorage.setItem(Ze,JSON.stringify(i)),(0,K.YA)().log(`${i.length} identify submissions still pending for future retry`)):(window.localStorage.removeItem(Ze),(0,K.YA)().log("All pending identify submissions processed successfully")))}catch(s){console.warn("Error updating pending identify submissions storage:",s)}}async function Ji(n,e){let t="c15t-pending-consent-submissions",i=e?.body?.subjectId;try{if(typeof window<"u"&&((0,Y._y)({consents:e?.body?.preferences||{},consentInfo:{time:Date.now(),subjectId:i,externalId:e?.body?.externalSubjectId,identityProvider:e?.body?.identityProvider}},void 0,n),e?.body&&window.localStorage)){let r=[];try{let c=window.localStorage.getItem(t);c&&(r=JSON.parse(c))}catch(c){console.warn("Error parsing pending submissions:",c),r=[]}let o=e.body;r.some(c=>JSON.stringify(c)===JSON.stringify(o))||(r.push(o),window.localStorage.setItem(t,JSON.stringify(r)),(0,K.YA)().log("Queued consent submission for retry on next page load"))}}catch(r){console.warn("Failed to write to localStorage in offline fallback:",r)}let s=me(!0,null,null,null);return e?.onSuccess&&await e.onSuccess(s),s}async function Zi(n,e,t){return(0,Y._y)({consents:t?.body?.preferences||{},consentInfo:{time:Date.now(),subjectId:t?.body?.subjectId,externalId:t?.body?.externalSubjectId,identityProvider:t?.body?.identityProvider}},void 0,e),await On(n,Oe.POST_SUBJECT,"POST",t,async s=>Ji(e,s))}var Qe=class{backendURL;storageConfig;iabConfig;headers;customFetch;corsMode;retryConfig;fetcherContext;constructor(e){this.backendURL=e.backendURL.endsWith("/")?e.backendURL.slice(0,-1):e.backendURL,this.headers={"Content-Type":"application/json",...e.headers},this.customFetch=e.customFetch,this.corsMode=e.corsMode||"cors",this.storageConfig=e.storageConfig,this.iabConfig=e.iabConfig,this.retryConfig={maxRetries:e.retryConfig?.maxRetries??de.maxRetries??3,initialDelayMs:e.retryConfig?.initialDelayMs??de.initialDelayMs??100,backoffFactor:e.retryConfig?.backoffFactor??de.backoffFactor??2,retryableStatusCodes:e.retryConfig?.retryableStatusCodes??de.retryableStatusCodes,nonRetryableStatusCodes:e.retryConfig?.nonRetryableStatusCodes??de.nonRetryableStatusCodes,shouldRetry:e.retryConfig?.shouldRetry??de.shouldRetry,retryOnNetworkError:e.retryConfig?.retryOnNetworkError??de.retryOnNetworkError},this.fetcherContext={backendURL:this.backendURL,headers:this.headers,customFetch:this.customFetch,corsMode:this.corsMode,retryConfig:this.retryConfig},this.checkPendingConsentSubmissions(),this.checkPendingIdentifySubmissions()}async init(e){return qi(this.fetcherContext,e,this.iabConfig)}async setConsent(e){return Zi(this.fetcherContext,this.storageConfig,e)}async identifyUser(e){return Hi(this.fetcherContext,this.storageConfig,e)}async $fetch(e,t){return _e(this.fetcherContext,e,t)}checkPendingConsentSubmissions(){$i(this.fetcherContext,e=>this.processPendingConsentSubmissions(e))}async processPendingConsentSubmissions(e){return Ki(this.fetcherContext,e)}checkPendingIdentifySubmissions(){Yi(this.fetcherContext,e=>this.processPendingIdentifySubmissions(e))}async processPendingIdentifySubmissions(e){return Wi(this.fetcherContext,e)}};function Xe(n,e=500,t="HANDLER_ERROR",i){return Oi(n,e,t,i)}async function Et(n,e,t){let i=n[e];if(!i){let s=Xe(`No endpoint handler found for '${String(e)}'`,404,"ENDPOINT_NOT_FOUND");if(t?.throw)throw new Error(`No endpoint handler found for '${String(e)}'`);return s}try{let s=await i(t);return{data:s.data,error:s.error,ok:s.ok??!s.error,response:s.response}}catch(s){let r=Xe(s instanceof Error?s.message:String(s),0,"HANDLER_ERROR",s);if(t?.throw)throw s;return r}}async function Qi(n,e,t,i){let s=t.replace(_n,"").split("/")[0],r=e[t];if(r)try{return await r(i)}catch(o){return Xe(o instanceof Error?o.message:String(o),0,"HANDLER_ERROR",o)}return!s||!(s in n)?Xe(`No endpoint handler found for '${t}'`,404,"ENDPOINT_NOT_FOUND"):await Et(n,s,i)}async function Xi(n,e){let t=("init"in n&&n.init!==void 0,"init");return await Et(n,t,e)}async function es(n,e){return await Et(n,"setConsent",e)}var jt=class{endpointHandlers;dynamicHandlers={};constructor(e){this.endpointHandlers=e.endpointHandlers}async init(e){return Xi(this.endpointHandlers,e)}async setConsent(e){return es(this.endpointHandlers,e)}async identifyUser(e){if(this.endpointHandlers.identifyUser)return this.endpointHandlers.identifyUser(e);let t=e.body?.id;return t?this.$fetch(`/subjects/${t}`,{...e,method:"PATCH"}):{ok:!1,data:null,response:null,error:{message:"Subject ID is required to identify user",status:400,code:"MISSING_SUBJECT_ID"}}}registerHandler(e,t){this.dynamicHandlers[e]=t}async $fetch(e,t){return Qi(this.endpointHandlers,this.dynamicHandlers,e,t)}};function ts(n,e){let t={EU:new Set(["AT","BE","BG","HR","CY","CZ","DK","EE","FI","FR","DE","GR","HU","IE","IT","LV","LT","LU","MT","NL","PL","PT","RO","SK","SI","ES","SE"]),EEA:new Set(["IS","NO","LI"]),UK:new Set(["GB"]),CH:new Set(["CH"]),BR:new Set(["BR"]),CA:new Set(["CA"]),AU:new Set(["AU"]),JP:new Set(["JP"]),KR:new Set(["KR"]),CA_QC_REGIONS:new Set(["QC"])},i="NONE";if(n){let s=n.toUpperCase(),r=e&&typeof e=="string"?(e.includes("-")?e.split("-").pop():e).toUpperCase():null;if(s==="CA"&&r&&t.CA_QC_REGIONS.has(r))return"QC_LAW25";let o=[{sets:[t.EU,t.EEA,t.UK],code:"GDPR"},{sets:[t.CH],code:"CH"},{sets:[t.BR],code:"BR"},{sets:[t.CA],code:"PIPEDA"},{sets:[t.AU],code:"AU"},{sets:[t.JP],code:"APPI"},{sets:[t.KR],code:"PIPA"}];for(let{sets:a,code:c}of o)if(a.some(u=>u.has(s))){i=c;break}}return i}var We=U("./src/translations/index.ts");function Un(n=null){return Lt(!0,n)}async function St(n){let e=Un();return n?.onSuccess&&await n.onSuccess(e),e}async function ns(n,e,t){let i=e?.headers?.["x-c15t-country"]??"GB",s=e?.headers?.["x-c15t-region"]??null,r,o,a=e?.headers?.["accept-language"]??null;if(n?.translations&&Object.keys(n.translations).length>0){let d=n.translations,l=Array.from(new Set(["en",...Object.keys(d)])),y=n.defaultLanguage??"en";r=(0,le.selectLanguage)(l,{header:a,fallback:y});let m=d[r]??{};o=(0,le.deepMergeTranslations)(le.enTranslations,m)}else{let d=Object.keys(We.Z.translations),l=We.Z.defaultLanguage??"en";r=(0,le.selectLanguage)(d,{header:a,fallback:l}),o=We.Z.translations[r]}let c=ts(i,s),u=null;if(t?.enabled)if(t.gvl)u=t.gvl;else try{u=await(0,Rn.ix)(t.vendorIds)}catch(d){console.warn("Failed to fetch GVL in offline mode:",d)}let p=Un({jurisdiction:c,location:{countryCode:i,regionCode:s},translations:{language:r,translations:o},branding:"c15t",gvl:u});return e?.onSuccess&&await e.onSuccess(p),p}async function is(n,e){let t=e?.body?.subjectId;try{typeof window<"u"&&(0,Y._y)({consentInfo:{time:Date.now(),subjectId:t,externalId:e?.body?.externalSubjectId,identityProvider:e?.body?.identityProvider},consents:e?.body?.preferences||{}},void 0,n)}catch(i){console.warn("Failed to write to storage:",i)}return await St(e)}var At=class{storageConfig;initialTranslationConfig;iabConfig;constructor(e,t,i){this.storageConfig=e,this.initialTranslationConfig=t,this.iabConfig=i}async init(e){return ns(this.initialTranslationConfig,e,this.iabConfig)}async setConsent(e){return is(this.storageConfig,e)}async identifyUser(e){return console.warn("identifyUser called in offline mode - external ID will not be linked"),St(e)}async $fetch(e,t){return await St(t)}},ss="/api/c15t",rs="c15t",Ye=new Map;function os(n){return n?Object.keys(n).sort().map(t=>{let i=n[t];return i==null?`${t}:null`:`${t}:${String(i)}`}).join("|"):""}function as(n){let e=os(n.storageConfig),t=e?`:storage:${e}`:"";if(n.mode==="offline"){let s=n.store?.initialTranslationConfig?.translations,r=s?`:translations:${Object.keys(s).sort().join(",")}`:"",a=n.store?.iab?.enabled?":iab:enabled":"";return`offline${t}${r}${a}`}if(n.mode==="custom")return`custom:${Object.keys(n.endpointHandlers||{}).sort().join(",")}${t}`;let i="";return"headers"in n&&n.headers&&(i=`:headers:${Object.keys(n.headers).sort().map(r=>`${r}=${n.headers?.[r]}`).join(",")}`),`c15t:${n.backendURL||""}${i}${t}`}function wt(n){let e=as(n);if(Ye.has(e)){if(n.mode!=="offline"&&n.mode!=="custom"&&"headers"in n&&n.headers){let r=Ye.get(e);r instanceof Qe&&(r.headers={"Content-Type":"application/json",...n.headers})}let s=Ye.get(e);if(s)return new Proxy(s,{get(r,o){return r[o]}})}let t=n.mode||rs,i;switch(t){case"custom":{let s=n;i=new jt({endpointHandlers:s.endpointHandlers});break}case"offline":{let s=n.store?.iab;i=new At(n.storageConfig,n.store?.initialTranslationConfig,s?{enabled:s.enabled,vendorIds:s.vendors,gvl:s.gvl}:void 0);break}default:{let s=n,r=n.store?.iab;i=new Qe({backendURL:s.backendURL||ss,headers:s.headers,customFetch:s.customFetch,retryConfig:s.retryConfig,storageConfig:n.storageConfig,iabConfig:r?{enabled:r.enabled,vendorIds:r.vendors,gvl:r.gvl}:void 0});break}}return Ye.set(e,i),i}var Vt=U("./src/libs/generate-subject-id.ts");function Gn(n,e){if(n.length===0)throw new TypeError(`${e} condition cannot be empty`)}function cs(n,e){if(!(n in e))throw new Error(`Consent category "${n}" not found in consent state`);return e[n]||!1}function ls(n,e){let t=Array.isArray(n)?n:[n];return Gn(t,"AND"),t.every(i=>et(i,e))}function us(n,e){let t=Array.isArray(n)?n:[n];return Gn(t,"OR"),t.some(i=>et(i,e))}function et(n,e){if(typeof n=="string")return cs(n,e);if(typeof n=="object"&&n!==null){if("and"in n)return ls(n.and,e);if("or"in n)return us(n.or,e);if("not"in n)return!et(n.not,e)}throw new TypeError(`Invalid condition structure: ${JSON.stringify(n)}`)}function tt(n,e){return et(n,e)}function Hn(n){let e=new Set;function t(i){if(typeof i=="string")return void e.add(i);typeof i=="object"&&i!==null&&("and"in i?(Array.isArray(i.and)?i.and:[i.and]).forEach(t):"or"in i?(Array.isArray(i.or)?i.or:[i.or]).forEach(t):"not"in i&&t(i.not))}return t(n),Array.from(e)}var nt=U("./src/libs/iab-tcf/index.ts"),Te=U("./src/types/consent-types.ts");function ds(n){let e=n.getAttribute("data-category");if(e){if(!Te.W.includes(e))throw new Error(`Invalid category attribute "${e}" on iframe. Must be one of: ${Te.W.join(", ")}`);return e}}function zt(n,e){let t=n.getAttribute("data-src"),i=ds(n);if(!i)return;tt(i,e)?t&&!n.src&&(n.src=t,n.removeAttribute("data-src")):n.src&&n.removeAttribute("src")}function qn(){if(typeof document>"u")return[];let n=document.querySelectorAll("iframe[data-category]"),e=new Set;return n?(n.forEach(t=>{let i=t.getAttribute("data-category");if(!i)return;let s=i.trim();Te.W.includes(s)&&e.add(s)}),Array.from(e)):[]}function En(n){if(typeof document>"u")return;let e=document.querySelectorAll("iframe");e&&e.forEach(t=>{zt(t,n)})}function ps(n,e){let t=new MutationObserver(i=>{let s=n(),r=!1;if(i.forEach(o=>{o.addedNodes.forEach(a=>{if(a.nodeType===Node.ELEMENT_NODE){let c=a;c.tagName&&c.tagName.toUpperCase()==="IFRAME"&&(zt(c,s),c.hasAttribute("data-category")&&(r=!0));let u=c.querySelectorAll?.("iframe");u&&u.length>0&&u.forEach(p=>{zt(p,s),p.hasAttribute("data-category")&&(r=!0)})}})}),r&&e){let o=qn();o.length>0&&e(o)}});return t.observe(document.body,{childList:!0,subtree:!0}),t}function $n(){if(typeof crypto<"u"&&crypto.randomUUID)return crypto.randomUUID().replace(/-/g,"").substring(0,8);if(typeof crypto<"u"&&crypto.getRandomValues){let e=new Uint8Array(4);return crypto.getRandomValues(e),Array.from(e,t=>t.toString(36)).join("").padEnd(8,"0").substring(0,8)}return Math.random().toString(36).substring(2).padEnd(8,"0").substring(0,8)}function Ct(n,e,t){return e?(t[n]||(t[n]=$n()),t[n]):`c15t-script-${n}`}var Re=new Map;function Me(n){return Re.has(n)}function xt(n){return Re.get(n)}function It(n,e){Re.set(n,e)}function Pe(n){Re.delete(n)}function gs(){return Re}function ms(n,e){if(n.vendorId!==void 0){let t=String(n.vendorId);if(!e.vendorConsents[t])return!1}return!(n.iabPurposes&&n.iabPurposes.length>0&&!n.iabPurposes.every(i=>e.purposeConsents[i]===!0)||n.iabLegIntPurposes&&n.iabLegIntPurposes.length>0&&!n.iabLegIntPurposes.every(i=>e.purposeLegitimateInterests[i]===!0)||n.iabSpecialFeatures&&n.iabSpecialFeatures.length>0&&!n.iabSpecialFeatures.every(i=>e.specialFeatureOptIns[i]===!0))}function we(n,e,t){return t?.model==="iab"&&t.iabConsent&&(n.vendorId!==void 0||n.iabPurposes||n.iabLegIntPurposes||n.iabSpecialFeatures)?ms(n,t.iabConsent):tt(n.category,e)}function Kn(n,e,t={},i){let s=[];return n.forEach(r=>{if(!r.alwaysLoad&&!we(r,e,i))return;if(Me(r.id))return void r.onConsentChange?.({id:r.id,elementId:Ct(r.id,r.anonymizeId!==!1,t),hasConsent:we(r,e,i),consents:e});if(r.src&&r.textContent)throw new Error(`Script '${r.id}' cannot have both 'src' and 'textContent'. Choose one.`);if(!r.src&&!r.textContent&&!r.callbackOnly)throw new Error(`Script '${r.id}' must have either 'src', 'textContent', or 'callbackOnly' set to true.`);if(r.callbackOnly===!0){let l=r.anonymizeId!==!1,y=Ct(r.id,l,t),m={id:r.id,elementId:y,consents:e,hasConsent:we(r,e,i)};r.onBeforeLoad&&r.onBeforeLoad(m),r.onLoad&&r.onLoad(m),It(r.id,null),s.push(r.id);return}let o=r.anonymizeId!==!1,a=Ct(r.id,o,t);if(r.persistAfterConsentRevoked===!0){let l=document.getElementById(a);if(l){let y={id:r.id,hasConsent:we(r,e,i),elementId:a,consents:e,element:l};r.onConsentChange?.(y),r.onLoad?.(y),It(r.id,l),s.push(r.id);return}}let c=document.createElement("script");c.id=a,r.src?c.src=r.src:r.textContent&&(c.textContent=r.textContent),r.fetchPriority&&(c.fetchPriority=r.fetchPriority),r.async&&(c.async=!0),r.defer&&(c.defer=!0),r.nonce&&(c.nonce=r.nonce),r.attributes&&Object.entries(r.attributes).forEach(([l,y])=>{c.setAttribute(l,y)});let u={id:r.id,hasConsent:we(r,e,i),elementId:a,consents:e,element:c};r.onLoad&&(r.textContent?setTimeout(()=>{r.onLoad?.({...u})},0):c.addEventListener("load",()=>{r.onLoad?.({...u})})),r.onError&&(r.textContent||c.addEventListener("error",()=>{r.onError?.({...u,error:new Error(`Failed to load script: ${r.src}`)})})),r.onBeforeLoad&&r.onBeforeLoad(u);let p=r.target??"head",d=p==="body"?document.body:document.head;if(!d)throw new Error(`Document ${p} is not available for script injection`);d.appendChild(c),It(r.id,c),s.push(r.id)}),s}function fs(n,e,t={},i){let s=[];return n.forEach(r=>{if(Me(r.id)&&!r.alwaysLoad&&!we(r,e,i)){let o=xt(r.id);r.callbackOnly===!0||o===null?(Pe(r.id),s.push(r.id)):o&&(r.persistAfterConsentRevoked?(Pe(r.id),s.push(r.id)):(o.remove(),Pe(r.id),s.push(r.id)))}}),s}function hs(n,e,t={},i){let s=fs(n,e,t,i);return{loaded:Kn(n,e,t,i),unloaded:s}}function ks(n){return Me(n)}function vs(){return Array.from(gs().keys())}function ys(n,e,t,i={},s){let r=e.find(o=>o.id===n);if(!r)return!1;if(Me(n)){let o=xt(n);r.callbackOnly===!0||o===null?Pe(n):o&&(r.persistAfterConsentRevoked||o.remove(),Pe(n))}return!r.alwaysLoad&&!we(r,t,s)?!1:(Kn([r],t,i,s),!0)}function bs(n,e){let t=()=>{let{scripts:i,consents:s,scriptIdMap:r,model:o,iab:a}=n(),c=a?.config.enabled?{vendorConsents:a.vendorConsents,vendorLegitimateInterests:a.vendorLegitimateInterests,purposeConsents:a.purposeConsents,purposeLegitimateInterests:a.purposeLegitimateInterests,specialFeatureOptIns:a.specialFeatureOptIns}:void 0,u=hs(i,s,r,{model:o,iabConsent:c}),p={...n().loadedScripts};return u.loaded.forEach(d=>{p[d]=!0}),u.unloaded.forEach(d=>{p[d]=!1}),e({loadedScripts:p}),u};return{updateScripts:()=>t(),setScripts:i=>{let s=n(),r={...s.scriptIdMap};i.forEach(u=>{u.anonymizeId!==!1&&(r[u.id]=$n())});let o=i.flatMap(u=>Hn(u.category)),a=new Set([...s.consentCategories,...o]),c=Array.from(a);e({scripts:[...s.scripts,...i],scriptIdMap:r,consentCategories:c}),t()},removeScript:i=>{let s=n();if(Me(i)){let o=xt(i);o&&(o.remove(),Pe(i))}let r={...s.scriptIdMap};delete r[i],e({scripts:s.scripts.filter(o=>o.id!==i),loadedScripts:{...s.loadedScripts,[i]:!1},scriptIdMap:r})},reloadScript:i=>{let s=n();return ys(i,s.scripts,s.consents,s.scriptIdMap)},isScriptLoaded:i=>ks(i),getLoadedScriptIds:()=>vs()}}var ws=U("./src/version.ts"),Cs=U("./src/libs/iab-tcf/store.ts");function Is(n,e){let t=null,i=!1;return{initializeIframeBlocker:()=>{if(i||typeof document>"u")return;let s=n();if(s.iframeBlockerConfig?.disableAutomaticBlocking)return;let r=()=>{let o=qn();o.length>0&&n().updateConsentCategories(o)};document.readyState==="loading"?document.addEventListener("DOMContentLoaded",r):r(),setTimeout(r,100),En(s.consents),t=ps(()=>n().consents,o=>n().updateConsentCategories(o)),i=!0},updateIframeConsents:()=>{if(!i||typeof document>"u")return;let s=n(),{consents:r,iframeBlockerConfig:o}=s;o?.disableAutomaticBlocking||En(r)},destroyIframeBlocker:()=>{if(!i||typeof document>"u")return;let s=n(),{iframeBlockerConfig:r}=s;r?.disableAutomaticBlocking||(t&&(t.disconnect(),t=null),i=!1)}}}var Pt="c15t:pending-consent-sync";function js(n,e,t,i,s){if(!i||t===null)return!1;let r=new Set(s.filter(a=>a.disabled).map(a=>a.name));return Object.entries(e).some(([a,c])=>!r.has(a)&&n[a]===!0&&c===!1)}async function Ss({manager:n,type:e,get:t,set:i,options:s}){let{callbacks:r,selectedConsents:o,consents:a,consentTypes:c,updateScripts:u,updateIframeConsents:p,updateNetworkBlockerConsents:d,consentCategories:l,locationInfo:y,model:m,consentInfo:b,reloadOnConsentRevoked:h}=t(),w={...a},C=b,A={...o??a??{}},x=Date.now();if(e==="all")for(let f of c)l.includes(f.name)&&(A[f.name]=!0);else if(e==="necessary")for(let f of c)A[f.name]=f.disabled===!0?f.defaultValue:!1;let D=b?.subjectId;D||(D=(0,Vt.L)());let F=t().consentInfo?.externalId||t().user?.id,O=t().consentInfo?.identityProvider||t().user?.identityProvider,X=js(w,A,C,h,c);if(i({consents:A,selectedConsents:A,activeUI:"none",consentInfo:{time:x,subjectId:D,externalId:F,identityProvider:O}}),X){let f={type:e,subjectId:D,externalId:F,identityProvider:O,preferences:A,givenAt:x,jurisdiction:y?.jurisdiction??void 0,jurisdictionModel:m,domain:window.location.hostname,uiSource:s?.uiSource??"api"};try{localStorage.setItem(Pt,JSON.stringify(f))}catch{}r.onConsentSet?.({preferences:A}),r.onBeforeConsentRevocationReload?.({preferences:A}),window.location.reload();return}await new Promise(f=>setTimeout(f,0)),p(),u(),d(),r.onConsentSet?.({preferences:A});let g=await n.setConsent({body:{type:"cookie_banner",domain:window.location.hostname,preferences:A,subjectId:D,externalSubjectId:String(F),identityProvider:O,jurisdiction:y?.jurisdiction??void 0,jurisdictionModel:m??void 0,givenAt:x,uiSource:s?.uiSource??"api",consentAction:e}});if(!g.ok){let f=g.error?.message??"Failed to save consents";r.onError?.({error:f}),r.onError||console.error(f)}}function As(n,e){return n==null||n==="NONE"?null:e&&["UK_GDPR","GDPR"].includes(n)?"iab":["UK_GDPR","GDPR","CH","BR","APPI","PIPA","QC_LAW25"].includes(n)?"opt-in":["CCPA","AU","PIPEDA"].includes(n)?"opt-out":"opt-in"}function zs(){if(typeof window>"u")return!1;try{let e=window.navigator.globalPrivacyControl;return e===!0||e==="1"}catch{return!1}}var Vn=U("./src/libs/iab-tcf/cmp-defaults.ts");function be({get:n,set:e},t){let{iab:i}=n();i&&e({iab:{...i,...t}})}async function Ps(n,e,t){let{get:i}=e;if(t!==null){be(e,{isLoadingGVL:!0,nonIABVendors:n.customVendors??[]});try{let{initializeIABStub:s,fetchGVL:r,createCMPApi:o}=await Promise.resolve().then(U.bind(U,"./src/libs/iab-tcf/index.ts"));s();let a;if(t)a=t;else if(a=await r(),a===null)return void be(e,{isLoadingGVL:!1});be(e,{gvl:a,isLoadingGVL:!1});let c={},u={};for(let[h,w]of Object.entries(a.vendors)){let C=String(h);w.purposes&&w.purposes.length>0&&(c[C]=!1),w.legIntPurposes&&w.legIntPurposes.length>0&&(u[C]=!0)}(n.customVendors??[]).forEach(h=>{let w=String(h.id);h.purposes&&h.purposes.length>0&&(c[w]=!1),h.legIntPurposes&&h.legIntPurposes.length>0&&(u[w]=!0)});let d=(0,Y.If)(i().storageConfig);d?.iabCustomVendorConsents&&Object.assign(c,d.iabCustomVendorConsents),d?.iabCustomVendorLegitimateInterests&&Object.assign(u,d.iabCustomVendorLegitimateInterests),be(e,{vendorConsents:c,vendorLegitimateInterests:u});let l=n.cmpId??Vn.D,y=n.cmpVersion??Vn.I;if(l===0)throw new Error("[c15t] IAB TCF Error: CMP ID is 0. A valid CMP ID registered with IAB Europe is required for IAB TCF compliance.\nIf using consent.io, the CMP ID should be provided automatically via /init.\nIf self-hosting, configure it on the backend via `advanced.iab.cmpId` or on the client via `iab.cmpId`.\nTo register your own CMP: https://iabeurope.eu/tcf-for-cmps/");let m=o({cmpId:l,cmpVersion:y,gvl:a,gdprApplies:!0});be(e,{cmpApi:m});let b=m.loadFromStorage();b&&await Ts(b,e),i().updateScripts()}catch(s){console.error("Failed to initialize IAB mode:",s),be(e,{isLoadingGVL:!1})}}}async function Ts(n,e){let{set:t}=e;try{let{decodeTCString:i,iabPurposesToC15tConsents:s}=await Promise.resolve().then(U.bind(U,"./src/libs/iab-tcf/index.ts")),r=await i(n),o=(0,Y.If)(e.get().storageConfig),a={...r.vendorConsents,...o?.iabCustomVendorConsents??{}},c={...r.vendorLegitimateInterests,...o?.iabCustomVendorLegitimateInterests??{}},u=s(r.purposeConsents);be(e,{tcString:n,purposeConsents:r.purposeConsents,purposeLegitimateInterests:r.purposeLegitimateInterests,vendorConsents:a,vendorLegitimateInterests:c,specialFeatureOptIns:r.specialFeatureOptIns}),t({consents:u,selectedConsents:u,activeUI:"none"})}catch{}}function Ls(n,e){return n?{necessary:!0,functionality:!0,experience:!0,marketing:!e,measurement:!e}:null}function Yn(n,e,t,i){let s=As(n,e),r=i!==void 0?i:zs(),a=Ls((s===null||s==="opt-out")&&t===null,r);return{consentModel:s,autoGrantedConsents:a}}function Es(n,e,t,i){let{get:s,initialTranslationConfig:r}=e,{consentInfo:o}=s(),{translations:a,location:c}=n,{consentModel:u,autoGrantedConsents:p}=Yn(n.jurisdiction??null,i,o,e.get().overrides?.gpc),d={model:u,isLoadingConsentInfo:!1,branding:n.branding??"c15t",hasFetchedBanner:!0,lastBannerFetchData:n,locationInfo:{countryCode:c?.countryCode??null,regionCode:c?.regionCode??null,jurisdiction:n.jurisdiction??null}};return o===null&&(d.activeUI=u?"banner":"none"),p&&(d.consents=p,d.selectedConsents=p),a?.language&&a?.translations&&(d.translationConfig=(0,le.prepareTranslationConfig)({translations:{[a.language]:a.translations},disableAutoLanguageSwitch:!0,defaultLanguage:a.language},r)),d}function Vs(n,e,t){let{get:i}=e,{callbacks:s}=i(),{translations:r}=n;t&&s?.onConsentSet?.({preferences:t}),r?.language&&r?.translations&&s?.onBannerFetched?.({jurisdiction:n.jurisdiction,location:n.location,translations:{language:r.language,translations:r.translations}})}function Wn(n,e,t,i){let{set:s,get:r}=e,{consentInfo:o,iab:a}=r(),c=a?.config.enabled&&!i,u=a?.config.enabled&&!c;c&&console.warn("IAB mode disabled: Server returned 200 without GVL. Client IAB settings overridden.");let{consentModel:p,autoGrantedConsents:d}=Yn(n.jurisdiction??null,u,o,r().overrides?.gpc),l=Es(n,e,t,u);if(c&&a?l.iab={...a,config:{...a.config,enabled:!1}}:a&&n.cmpId!=null&&(l.iab={...a,config:{...a.config,cmpId:n.cmpId}}),s(l),Vs(n,e,d),r().updateScripts(),u&&p==="iab"&&a){let y=n.customVendors??[],m=a.config.customVendors??[],b=new Set(y.map(C=>C.id)),h=[...y,...m.filter(C=>!b.has(C.id))],w={...a.config,customVendors:h,...n.cmpId!=null&&{cmpId:n.cmpId}};Ps(w,{set:s,get:r},i).catch(C=>{console.error("Failed to initialize IAB mode in updateStore:",C)})}}function xs(n){try{if(window.localStorage)return window.localStorage.setItem("c15t-storage-test-key","test"),window.localStorage.removeItem("c15t-storage-test-key"),!0}catch(e){console.warn("localStorage not available, skipping consent banner:",e),n({isLoadingConsentInfo:!1,activeUI:"none"})}return!1}async function xn(n){let{get:e,set:t,manager:i}=n,{callbacks:s}=e();if(typeof window>"u")return;let r=xs(t);if(!r)return;t({isLoadingConsentInfo:!0}),Ds(i,s);let o=await Fs(n);return o||Bs(n,r,i,s)}async function Fs(n){let{ssrData:e,get:t,set:i}=n;if(!e||t().overrides)return void i({ssrDataUsed:!1,ssrSkippedReason:"no_data"});let s=await e;if(s?.init)return Wn(s.init,n,!0,s.gvl),i({ssrDataUsed:!0,ssrSkippedReason:null}),s.init;i({ssrDataUsed:!1,ssrSkippedReason:"fetch_failed"})}async function Bs(n,e,t,i){let{set:s}=n;try{let{language:r,country:o,region:a}=n.get().overrides??{},{data:c,error:u}=await t.init({headers:{...r&&{"accept-language":r},...o&&{"x-c15t-country":o},...a&&{"x-c15t-region":a}},onError:i.onError?p=>{i.onError?.({error:p.error?.message||"Unknown error"})}:void 0});if(u||!c)throw new Error(`Failed to fetch consent banner info: ${u?.message}`);return Wn(c,n,e,c.gvl??void 0),c}catch(r){console.error("Error fetching consent banner information:",r),s({isLoadingConsentInfo:!1,activeUI:"none"});let o=r instanceof Error?r.message:"Unknown error fetching consent banner information";i.onError?.({error:o});return}}function Ds(n,e){try{let t=localStorage.getItem(Pt);if(!t)return;localStorage.removeItem(Pt);let i=JSON.parse(t);n.setConsent({body:{type:"cookie_banner",domain:i.domain,preferences:i.preferences,subjectId:i.subjectId,externalSubjectId:i.externalId,identityProvider:i.identityProvider,jurisdiction:i.jurisdiction,jurisdictionModel:i.jurisdictionModel??void 0,givenAt:i.givenAt,uiSource:i.uiSource??"api"}}).then(s=>{if(!s.ok){let r=s.error?.message??"Failed to sync consent after reload";e.onError?.({error:r}),e.onError||console.error("Failed to sync consent after reload:",r)}}).catch(s=>{let r=s instanceof Error?s.message:"Failed to sync consent after reload";e.onError?.({error:r}),e.onError||console.error("Failed to sync consent after reload:",s)})}catch{}}function Tt(n){return n?n.toUpperCase():"GET"}function Ns(n){if(!n)return null;try{return typeof window>"u"?null:new URL(n,window.location.href)}catch{return null}}function _s(n,e){if(!n)return!1;let t=e.domain.trim().toLowerCase(),i=n.trim().toLowerCase();if(!t||!i)return!1;if(i===t)return!0;let s=`.${t}`;return i.endsWith(s)}function Os(n,e){return typeof e.pathIncludes=="string"?n?n.includes(e.pathIncludes):!1:!0}function Rs(n,e){if(!e.methods||e.methods.length===0)return!0;if(!n)return!1;let t=Tt(n);return e.methods.some(i=>Tt(i)===t)}function Ms(n,e,t){return!(!_s(n.hostname,t)||!Os(n.pathname,t)||!Rs(e,t))}function Fn(n,e,t){if(!t)return{shouldBlock:!1};if(!(t.enabled!==!1))return{shouldBlock:!1};if(!t.rules||t.rules.length===0)return{shouldBlock:!1};let s=Ns(n.url);if(!s)return{shouldBlock:!1};let r=Tt(n.method);for(let o of t.rules){if(!Ms(s,r,o))continue;if(!tt(o.category,e))return{shouldBlock:!0,rule:o}}return{shouldBlock:!1}}function Us(n,e){let t=null,i=null,s=null,r=!1,o=null,a=(d,l)=>{if(d){if(d.logBlockedRequests!==!1){let y=l.rule?.id??"unknown";console.warn("[c15t] Network request blocked by consent manager",{method:l.method,url:l.url,ruleId:y})}d.onRequestBlocked&&d.onRequestBlocked(l)}},c=()=>o||n().consents,u=()=>{typeof window>"u"||!(typeof window.fetch=="function")||t||(t=window.fetch,window.fetch=(l,y)=>{let b=n().networkBlocker;if(!t)throw new Error("Network blocker fetch wrapper not initialized.");if(!(b?.enabled&&b?.rules&&b?.rules.length>0))return t.call(window,l,y);let w="GET";y?.method?w=y.method:l instanceof Request&&(w=l.method);let C;C=typeof l=="string"||l instanceof URL?l.toString():l.url;let A=c(),{shouldBlock:x,rule:D}=Fn({url:C,method:w},A,b);if(x){a(b,{method:w,url:C,rule:D});let F=new Response(null,{status:451,statusText:"Request blocked by consent manager"});return Promise.resolve(F)}return t.call(window,l,y)})},p=()=>{typeof window>"u"||!(window.XMLHttpRequest!==void 0&&typeof window.XMLHttpRequest.prototype.open=="function"&&typeof window.XMLHttpRequest.prototype.send=="function")||i||s||(i=window.XMLHttpRequest.prototype.open,s=window.XMLHttpRequest.prototype.send,window.XMLHttpRequest.prototype.open=function(l,y,m,b,h){let w=this;if(w.__c15tMethod=l,w.__c15tUrl=y,!i)throw new Error("Network blocker XHR open wrapper not initialized.");return i.call(this,l,y,m??!0,b,h)},window.XMLHttpRequest.prototype.send=function(l){let m=n().networkBlocker;if(m?.enabled!==!1&&m?.rules&&m?.rules.length>0){let w=this,C=w.__c15tMethod||"GET",A=w.__c15tUrl||"",x=c(),{shouldBlock:D,rule:F}=Fn({url:A,method:C},x,m);if(D){a(m,{method:C,url:A,rule:F});try{this.abort()}catch{}let O=new ProgressEvent("error");typeof this.onerror=="function"&&this.onerror(O),this.dispatchEvent(O);return}}if(!s)throw new Error("Network blocker XHR send wrapper not initialized.");return s.call(this,l)})};return{initializeNetworkBlocker:()=>{if(r||typeof window>"u")return;let d=n(),l=d.networkBlocker;l?.enabled&&l?.rules&&l?.rules.length>0&&(o=d.consents,u(),p(),r=!0)},updateNetworkBlockerConsents:()=>{r&&(o=n().consents)},setNetworkBlocker:d=>{let y=d?.enabled!==!1&&d?.rules&&d?.rules.length>0;if(e({networkBlocker:d}),!y){if(!r||typeof window>"u")return;t&&(window.fetch=t,t=null),i&&s&&(window.XMLHttpRequest.prototype.open=i,window.XMLHttpRequest.prototype.send=s,i=null,s=null),o=null,r=!1;return}r||(o=n().consents,u(),p(),r=!0)},destroyNetworkBlocker:()=>{r&&(typeof window>"u"||(t&&(window.fetch=t,t=null),i&&s&&(window.XMLHttpRequest.prototype.open=i,window.XMLHttpRequest.prototype.send=s,i=null,s=null),o=null,r=!1))}}}var Gs=U("./src/store/initial-state.ts"),Bn=n=>{if(typeof window>"u")return null;try{return(0,Y.If)(n)}catch(e){return console.error("Failed to retrieve stored consent:",e),null}},Hs=(n,e={})=>{let{namespace:t="c15tStore",iab:i,ssrData:s,initialConsentCategories:r,initialTranslationConfig:o,enabled:a,debug:c,...u}=e;(0,K.tJ)(e.debug===!0);let p=Bn(e.storageConfig),d=qt((l,y)=>({...Gs.ue,...u,namespace:t,iab:i?(0,Cs.yx)(i,y,l,n):null,...r&&{consentCategories:r},...p?{consents:p.consents,selectedConsents:p.consents,consentInfo:p.consentInfo,user:p.consentInfo?.externalId?{id:p.consentInfo.externalId,identityProvider:p.consentInfo.identityProvider}:void 0,activeUI:"none",isLoadingConsentInfo:!1}:{activeUI:"none",isLoadingConsentInfo:!0},setActiveUI:(m,b={})=>{if(m==="none"||m==="dialog")return void l({activeUI:m});if(b.force)return void l({activeUI:"banner"});let h=y();!Bn()&&!h.consentInfo&&!h.isLoadingConsentInfo&&l({activeUI:"banner"})},setSelectedConsent:(m,b)=>{l(h=>h.consentTypes.find(C=>C.name===m)?.disabled?h:{selectedConsents:{...h.selectedConsents,[m]:b}})},saveConsents:async(m,b)=>await Ss({manager:n,type:m,get:y,set:l,options:b}),setConsent:(m,b)=>{l(h=>h.consentTypes.find(A=>A.name===m)?.disabled?h:{selectedConsents:{...h.consents,[m]:b}}),y().saveConsents("custom")},resetConsents:()=>{l(()=>{let m=Te.y.reduce((h,w)=>(h[w.name]=w.defaultValue,h),{}),b={consents:m,selectedConsents:m,consentInfo:null};return(0,Y.jD)(void 0,e.storageConfig),b})},setConsentCategories:m=>l({consentCategories:m}),setCallback:(m,b)=>{let h=y();if(l(w=>({callbacks:{...w.callbacks,[m]:b}})),m==="onConsentSet"&&b&&typeof b=="function"&&b?.({preferences:h.consents}),m==="onBannerFetched"&&h.hasFetchedBanner&&h.lastBannerFetchData&&b&&typeof b=="function"){let{lastBannerFetchData:w}=h,C=w.jurisdiction??"NONE";b?.({jurisdiction:{code:C,message:""},location:{countryCode:w.location.countryCode??null,regionCode:w.location.regionCode??null},translations:{language:w.translations.language,translations:w.translations.translations}})}},setLocationInfo:m=>l({locationInfo:m}),initConsentManager:()=>xn({manager:n,ssrData:e.ssrData,initialTranslationConfig:e.initialTranslationConfig,get:y,set:l}),getDisplayedConsents:()=>{let{consentCategories:m,consentTypes:b}=y();return b.filter(h=>m.includes(h.name))},hasConsented:()=>{let{consentInfo:m}=y();return m!=null},has:m=>{let{consents:b}=y();return tt(m,b)},setTranslationConfig:m=>{l({translationConfig:m})},updateConsentCategories:m=>{let b=new Set([...y().consentCategories,...m]),h=Array.from(b);l({consentCategories:h})},identifyUser:async m=>{let b=y().consentInfo,h=b?.subjectId;l({user:m}),h&&(String(b?.externalId)===String(m.id)&&b?.identityProvider===m.identityProvider||(await n.identifyUser({body:{id:h,externalId:m.id,identityProvider:m.identityProvider}}),l({consentInfo:{...b,time:b?.time||Date.now(),subjectId:h,externalId:m.id,identityProvider:m.identityProvider}})))},setOverrides:async m=>(l({overrides:{...y().overrides,...m}}),await xn({manager:n,initialTranslationConfig:e.initialTranslationConfig,get:y,set:l})),setLanguage:async m=>await y().setOverrides({...y().overrides??{},language:m}),...bs(y,l),...Is(y,l),...Us(y,l)}));return d.getState().initializeIframeBlocker(),e.networkBlocker&&(d.setState({networkBlocker:e.networkBlocker}),d.getState().initializeNetworkBlocker()),e.scripts&&e.scripts.length>0&&d.getState().updateConsentCategories(e.scripts.flatMap(l=>Hn(l.category))),typeof window<"u"&&(window[t]=d,d.getState().callbacks.onConsentSet?.({preferences:d.getState().consents}),e.user&&d.getState().identifyUser(e.user),d.getState().initConsentManager()),d},qs="/api/c15t",Dn=new Map,Nn=new Map;function $s(n){let e=n.enabled===!1?"disabled":"enabled";return`${n.mode??"c15t"}:${n.backendURL??"default"}:${n.endpointHandlers?"custom":"none"}:${n.storageConfig?.storageKey??"default"}:${n.defaultLanguage??"default"}:${e}`}function Jn(n,e){let{mode:t,backendURL:i,store:s,translations:r,storageConfig:o,enabled:a,iab:c,consentCategories:u,debug:p}=n,d=$s({mode:t,backendURL:i,endpointHandlers:"endpointHandlers"in n?n.endpointHandlers:void 0,storageConfig:o,defaultLanguage:r?.defaultLanguage,enabled:a}),l=Dn.get(d);if(!l){let m={...s,initialTranslationConfig:r,iab:c};l=t==="offline"?wt({mode:"offline",store:m,storageConfig:o}):t==="custom"&&"endpointHandlers"in n?wt({mode:"custom",endpointHandlers:n.endpointHandlers,store:m,storageConfig:o}):wt({mode:"c15t",backendURL:i||qs,store:m,storageConfig:o}),Dn.set(d,l)}let y=Nn.get(d);return y||(y=Hs(l,{config:{pkg:e?.pkg||"c15t",version:e?.version||ws.r,mode:t||"Unknown"},...n,...s,initialTranslationConfig:r,initialConsentCategories:u,debug:p}),Nn.set(d,y)),{consentManager:l,consentStore:y,cacheKey:d}}var Ha=Te.W,qa=nt.xe,$a=Te.y,Ka=le.deepMergeTranslations,Ya=We.Z,Wa=Y.jD,Ja=Y.Yj,Za=le.detectBrowserLanguage,Qa=nt.fetchGVL,Xa=Vt.L,ec=nt.Ww,tc=Y.If,nc=Y.Ri,ic=Y.Xk,sc=Vt.U,rc=le.mergeTranslationConfigs,oc=le.prepareTranslationConfig,ac=Y._y,cc=Y.TV,lc=nt.wL;window.c15t={getOrCreateConsentRuntime:Jn};})(); diff --git a/docs/theme/consent-banner.css b/docs/theme/consent-banner.css new file mode 100644 index 0000000000000000000000000000000000000000..bdebbed80a997ca57be2516cdd0472fd4f52cae9 --- /dev/null +++ b/docs/theme/consent-banner.css @@ -0,0 +1,292 @@ +#c15t-banner { + --color-offgray-50: hsl(218, 12%, 95%); + --color-offgray-100: hsl(218, 12%, 88%); + --color-offgray-200: hsl(218, 12%, 80%); + --color-offgray-300: hsl(218, 12%, 75%); + --color-offgray-400: hsl(218, 12%, 64%); + --color-offgray-500: hsl(218, 12%, 56%); + --color-offgray-600: hsl(218, 12%, 48%); + --color-offgray-700: hsl(218, 12%, 40%); + --color-offgray-800: hsl(218, 12%, 34%); + --color-offgray-900: hsl(218, 12%, 24%); + --color-offgray-950: hsl(218, 12%, 15%); + --color-offgray-1000: hsl(218, 12%, 5%); + + --color-blue-50: oklch(97% 0.014 254.604); + --color-blue-100: oklch(93.2% 0.032 255.585); + --color-blue-200: oklch(88.2% 0.059 254.128); + --color-blue-300: oklch(80.9% 0.105 251.813); + --color-blue-400: oklch(70.7% 0.165 254.624); + --color-blue-500: oklch(62.3% 0.214 259.815); + --color-blue-600: oklch(54.6% 0.245 262.881); + --color-blue-700: oklch(48.8% 0.243 264.376); + --color-blue-800: oklch(42.4% 0.199 265.638); + --color-blue-900: oklch(37.9% 0.146 265.522); + --color-blue-950: oklch(28.2% 0.091 267.935); + + --color-accent-blue: hsla(218, 93%, 42%, 1); + + position: fixed; + z-index: 9999; + bottom: 16px; + right: 16px; + border-radius: 4px; + max-width: 300px; + background: white; + border: 1px solid + color-mix(in oklab, var(--color-offgray-200) 50%, transparent); + box-shadow: 6px 6px 0 + color-mix(in oklab, var(--color-accent-blue) 6%, transparent); +} + +.dark #c15t-banner { + border-color: color-mix(in oklab, var(--color-offgray-600) 14%, transparent); + background: var(--color-offgray-1000); + box-shadow: 5px 5px 0 + color-mix(in oklab, var(--color-accent-blue) 8%, transparent); +} + +#c15t-banner > div:first-child { + padding: 12px; + display: flex; + flex-direction: column; +} + +#c15t-banner a { + color: var(--links); + text-decoration: underline; + text-decoration-color: var(--link-line-decoration); +} + +#c15t-banner a:hover { + text-decoration-color: var(--link-line-decoration-hover); +} + +#c15t-description { + font-size: 12px; + margin: 0; + margin-top: 4px; +} + +#c15t-configure-section { + display: flex; + flex-direction: column; + gap: 8px; + border-top: 1px solid var(--divider); + padding: 12px; +} + +#c15t-configure-section > div { + display: flex; + align-items: center; + justify-content: space-between; +} + +#c15t-configure-section label { + text-transform: uppercase; + font-size: 11px; +} + +#c15t-footer { + padding: 12px; + display: flex; + justify-content: space-between; + border-top: 1px solid var(--divider); + background-color: color-mix( + in oklab, + var(--color-offgray-50) 50%, + transparent + ); +} + +.dark #c15t-footer { + background-color: color-mix( + in oklab, + var(--color-offgray-600) 4%, + transparent + ); +} + +.c15t-button { + display: inline-flex; + align-items: center; + justify-content: center; + max-height: 28px; + color: black; + padding: 4px 8px; + font-size: 14px; + border-radius: 4px; + background: transparent; + border: 1px solid transparent; + transition: 100ms; + transition-property: box-shadow, border-color, background-color; +} + +.c15t-button:hover { + background: color-mix(in oklab, var(--color-offgray-100) 50%, transparent); +} + +.dark .c15t-button { + color: var(--color-offgray-50); +} + +.dark .c15t-button:hover { + background: color-mix(in oklab, var(--color-offgray-500) 10%, transparent); +} + +.c15t-button.icon { + padding: 0; + width: 24px; + height: 24px; +} + +.c15t-button.primary { + color: var(--color-blue-700); + background: color-mix(in oklab, var(--color-blue-50) 60%, transparent); + border-color: color-mix(in oklab, var(--color-blue-500) 20%, transparent); + box-shadow: color-mix(in oklab, var(--color-blue-400) 10%, transparent) 0 -2px + 0 0 inset; +} + +.c15t-button.primary:hover { + background: color-mix(in oklab, var(--color-blue-100) 50%, transparent); + box-shadow: none; +} + +.dark .c15t-button.primary { + color: var(--color-blue-50); + background: color-mix(in oklab, var(--color-blue-500) 10%, transparent); + border-color: color-mix(in oklab, var(--color-blue-300) 10%, transparent); + box-shadow: color-mix(in oklab, var(--color-blue-300) 8%, transparent) 0 -2px + 0 0 inset; +} + +.dark .c15t-button.primary:hover { + background: color-mix(in oklab, var(--color-blue-500) 20%, transparent); + box-shadow: none; +} + +.c15t-button.secondary { + background: color-mix(in oklab, var(--color-offgray-50) 60%, transparent); + border-color: color-mix(in oklab, var(--color-offgray-200) 50%, transparent); + box-shadow: color-mix(in oklab, var(--color-offgray-500) 10%, transparent) + 0 -2px 0 0 inset; +} + +.c15t-button.secondary:hover { + background: color-mix(in oklab, var(--color-offgray-100) 50%, transparent); + box-shadow: none; +} + +.dark .c15t-button.secondary { + background: color-mix(in oklab, var(--color-offgray-300) 5%, transparent); + border-color: color-mix(in oklab, var(--color-offgray-400) 20%, transparent); + box-shadow: color-mix(in oklab, var(--color-offgray-300) 8%, transparent) + 0 -2px 0 0 inset; +} + +.dark .c15t-button.secondary:hover { + background: color-mix(in oklab, var(--color-offgray-200) 10%, transparent); + box-shadow: none; +} + +.c15t-switch { + position: relative; + display: inline-block; + width: 32px; + height: 20px; + flex-shrink: 0; +} + +.c15t-switch input { + opacity: 0; + width: 0; + height: 0; + position: absolute; +} + +.c15t-slider { + position: absolute; + cursor: pointer; + inset: 0; + background-color: color-mix( + in oklab, + var(--color-offgray-100) 80%, + transparent + ); + border-radius: 20px; + box-shadow: inset 0 0 0 1px color-mix(in oklab, #000 5%, transparent); + transition: background-color 0.2s; +} + +.c15t-slider:hover { + background-color: var(--color-offgray-100); +} + +.dark .c15t-slider { + background-color: color-mix(in oklab, #fff 5%, transparent); + box-shadow: inset 0 0 0 1px color-mix(in oklab, #fff 15%, transparent); +} + +.dark .c15t-slider:hover { + background-color: color-mix(in oklab, #fff 10%, transparent); +} + +.c15t-slider:before { + position: absolute; + content: ""; + height: 14px; + width: 14px; + left: 3px; + bottom: 3px; + background-color: white; + border-radius: 50%; + box-shadow: + 0 1px 3px 0 rgb(0 0 0 / 0.1), + 0 1px 2px -1px rgb(0 0 0 / 0.1); + transition: transform 0.2s; +} + +.c15t-switch input:checked + .c15t-slider { + background-color: var(--color-accent-blue); + box-shadow: inset 0 0 0 1px color-mix(in oklab, #000 5%, transparent); +} + +.c15t-switch input:checked + .c15t-slider:hover { + background-color: var(--color-accent-blue); +} + +.dark .c15t-switch input:checked + .c15t-slider { + background-color: var(--color-accent-blue); + box-shadow: inset 0 0 0 1px color-mix(in oklab, #fff 15%, transparent); +} + +.c15t-switch input:checked + .c15t-slider:before { + transform: translateX(12px); +} + +.c15t-switch input:disabled + .c15t-slider { + opacity: 0.5; + cursor: default; + pointer-events: none; +} + +.c15t-switch input:disabled + .c15t-slider:hover { + background-color: color-mix( + in oklab, + var(--color-offgray-100) 80%, + transparent + ); +} + +#c15t-manage-consent-btn { + appearance: none; + background: none; + border: none; + padding: 0; + cursor: pointer; +} + +#c15t-manage-consent-btn:hover { + text-decoration-color: var(--link-line-decoration-hover); +} diff --git a/docs/theme/index.hbs b/docs/theme/index.hbs index 8e6d185a57874a84bd373115e2f4b988a6c0b864..1c833ee94d428a1578b35c7944c4d300a04a21db 100644 --- a/docs/theme/index.hbs +++ b/docs/theme/index.hbs @@ -70,6 +70,8 @@ {{/if}} + +
@@ -343,6 +345,13 @@ href="https://zed.dev/blog" >Blog + +
@@ -444,23 +453,82 @@ {{/if}} {{/if}} - - + +
diff --git a/typos.toml b/typos.toml index 6f76cc75d25add39d841c07bbde82f93514adac5..c4e326359dec6e2a47861df1aab7b66f0644d7a3 100644 --- a/typos.toml +++ b/typos.toml @@ -42,6 +42,8 @@ extend-exclude = [ "crates/gpui_windows/src/window.rs", # Some typos in the base mdBook CSS. "docs/theme/css/", + # Automatically generated JS. + "docs/theme/c15t@*.js", # Spellcheck triggers on `|Fixe[sd]|` regex part. "script/danger/dangerfile.ts", # Eval examples for prompts and criteria