From f4e9603f3ebe7dd6c6e292a1563128e19da95cbd Mon Sep 17 00:00:00 2001 From: HugoCasa Date: Wed, 25 Feb 2026 15:44:42 +0100 Subject: [PATCH 01/18] workmux better ee cleanup + cursor wrapper autocompletion and open-ee --- README_WORKMUX_DEV.md | 12 +++++- scripts/wm-cursor | 81 +++++++++++++++++++++++++++++++++++++++- scripts/worktree-cleanup | 30 ++++++++++----- 3 files changed, 110 insertions(+), 13 deletions(-) diff --git a/README_WORKMUX_DEV.md b/README_WORKMUX_DEV.md index bfb2d7e576..0b113e47ee 100644 --- a/README_WORKMUX_DEV.md +++ b/README_WORKMUX_DEV.md @@ -65,7 +65,7 @@ Setting up zsh autocomplete is also recommended — see the [workmux docs](https Each worktree is assigned a **slot** that determines its ports: | Slot | Backend | Frontend | -|------|---------|----------| +| ---- | ------- | -------- | | 0 | 8000 | 3000 | | 1 | 8010 | 3010 | | 2 | 8020 | 3020 | @@ -192,7 +192,6 @@ sandbox: This mounts both the main EE repo (used by the main worktree) and the EE worktrees directory (used by feature worktrees) into every sandbox container. - ## Cursor SSH Integration (`wmc`) `wm-cursor` (aliased as `wmc`) gives each worktree its own Cursor SSH remote window with an independently-focused tmux session. All windows are visible in the status bar across all Cursor terminals, but each one is focused on its own worktree. @@ -219,6 +218,7 @@ This: 1. **Merges `.vscode/settings.json`** — adds the `wm-tmux` terminal profile (auto-attaches to the `main` tmux session), disables auto port forwarding, configures forwarding for ports 8000/3000/5432, and stops rust-analyzer from auto-starting. Existing settings are preserved. 2. **Creates `.vscode/tasks.json`** — auto-starts the dev database (`start-dev-db.sh`) when the folder opens. 3. **Adds `wmc` alias to `~/.zshrc`** — so you can use `wmc` from any tmux window. +4. **Adds `eval "$(wmc completions)"`** to `~/.zshrc` — provides tab-completion for subcommands and worktree names (for `open`, `open-ee`, and `close`). After setup, reopen Cursor's terminal to pick up the new profile. @@ -240,6 +240,14 @@ This runs `workmux add`, creates a grouped tmux session, writes `.vscode/setting wmc open my-feature ``` +**Open the EE worktree in Cursor (no tmux session):** + +```bash +wmc open-ee my-feature +``` + +This finds the matching `windmill-ee-private__worktrees/` directory and opens it in a new Cursor window. + **Close a worktree's Cursor window and tmux window (keeps the worktree):** ```bash diff --git a/scripts/wm-cursor b/scripts/wm-cursor index 552bdcd985..596d89bd3c 100755 --- a/scripts/wm-cursor +++ b/scripts/wm-cursor @@ -199,6 +199,40 @@ cmd_close() { workmux close $name } +cmd_open_ee() { + local name=${1:?Usage: wm-cursor open-ee } + + local wt_path=$(workmux path $name) + local main_repo_root="$(cd "$(git -C "$wt_path" rev-parse --git-common-dir 2>/dev/null)/.." && pwd)" + + # Find ee repo (same discovery logic as worktree-env) + local ee_repo="" candidate + for candidate in \ + "${main_repo_root:+${main_repo_root}/../windmill-ee-private}" \ + "${wt_path}/../windmill-ee-private" \ + "${HOME}/windmill-ee-private" \ + "${HOME}/projects/windmill-ee-private"; do + if [[ -n $candidate ]] && [[ -d $candidate ]]; then + ee_repo=${candidate:A} + break + fi + done + + if [[ -z $ee_repo ]]; then + print -u2 "Error: Could not find windmill-ee-private repo" + exit 1 + fi + + local ee_worktree_dir="${ee_repo}__worktrees/${name}" + if [[ ! -d $ee_worktree_dir ]]; then + print -u2 "Error: EE worktree not found at ${ee_worktree_dir}" + exit 1 + fi + + $cursor_bin -n $ee_worktree_dir + print "Opened Cursor for EE worktree: ${ee_worktree_dir}" +} + cmd_setup() { local repo_root=${1:?Usage: wm-cursor setup } repo_root=${repo_root:A} @@ -275,7 +309,7 @@ json.dump(json.loads(text), sys.stdout, indent=2) print "Created ${settings_file}" fi - # --- zsh alias --- + # --- zsh alias + completion --- local rc=${ZDOTDIR:-$HOME}/.zshrc local alias_line="alias wmc=${(q)script_path}" @@ -286,6 +320,45 @@ json.dump(json.loads(text), sys.stdout, indent=2) print "\n# wm-cursor alias\n${alias_line}" >> $rc print "Added wmc alias to ${rc}" fi + + local eval_line='eval "$(wmc completions)"' + if ! grep -qF 'wmc completions' $rc; then + print "${eval_line}" >> $rc + print "Added completions eval to ${rc}" + fi +} + +cmd_completions() { + cat <<'COMP' +_wmc_worktree_names() { + local -a names + names=(${(f)"$(git worktree list --porcelain 2>/dev/null | sed -n 's|^worktree .*/||p' | tail -n +2)"}) + _describe 'worktree' names +} + +_wmc() { + local -a subcmds=( + 'add:Create worktree + open Cursor' + 'open:Open Cursor for existing worktree' + 'open-ee:Open EE worktree in Cursor' + 'close:Clean up grouped tmux session' + 'setup:Set up .vscode settings, tasks + wmc alias' + 'completions:Print zsh completions' + ) + + if (( CURRENT == 2 )); then + _describe 'subcommand' subcmds + else + case $words[2] in + open|open-ee|close) + _wmc_worktree_names + ;; + esac + fi +} + +compdef _wmc wmc wm-cursor +COMP } # --- Main --- @@ -293,16 +366,20 @@ json.dump(json.loads(text), sys.stdout, indent=2) case ${1-} in add) shift; cmd_add "$@" ;; open) shift; cmd_open "$@" ;; + open-ee) shift; cmd_open_ee "$@" ;; close) shift; cmd_close "$@" ;; setup) shift; cmd_setup "$@" ;; + completions) cmd_completions ;; *) - print -u2 "Usage: wm-cursor [args...]" + print -u2 "Usage: wm-cursor [args...]" print -u2 "" print -u2 "Subcommands:" print -u2 " add [--features ] [workmux-add-args...] Create worktree + open Cursor" print -u2 " open [--features ] Open Cursor for existing worktree" + print -u2 " open-ee Open EE worktree in Cursor" print -u2 " close Clean up grouped tmux session" print -u2 " setup Set up .vscode settings, tasks + wmc alias" + print -u2 " completions Print zsh completions (use with eval)" print -u2 "" print -u2 "Options:" print -u2 " --features Cargo features for the backend (e.g. \"enterprise,parquet\")" diff --git a/scripts/worktree-cleanup b/scripts/worktree-cleanup index cdac356635..078d3882e6 100755 --- a/scripts/worktree-cleanup +++ b/scripts/worktree-cleanup @@ -26,18 +26,30 @@ fi # Remove the matching windmill-ee-private worktree if one exists wt_basename=$(basename "$wt_dir") -# Check parent directory first (sibling to worktree root), then fall back to home +# Find ee repo using same discovery logic as worktree-env +main_repo_root="$(cd "$(git -C "$wt_dir" rev-parse --git-common-dir 2>/dev/null)/.." && pwd)" parent_dir="$(cd "$wt_dir/.." && pwd)" -echo "[cleanup] wt_basename=$wt_basename parent_dir=$parent_dir" -if [ -d "${parent_dir}/windmill-ee-private" ]; then - ee_repo="${parent_dir}/windmill-ee-private" -else - ee_repo="${HOME}/windmill-ee-private" +echo "[cleanup] wt_basename=$wt_basename main_repo_root=$main_repo_root parent_dir=$parent_dir" + +ee_repo="" +for candidate in \ + "${main_repo_root:+${main_repo_root}/../windmill-ee-private}" \ + "${parent_dir}/windmill-ee-private" \ + "${HOME}/windmill-ee-private" \ + "${HOME}/projects/windmill-ee-private"; do + if [ -n "$candidate" ] && [ -d "$candidate" ]; then + ee_repo="$(cd "$candidate" && pwd)" + break + fi +done + +if [ -z "$ee_repo" ]; then + echo "[cleanup] Could not find windmill-ee-private repo, skipping EE worktree cleanup" fi -ee_worktree_dir="${ee_repo}__worktrees/${wt_basename}" -echo "[cleanup] ee_repo=$ee_repo ee_worktree_dir=$ee_worktree_dir exists=$([ -d "$ee_worktree_dir" ] && echo yes || echo no)" -if [ -d "$ee_worktree_dir" ]; then +ee_worktree_dir="${ee_repo:+${ee_repo}__worktrees/${wt_basename}}" +echo "[cleanup] ee_repo=${ee_repo:-} ee_worktree_dir=${ee_worktree_dir:-} exists=$([ -n "$ee_worktree_dir" ] && [ -d "$ee_worktree_dir" ] && echo yes || echo no)" +if [ -n "$ee_worktree_dir" ] && [ -d "$ee_worktree_dir" ]; then git -C "$ee_repo" worktree remove "$ee_worktree_dir" --force 2>/dev/null \ && echo "[cleanup] Removed EE worktree at $ee_worktree_dir" \ || echo "[cleanup] Warning: Could not remove EE worktree at $ee_worktree_dir" From 8e7ba9b33da2ddba0eba8341219b9a3576a9d95d Mon Sep 17 00:00:00 2001 From: Diego Imbert <70353967+diegoimbert@users.noreply.github.com> Date: Wed, 25 Feb 2026 16:06:12 +0100 Subject: [PATCH 02/18] feat: Data table as pg resource / trigger (#8088) * Enable running pg scripts with datatable database input * Postgres triggers for data tables * REPLICATION attribute on custom_instance_user * disable edit for datatables * Update backend/windmill-trigger-postgres/src/replication_message.rs Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> --------- Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> --- ..._custom_instance_user_replication.down.sql | 10 ++ ...00_custom_instance_user_replication.up.sql | 10 ++ backend/windmill-api-settings/src/lib.rs | 164 ++++++++++-------- .../src/converter.rs | 15 +- backend/windmill-trigger-postgres/src/lib.rs | 30 +++- .../windmill-trigger-postgres/src/listener.rs | 6 +- .../windmill-trigger-postgres/src/mapper.rs | 5 +- .../windmill-trigger-postgres/src/relation.rs | 62 ++++--- .../src/replication_message.rs | 11 +- frontend/src/lib/components/ArgInput.svelte | 1 + .../lib/components/ObjectResourceInput.svelte | 13 +- .../src/lib/components/ResourcePicker.svelte | 48 +++-- .../PostgresTriggerEditorInner.svelte | 1 + .../CustomInstanceDbWizardModal.svelte | 3 +- 14 files changed, 241 insertions(+), 138 deletions(-) create mode 100644 backend/migrations/20260225000000_custom_instance_user_replication.down.sql create mode 100644 backend/migrations/20260225000000_custom_instance_user_replication.up.sql diff --git a/backend/migrations/20260225000000_custom_instance_user_replication.down.sql b/backend/migrations/20260225000000_custom_instance_user_replication.down.sql new file mode 100644 index 0000000000..bd8b648579 --- /dev/null +++ b/backend/migrations/20260225000000_custom_instance_user_replication.down.sql @@ -0,0 +1,10 @@ +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'custom_instance_user') THEN + ALTER ROLE custom_instance_user NOREPLICATION; + END IF; +EXCEPTION + WHEN others THEN + RAISE NOTICE 'Error revoking REPLICATION from custom_instance_user: %', SQLERRM; +END +$$; diff --git a/backend/migrations/20260225000000_custom_instance_user_replication.up.sql b/backend/migrations/20260225000000_custom_instance_user_replication.up.sql new file mode 100644 index 0000000000..146cdf6afe --- /dev/null +++ b/backend/migrations/20260225000000_custom_instance_user_replication.up.sql @@ -0,0 +1,10 @@ +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'custom_instance_user') THEN + ALTER ROLE custom_instance_user REPLICATION; + END IF; +EXCEPTION + WHEN others THEN + RAISE NOTICE 'Error granting REPLICATION to custom_instance_user: %', SQLERRM; +END +$$; diff --git a/backend/windmill-api-settings/src/lib.rs b/backend/windmill-api-settings/src/lib.rs index a58a760372..04a1eb617b 100644 --- a/backend/windmill-api-settings/src/lib.rs +++ b/backend/windmill-api-settings/src/lib.rs @@ -12,24 +12,24 @@ use std::{collections::HashMap, time::Duration}; mod ee; pub mod ee_oss; -use windmill_api_auth::{require_super_admin, ApiAuthed}; #[cfg(feature = "enterprise")] use windmill_api_auth::require_devops_role; +use windmill_api_auth::{require_super_admin, ApiAuthed}; use windmill_common::utils::HTTP_CLIENT_PERMISSIVE as HTTP_CLIENT; use windmill_common::DB; use ee_oss::validate_license_key; use windmill_common::usernames::generate_instance_username_for_all_users; -use axum::{ - extract::{Extension, Path}, - routing::{get, post}, - body::Body, - response::Response, - Json, Router, -}; #[cfg(feature = "enterprise")] use axum::extract::Query; +use axum::{ + body::Body, + extract::{Extension, Path}, + response::Response, + routing::{get, post}, + Json, Router, +}; use serde_json::json; use serde::{Deserialize, Serialize}; @@ -43,9 +43,8 @@ use windmill_common::{ get_database_url, global_settings::{ APP_WORKSPACED_ROUTE_SETTING, AUTOMATE_USERNAME_CREATION_SETTING, - CRITICAL_ALERT_MUTE_UI_SETTING, DEFAULT_TAGS_WORKSPACES_SETTING, - EMAIL_DOMAIN_SETTING, ENV_SETTINGS, HUB_ACCESSIBLE_URL_SETTING, - HUB_BASE_URL_SETTING, + CRITICAL_ALERT_MUTE_UI_SETTING, DEFAULT_TAGS_WORKSPACES_SETTING, EMAIL_DOMAIN_SETTING, + ENV_SETTINGS, HUB_ACCESSIBLE_URL_SETTING, HUB_BASE_URL_SETTING, }, instance_config::{self, ApplyMode, InstanceConfig}, server::Smtp, @@ -171,7 +170,9 @@ pub async fn test_s3_bucket( .await? .store; - let mut list = client.list(Some(&windmill_object_store::object_store_reexports::Path::from("".to_string()))); + let mut list = client.list(Some( + &windmill_object_store::object_store_reexports::Path::from("".to_string()), + )); let first_file = list.next().await; if first_file.is_some() { if let Err(e) = first_file.as_ref().unwrap() { @@ -189,7 +190,10 @@ pub async fn test_s3_bucket( )); tracing::info!("Testing blob storage at path: {path}"); client - .put(&path, windmill_object_store::object_store_reexports::PutPayload::from_static(b"hello")) + .put( + &path, + windmill_object_store::object_store_reexports::PutPayload::from_static(b"hello"), + ) .await .map_err(|e| anyhow::anyhow!("error writing file to {path}: {e:#}"))?; let content = client @@ -290,17 +294,17 @@ pub async fn set_global_setting_internal( match value { serde_json::Value::Null => { if instance_config::PROTECTED_SETTINGS.contains(&key.as_str()) { - return Err(error::Error::BadRequest( - format!("{key} is a protected setting and cannot be deleted"), - )); + return Err(error::Error::BadRequest(format!( + "{key} is a protected setting and cannot be deleted" + ))); } delete_global_setting(db, &key).await?; } serde_json::Value::String(x) if x.is_empty() => { if instance_config::PROTECTED_SETTINGS.contains(&key.as_str()) { - return Err(error::Error::BadRequest( - format!("{key} is a protected setting and cannot be set to empty"), - )); + return Err(error::Error::BadRequest(format!( + "{key} is a protected setting and cannot be set to empty" + ))); } delete_global_setting(db, &key).await?; } @@ -437,7 +441,8 @@ async fn get_instance_config_yaml( let config = InstanceConfig::from_db(&db) .await .map_err(|e| error::Error::internal_err(e.to_string()))?; - let yaml = config.to_sorted_yaml() + let yaml = config + .to_sorted_yaml() .map_err(|e| error::Error::internal_err(e))?; Response::builder() .header("content-type", "application/yaml") @@ -478,8 +483,7 @@ async fn set_instance_config( .map(|(k, v)| { ( k.clone(), - serde_json::to_value(v) - .expect("WorkerGroupConfig serialization cannot fail"), + serde_json::to_value(v).expect("WorkerGroupConfig serialization cannot fail"), ) }) .collect(); @@ -489,8 +493,7 @@ async fn set_instance_config( .map(|(k, v)| { ( k.clone(), - serde_json::to_value(v) - .expect("WorkerGroupConfig serialization cannot fail"), + serde_json::to_value(v).expect("WorkerGroupConfig serialization cannot fail"), ) }) .collect(); @@ -944,7 +947,8 @@ async fn setup_custom_instance_pg_database_inner( GRANT CREATE ON DATABASE \"{dbname}\" TO custom_instance_user; ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO custom_instance_user; - ALTER ROLE custom_instance_user CREATEROLE;" + ALTER ROLE custom_instance_user CREATEROLE; + ALTER ROLE custom_instance_user REPLICATION;" )) .await .map_err(|e| { @@ -1083,19 +1087,16 @@ async fn sync_cached_resource_types( use windmill_common::worker::HUB_RT_CACHE_DIR; let cache_path = format!("{}/resource_types.json", HUB_RT_CACHE_DIR); - let content = tokio::fs::read_to_string(&cache_path) - .await - .map_err(|e| { - error::Error::NotFound(format!( - "No cached resource types found at {}: {}", - cache_path, e - )) - })?; + let content = tokio::fs::read_to_string(&cache_path).await.map_err(|e| { + error::Error::NotFound(format!( + "No cached resource types found at {}: {}", + cache_path, e + )) + })?; - let cached_types: Vec = - serde_json::from_str(&content).map_err(|e| { - error::Error::InternalErr(format!("Failed to parse cached resource types: {}", e)) - })?; + let cached_types: Vec = serde_json::from_str(&content).map_err(|e| { + error::Error::InternalErr(format!("Failed to parse cached resource types: {}", e)) + })?; let mut synced_count = 0; @@ -1176,7 +1177,10 @@ mod tests { deserialized.global_settings.base_url.as_deref(), Some("https://windmill.example.com") ); - assert_eq!(deserialized.global_settings.retention_period_secs, Some(86400)); + assert_eq!( + deserialized.global_settings.retention_period_secs, + Some(86400) + ); assert_eq!(deserialized.global_settings.expose_metrics, Some(true)); let wc = &deserialized.worker_configs["default"]; assert_eq!( @@ -1208,9 +1212,7 @@ mod tests { let retention_pos = yaml.find("retention_period_secs:").unwrap(); assert!( - base_url_pos < email_pos - && email_pos < expose_pos - && expose_pos < retention_pos, + base_url_pos < email_pos && email_pos < expose_pos && expose_pos < retention_pos, "global_settings keys should be alphabetically sorted, got yaml:\n{yaml}" ); } @@ -1220,22 +1222,34 @@ mod tests { let config = InstanceConfig { global_settings: GlobalSettings::default(), worker_configs: BTreeMap::from([ - ("gpu".to_string(), WorkerGroupConfig { - init_bash: Some("echo gpu".to_string()), - ..Default::default() - }), - ("native".to_string(), WorkerGroupConfig { - init_bash: Some("echo native".to_string()), - ..Default::default() - }), - ("default".to_string(), WorkerGroupConfig { - init_bash: Some("echo default".to_string()), - ..Default::default() - }), - ("alpha".to_string(), WorkerGroupConfig { - init_bash: Some("echo alpha".to_string()), - ..Default::default() - }), + ( + "gpu".to_string(), + WorkerGroupConfig { + init_bash: Some("echo gpu".to_string()), + ..Default::default() + }, + ), + ( + "native".to_string(), + WorkerGroupConfig { + init_bash: Some("echo native".to_string()), + ..Default::default() + }, + ), + ( + "default".to_string(), + WorkerGroupConfig { + init_bash: Some("echo default".to_string()), + ..Default::default() + }, + ), + ( + "alpha".to_string(), + WorkerGroupConfig { + init_bash: Some("echo alpha".to_string()), + ..Default::default() + }, + ), ]), }; @@ -1264,26 +1278,40 @@ mod tests { ..Default::default() }, worker_configs: BTreeMap::from([ - ("default".to_string(), WorkerGroupConfig { - worker_tags: Some(vec!["deno".to_string()]), - ..Default::default() - }), - ("native".to_string(), WorkerGroupConfig { - init_bash: Some("echo hi".to_string()), - ..Default::default() - }), + ( + "default".to_string(), + WorkerGroupConfig { + worker_tags: Some(vec!["deno".to_string()]), + ..Default::default() + }, + ), + ( + "native".to_string(), + WorkerGroupConfig { + init_bash: Some("echo hi".to_string()), + ..Default::default() + }, + ), ]), }; let yaml = config.to_sorted_yaml().unwrap(); let deserialized: InstanceConfig = serde_yml::from_str(&yaml).unwrap(); - assert_eq!(deserialized.global_settings.base_url.as_deref(), Some("https://rt.test")); - assert_eq!(deserialized.global_settings.retention_period_secs, Some(7200)); + assert_eq!( + deserialized.global_settings.base_url.as_deref(), + Some("https://rt.test") + ); + assert_eq!( + deserialized.global_settings.retention_period_secs, + Some(7200) + ); assert_eq!(deserialized.global_settings.expose_metrics, Some(false)); assert_eq!(deserialized.worker_configs.len(), 2); assert_eq!( - deserialized.worker_configs["default"].worker_tags.as_deref(), + deserialized.worker_configs["default"] + .worker_tags + .as_deref(), Some(["deno".to_string()].as_slice()) ); assert_eq!( diff --git a/backend/windmill-trigger-postgres/src/converter.rs b/backend/windmill-trigger-postgres/src/converter.rs index 172b62df77..7fe7de92b9 100644 --- a/backend/windmill-trigger-postgres/src/converter.rs +++ b/backend/windmill-trigger-postgres/src/converter.rs @@ -361,19 +361,13 @@ mod tests { fn test_timestamp() { let result = Converter::try_from_str(Some(Type::TIMESTAMP), "2024-01-15 14:30:00.0").unwrap(); - assert_eq!( - result, - Value::String("2024-01-15 14:30:00".to_string()) - ); + assert_eq!(result, Value::String("2024-01-15 14:30:00".to_string())); } #[test] fn test_timestamptz() { - let result = Converter::try_from_str( - Some(Type::TIMESTAMPTZ), - "2024-01-15 14:30:00.0+00", - ) - .unwrap(); + let result = + Converter::try_from_str(Some(Type::TIMESTAMPTZ), "2024-01-15 14:30:00.0+00").unwrap(); assert!(result.as_str().unwrap().contains("2024-01-15")); } @@ -484,8 +478,7 @@ mod tests { #[test] fn test_array_with_escaped_backslash() { - let result = - Converter::try_from_str(Some(Type::TEXT_ARRAY), r#"{"a\\b","c"}"#).unwrap(); + let result = Converter::try_from_str(Some(Type::TEXT_ARRAY), r#"{"a\\b","c"}"#).unwrap(); assert_eq!(result, json!(["a\\b", "c"])); } diff --git a/backend/windmill-trigger-postgres/src/lib.rs b/backend/windmill-trigger-postgres/src/lib.rs index 011453739c..d6fcebfddb 100644 --- a/backend/windmill-trigger-postgres/src/lib.rs +++ b/backend/windmill-trigger-postgres/src/lib.rs @@ -1,8 +1,5 @@ use std::collections::HashMap; -use windmill_api_auth::ApiAuthed; -use windmill_store::resources::try_get_resource_from_db_as; -use windmill_trigger::trigger_helpers::TriggerJobArgs; use chrono::Utc; use itertools::Itertools; use native_tls::{Certificate, TlsConnector}; @@ -13,6 +10,8 @@ use rust_postgres_native_tls::MakeTlsConnector; use serde::{Deserialize, Deserializer, Serialize}; use serde_json::value::RawValue; use sqlx::FromRow; +use windmill_api_auth::ApiAuthed; +use windmill_common::workspaces::get_datatable_resource_from_db_unchecked; use windmill_common::{ db::UserDB, error::{to_anyhow, Error, Result}, @@ -20,6 +19,8 @@ use windmill_common::{ utils::empty_as_none, DB, }; +use windmill_store::resources::try_get_resource_from_db_as; +use windmill_trigger::trigger_helpers::TriggerJobArgs; mod bool; mod converter; @@ -373,6 +374,26 @@ pub async fn get_raw_postgres_connection( Ok(client) } +pub async fn resolve_postgres_resource( + authed: &ApiAuthed, + user_db: Option, + db: &DB, + postgres_resource_path: &str, + w_id: &str, +) -> Result { + if let Some(datatable_name) = postgres_resource_path.strip_prefix("datatable://") { + let resource_value = + get_datatable_resource_from_db_unchecked(db, w_id, datatable_name).await?; + serde_json::from_value::(resource_value).map_err(|e| Error::SerdeJson { + error: e, + location: "resolve_postgres_resource".to_string(), + }) + } else { + try_get_resource_from_db_as::(authed, user_db, db, postgres_resource_path, w_id) + .await + } +} + pub async fn get_pg_connection( authed: ApiAuthed, user_db: Option, @@ -382,8 +403,7 @@ pub async fn get_pg_connection( logical_mode: bool, ) -> Result { let database = - try_get_resource_from_db_as::(&authed, user_db, db, postgres_resource_path, w_id) - .await?; + resolve_postgres_resource(&authed, user_db, db, postgres_resource_path, w_id).await?; Ok(get_raw_postgres_connection(&database, logical_mode).await?) } diff --git a/backend/windmill-trigger-postgres/src/listener.rs b/backend/windmill-trigger-postgres/src/listener.rs index cc71cedd33..eda9cdb8aa 100644 --- a/backend/windmill-trigger-postgres/src/listener.rs +++ b/backend/windmill-trigger-postgres/src/listener.rs @@ -15,7 +15,6 @@ use windmill_common::{ DB, }; -use windmill_store::resources::try_get_resource_from_db_as; use windmill_trigger::{listener::ListeningTrigger, trigger_helpers::TriggerJobArgs, Listener}; use super::{ @@ -26,7 +25,8 @@ use super::{ LogicalReplicationMessage::{Begin, Commit, Delete, Insert, Relation, Type, Update}, PrimaryKeepAliveBody, ReplicationMessage, }, - Postgres, PostgresConfig, PostgresTrigger, ERROR_PUBLICATION_NAME_NOT_EXISTS, + resolve_postgres_resource, Postgres, PostgresConfig, PostgresTrigger, + ERROR_PUBLICATION_NAME_NOT_EXISTS, }; const ERROR_REPLICATION_SLOT_NOT_EXISTS: &str = r#"The replication slot associated with this trigger no longer exists. Recreate a new replication slot or select an existing one in the advanced tab, or delete and recreate a new trigger"#; @@ -147,7 +147,7 @@ impl Listener for PostgresTrigger { .authed(db, &Self::TRIGGER_KIND.to_string()) .await?; - let database = try_get_resource_from_db_as::( + let database = resolve_postgres_resource( &authed, Some(UserDB::new(db.clone())), &db, diff --git a/backend/windmill-trigger-postgres/src/mapper.rs b/backend/windmill-trigger-postgres/src/mapper.rs index abd40a2838..13b75fbb0e 100644 --- a/backend/windmill-trigger-postgres/src/mapper.rs +++ b/backend/windmill-trigger-postgres/src/mapper.rs @@ -180,7 +180,10 @@ mod tests { fn test_postgres_to_typescript_date_types() { assert_eq!(postgres_to_typescript_type(Some(Type::DATE)), "string"); assert_eq!(postgres_to_typescript_type(Some(Type::TIMESTAMP)), "string"); - assert_eq!(postgres_to_typescript_type(Some(Type::TIMESTAMPTZ)), "string"); + assert_eq!( + postgres_to_typescript_type(Some(Type::TIMESTAMPTZ)), + "string" + ); assert_eq!(postgres_to_typescript_type(Some(Type::UUID)), "string"); } diff --git a/backend/windmill-trigger-postgres/src/relation.rs b/backend/windmill-trigger-postgres/src/relation.rs index 0907c134cf..c9ca338766 100644 --- a/backend/windmill-trigger-postgres/src/relation.rs +++ b/backend/windmill-trigger-postgres/src/relation.rs @@ -80,10 +80,17 @@ mod tests { use rust_postgres::types::Type; use serde_json::json; - use super::super::replication_message::{Column, ReplicaIdentity, RelationBody}; + use super::super::replication_message::{Column, RelationBody, ReplicaIdentity}; fn make_relation(o_id: Oid, columns: Vec) -> RelationBody { - RelationBody::new(None, o_id, "public".to_string(), "test".to_string(), ReplicaIdentity::Default, columns) + RelationBody::new( + None, + o_id, + "public".to_string(), + "test".to_string(), + ReplicaIdentity::Default, + columns, + ) } fn text_col(name: &str, typ: Option) -> Column { @@ -93,10 +100,13 @@ mod tests { #[test] fn test_row_to_json_text_columns() { let mut converter = RelationConverter::new(); - converter.add_relation(make_relation(1, vec![ - text_col("id", Some(Type::INT4)), - text_col("name", Some(Type::TEXT)), - ])); + converter.add_relation(make_relation( + 1, + vec![ + text_col("id", Some(Type::INT4)), + text_col("name", Some(Type::TEXT)), + ], + )); let tuple = vec![ TupleData::Text(Bytes::from("42")), @@ -111,15 +121,15 @@ mod tests { #[test] fn test_row_to_json_with_null() { let mut converter = RelationConverter::new(); - converter.add_relation(make_relation(1, vec![ - text_col("id", Some(Type::INT4)), - text_col("email", Some(Type::TEXT)), - ])); + converter.add_relation(make_relation( + 1, + vec![ + text_col("id", Some(Type::INT4)), + text_col("email", Some(Type::TEXT)), + ], + )); - let tuple = vec![ - TupleData::Text(Bytes::from("1")), - TupleData::Null, - ]; + let tuple = vec![TupleData::Text(Bytes::from("1")), TupleData::Null]; let result = converter.row_to_json((1, tuple)).unwrap(); assert_eq!(result["id"], json!(1)); @@ -129,9 +139,7 @@ mod tests { #[test] fn test_row_to_json_with_unchanged_toast() { let mut converter = RelationConverter::new(); - converter.add_relation(make_relation(1, vec![ - text_col("data", Some(Type::TEXT)), - ])); + converter.add_relation(make_relation(1, vec![text_col("data", Some(Type::TEXT))])); let tuple = vec![TupleData::UnchangedToast]; let result = converter.row_to_json((1, tuple)).unwrap(); @@ -141,9 +149,7 @@ mod tests { #[test] fn test_row_to_json_binary_rejected() { let mut converter = RelationConverter::new(); - converter.add_relation(make_relation(1, vec![ - text_col("data", Some(Type::BYTEA)), - ])); + converter.add_relation(make_relation(1, vec![text_col("data", Some(Type::BYTEA))])); let tuple = vec![TupleData::Binary(Bytes::from("data"))]; assert!(matches!( @@ -168,8 +174,12 @@ mod tests { converter.add_relation(make_relation(1, vec![text_col("a", Some(Type::TEXT))])); converter.add_relation(make_relation(2, vec![text_col("b", Some(Type::INT4))])); - let result1 = converter.row_to_json((1, vec![TupleData::Text(Bytes::from("hello"))])).unwrap(); - let result2 = converter.row_to_json((2, vec![TupleData::Text(Bytes::from("42"))])).unwrap(); + let result1 = converter + .row_to_json((1, vec![TupleData::Text(Bytes::from("hello"))])) + .unwrap(); + let result2 = converter + .row_to_json((2, vec![TupleData::Text(Bytes::from("42"))])) + .unwrap(); assert_eq!(result1["a"], json!("hello")); assert_eq!(result2["b"], json!(42)); @@ -178,11 +188,11 @@ mod tests { #[test] fn test_row_to_json_bool_column() { let mut converter = RelationConverter::new(); - converter.add_relation(make_relation(1, vec![ - text_col("active", Some(Type::BOOL)), - ])); + converter.add_relation(make_relation(1, vec![text_col("active", Some(Type::BOOL))])); - let result = converter.row_to_json((1, vec![TupleData::Text(Bytes::from("t"))])).unwrap(); + let result = converter + .row_to_json((1, vec![TupleData::Text(Bytes::from("t"))])) + .unwrap(); assert_eq!(result["active"], json!(true)); } } diff --git a/backend/windmill-trigger-postgres/src/replication_message.rs b/backend/windmill-trigger-postgres/src/replication_message.rs index 43dba75889..3e0390b367 100644 --- a/backend/windmill-trigger-postgres/src/replication_message.rs +++ b/backend/windmill-trigger-postgres/src/replication_message.rs @@ -682,10 +682,7 @@ mod tests { #[test] fn test_parse_insert_with_null_tuple() { - let data = Bytes::from(build_insert_message( - 10, - &[(TUPLE_DATA_NULL_BYTE, &[])], - )); + let data = Bytes::from(build_insert_message(10, &[(TUPLE_DATA_NULL_BYTE, &[])])); let body = XLogDataBody::new(0, 0, 0, data); match body.parse(&settings(false)).unwrap() { LogicalReplicationMessage::Insert(insert) => { @@ -698,10 +695,7 @@ mod tests { #[test] fn test_parse_insert_with_toast_tuple() { - let data = Bytes::from(build_insert_message( - 10, - &[(TUPLE_DATA_TOAST_BYTE, &[])], - )); + let data = Bytes::from(build_insert_message(10, &[(TUPLE_DATA_TOAST_BYTE, &[])])); let body = XLogDataBody::new(0, 0, 0, data); match body.parse(&settings(false)).unwrap() { LogicalReplicationMessage::Insert(insert) => { @@ -759,6 +753,7 @@ mod tests { buf.extend_from_slice(b"users\0"); // name buf.push(REPLICA_IDENTITY_DEFAULT_BYTE as u8); // replica identity buf.extend_from_slice(&1i16.to_be_bytes()); // num columns + buf.extend_from_slice(&1i16.to_be_bytes()); // num columns // column: flags=0, name="id", type_oid=23 (INT4), type_modifier=-1 buf.push(0); // flags buf.extend_from_slice(b"id\0"); // name diff --git a/frontend/src/lib/components/ArgInput.svelte b/frontend/src/lib/components/ArgInput.svelte index 3fc1a772e3..197275fbc1 100644 --- a/frontend/src/lib/components/ArgInput.svelte +++ b/frontend/src/lib/components/ArgInput.svelte @@ -1057,6 +1057,7 @@ {:else if inputCat == 'resource-object' && (resourceTypes == undefined || (format && format?.split('-').length > 1 && resourceTypes.includes(format?.substring('resource-'.length))))} void } @@ -30,16 +31,23 @@ defaultValue, editor = $bindable(undefined), disabled = false, + datatableAsPgResource = false, onClear = undefined }: Props = $props() function isResource() { - return isString(value) && value.length >= '$res:'.length + return isString(value) && value.startsWith('$res:') + } + + function isDatatable(val: any): boolean { + return (isString(val) && val.startsWith('datatable://')) || val === 'datatable' } function valueToPath() { if (isResource()) { return value.substr('$res:'.length) + } else if (isDatatable(value)) { + return value } } @@ -50,6 +58,7 @@ {:else if value == undefined || typeof value === 'string'} - import { ResourceService } from '$lib/gen' + import { ResourceService, WorkspaceService } from '$lib/gen' import { globalDbManagerDrawer, workspaceStore } from '$lib/stores' import { onMount, untrack } from 'svelte' import AppConnect from './AppConnectDrawer.svelte' @@ -28,6 +28,7 @@ class?: string onClear?: () => void excludedValues?: string[] + datatableAsPgResource?: boolean } let { @@ -45,7 +46,8 @@ selectInputClass = '', class: className = '', onClear = undefined, - excludedValues = undefined + excludedValues = undefined, + datatableAsPgResource = false }: Props = $props() if (initialValue && value == undefined) { @@ -117,6 +119,23 @@ type: x.resource_type })) + if (datatableAsPgResource && resourceType === 'postgresql') { + try { + const datatables = await WorkspaceService.listDataTables({ + workspace: $workspaceStore! + }) + for (const dt of datatables) { + nc.push({ + value: `datatable://${dt}`, + label: `datatable://${dt}`, + type: 'postgresql' + }) + } + } catch (e) { + console.error('Failed to load data tables', e) + } + } + // TODO check if this is needed if (!nc.find((x) => x.value == value) && (initialValue || value)) { nc.push({ value: value ?? initialValue!, label: value ?? initialValue!, type: '' }) @@ -157,6 +176,7 @@ let resourceEditor: ResourceEditorDrawer | undefined = $state() let dbManagerDrawer = $derived(globalDbManagerDrawer.val) let hovering = $state(false) + let isDatatableSelected = $derived(value?.startsWith('datatable://') ?? false) {#snippet endSnippet({ item, close })} - + + {/snippet} + + {/each} + + {/if} + + + + (triggerToDelete = undefined)} + > + {#if triggerToDelete} + Are you sure you want to delete the {getTriggerDisplayName(triggerToDelete.triggerKind)} trigger + '{triggerToDelete.path}'? + {/if} + {:else}
No comparison data available
diff --git a/frontend/src/lib/components/common/table/RowIcon.svelte b/frontend/src/lib/components/common/table/RowIcon.svelte index 80566800ff..e5769b9816 100644 --- a/frontend/src/lib/components/common/table/RowIcon.svelte +++ b/frontend/src/lib/components/common/table/RowIcon.svelte @@ -13,6 +13,7 @@ DollarSign, Folder, LayoutDashboard, + Mail, Route, Unplug } from 'lucide-svelte' @@ -37,6 +38,7 @@ | 'mqtt' | 'sqs' | 'gcp' + | 'emails' /** For 'trigger' kind, specifies the specific trigger type (routes, schedules, etc.) */ export let triggerKind: string | undefined = undefined @@ -78,6 +80,8 @@ {:else if effectiveKind === 'gcp'} + {:else if effectiveKind === 'emails'} + {:else if effectiveKind === 'trigger'} {:else} From c7c828b56e7a5f877ef0a78498018ed930bccb23 Mon Sep 17 00:00:00 2001 From: hugocasa Date: Wed, 25 Feb 2026 18:28:06 +0100 Subject: [PATCH 05/18] feat: add resume and cancel button text options to Slack approval API + formatted args + typo (#8095) --- backend/windmill-api/openapi.yaml | 20 +++++++++++ backend/windmill-api/src/approvals.rs | 15 +++++++-- backend/windmill-api/src/slack_approvals.rs | 37 ++++++++++++++++++--- typescript-client/client.ts | 15 +++++++++ 4 files changed, 80 insertions(+), 7 deletions(-) diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index c958a4c03c..c18a1fbf11 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -10680,6 +10680,16 @@ paths: required: false schema: type: string + - name: resume_button_text + in: query + required: false + schema: + type: string + - name: cancel_button_text + in: query + required: false + schema: + type: string responses: "200": description: Interactive slack approval message sent successfully @@ -10726,6 +10736,16 @@ paths: required: false schema: type: string + - name: resume_button_text + in: query + required: false + schema: + type: string + - name: cancel_button_text + in: query + required: false + schema: + type: string responses: "200": description: Interactive slack approval message sent successfully diff --git a/backend/windmill-api/src/approvals.rs b/backend/windmill-api/src/approvals.rs index e2344adc4e..fe9adb602e 100644 --- a/backend/windmill-api/src/approvals.rs +++ b/backend/windmill-api/src/approvals.rs @@ -83,6 +83,12 @@ pub struct QueryDynamicEnumJson { pub dynamic_enums_json: Option, } +#[derive(Deserialize, Debug)] +pub struct QueryButtonText { + pub resume_button_text: Option, + pub cancel_button_text: Option, +} + #[derive(Debug)] pub struct ApprovalFormDetails { pub message_str: String, @@ -266,7 +272,12 @@ pub async fn get_approval_form_details( }) }); - let args_str = args.map_or("None".to_string(), |a| a.get().to_string()); + let args_str = args.map_or("None".to_string(), |a| { + serde_json::from_str::(a.get()) + .ok() + .and_then(|v| serde_json::to_string_pretty(&v).ok()) + .unwrap_or_else(|| a.get().to_string()) + }); let parent_job_id_str = parent_job_id.map_or("None".to_string(), |id| id.to_string()); let script_path_str = script_path.as_deref().unwrap_or("None"); @@ -282,7 +293,7 @@ pub async fn get_approval_form_details( {}: {created_by}\n\n\ {}: {created_at_formatted}\n\n\ {}: {script_path_str}\n\n\ - {}: {args_str}\n\n\ + {}:\n```\n{args_str}\n```\n\n\ {}: {parent_job_id_str}\n\n", bold_format.replace("{}", "Created by"), bold_format.replace("{}", "Created at"), diff --git a/backend/windmill-api/src/slack_approvals.rs b/backend/windmill-api/src/slack_approvals.rs index ed6368dada..34a79b4651 100644 --- a/backend/windmill-api/src/slack_approvals.rs +++ b/backend/windmill-api/src/slack_approvals.rs @@ -16,8 +16,8 @@ use crate::jobs::{QueryApprover, ResumeUrls}; use crate::{ approvals::{ extract_w_id_from_resume_url, handle_resume_action, ApprovalFormDetails, FieldType, - MessageFormat, QueryDefaultArgsJson, QueryDynamicEnumJson, QueryFlowStepId, QueryMessage, - ResumeFormField, ResumeSchema, + MessageFormat, QueryButtonText, QueryDefaultArgsJson, QueryDynamicEnumJson, + QueryFlowStepId, QueryMessage, ResumeFormField, ResumeSchema, }, auth::OptTokened, }; @@ -107,6 +107,8 @@ struct ModalActionValue { flow_step_id: Option, default_args_json: Option, dynamic_enums_json: Option, + resume_button_text: Option, + cancel_button_text: Option, } #[derive(Deserialize, Debug)] @@ -200,6 +202,8 @@ pub async fn slack_app_callback_handler( container, default_args_json.as_ref(), dynamic_enums_json.as_ref(), + parsed_value.resume_button_text.as_deref(), + parsed_value.cancel_button_text.as_deref(), ) .await .map_err(|e| Error::BadRequest(e.to_string()))?; @@ -229,6 +233,7 @@ pub async fn request_slack_approval( Query(flow_step_id): Query, Query(default_args_json): Query, Query(dynamic_enums_json): Query, + Query(button_text): Query, ) -> Result { let slack_resource_path = slack_resource_path.slack_resource_path; let channel_id = channel_id.channel_id; @@ -255,6 +260,8 @@ pub async fn request_slack_approval( flow_step_id.as_str(), default_args_json.default_args_json.as_ref(), dynamic_enums_json.dynamic_enums_json.as_ref(), + button_text.resume_button_text.as_deref(), + button_text.cancel_button_text.as_deref(), ) .await .map_err(|e| Error::BadRequest(e.to_string()))?; @@ -752,6 +759,8 @@ async fn send_slack_message( flow_step_id: &str, default_args_json: Option<&serde_json::Value>, dynamic_enums_json: Option<&serde_json::Value>, + resume_button_text: Option<&str>, + cancel_button_text: Option<&str>, ) -> Result> { let url = "https://slack.com/api/chat.postMessage"; @@ -779,6 +788,14 @@ async fn send_slack_message( value["dynamic_enums_json"] = dynamic_enums_json.clone(); } + if let Some(resume_button_text) = resume_button_text { + value["resume_button_text"] = serde_json::json!(resume_button_text); + } + + if let Some(cancel_button_text) = cancel_button_text { + value["cancel_button_text"] = serde_json::json!(cancel_button_text); + } + let payload = serde_json::json!({ "channel": channel_id, "text": "A flow has been suspended. Please approve or reject the flow.", @@ -842,6 +859,8 @@ async fn get_modal_blocks( container: Container, default_args_json: Option<&serde_json::Value>, dynamic_enums_json: Option<&serde_json::Value>, + resume_button_text: Option<&str>, + cancel_button_text: Option<&str>, ) -> Result, Error> { let approval_details = crate::approvals::get_approval_form_details( db, @@ -895,6 +914,8 @@ async fn get_modal_blocks( &urls.resume, resource_path, container, + resume_button_text, + cancel_button_text, ))) } @@ -905,6 +926,8 @@ fn construct_payload( resume_url: &str, resource_path: &str, container: Container, + resume_button_text: Option<&str>, + cancel_button_text: Option<&str>, ) -> serde_json::Value { let mut view = serde_json::json!({ "type": "modal", @@ -912,12 +935,12 @@ fn construct_payload( "notify_on_close": true, "title": { "type": "plain_text", - "text": "Worfklow Suspended" + "text": "Workflow Suspended" }, "blocks": blocks, "submit": { "type": "plain_text", - "text": "Resume Workflow" + "text": resume_button_text.unwrap_or("Resume Workflow") }, "private_metadata": serde_json::json!({ "resume_url": resume_url, "resource_path": resource_path, "container": container, "hide_cancel": hide_cancel }).to_string(), }); @@ -925,7 +948,7 @@ fn construct_payload( if !hide_cancel { view["close"] = serde_json::json!({ "type": "plain_text", - "text": "Cancel Workflow" + "text": cancel_button_text.unwrap_or("Cancel Workflow") }); } @@ -949,6 +972,8 @@ async fn open_modal_with_blocks( container: Container, default_args_json: Option<&serde_json::Value>, dynamic_enums_json: Option<&serde_json::Value>, + resume_button_text: Option<&str>, + cancel_button_text: Option<&str>, ) -> Result<(), Box> { let resume_id = rand::random::(); let blocks_json = match get_modal_blocks( @@ -964,6 +989,8 @@ async fn open_modal_with_blocks( container, default_args_json, dynamic_enums_json, + resume_button_text, + cancel_button_text, ) .await { diff --git a/typescript-client/client.ts b/typescript-client/client.ts index 60caf10a46..96abcdd4be 100644 --- a/typescript-client/client.ts +++ b/typescript-client/client.ts @@ -1169,6 +1169,8 @@ interface SlackApprovalOptions { approver?: string; defaultArgsJson?: Record; dynamicEnumsJson?: Record; + resumeButtonText?: string; + cancelButtonText?: string; } interface TeamsApprovalOptions { @@ -1208,6 +1210,8 @@ interface TeamsApprovalOptions { * approver: "approver123", * defaultArgsJson: { key1: "value1", key2: 42 }, * dynamicEnumsJson: { foo: ["choice1", "choice2"], bar: ["optionA", "optionB"] }, + * resumeButtonText: "Resume", + * cancelButtonText: "Cancel", * }); * ``` * @@ -1220,6 +1224,8 @@ export async function requestInteractiveSlackApproval({ approver, defaultArgsJson, dynamicEnumsJson, + resumeButtonText, + cancelButtonText, }: SlackApprovalOptions): Promise { const workspace = getWorkspace(); const flowJobId = getEnv("WM_FLOW_JOB_ID"); @@ -1244,6 +1250,8 @@ export async function requestInteractiveSlackApproval({ flowStepId: string; defaultArgsJson?: string; dynamicEnumsJson?: string; + resumeButtonText?: string; + cancelButtonText?: string; } = { slackResourcePath, channelId, @@ -1265,6 +1273,13 @@ export async function requestInteractiveSlackApproval({ params.dynamicEnumsJson = JSON.stringify(dynamicEnumsJson); } + if (resumeButtonText) { + params.resumeButtonText = resumeButtonText; + } + if (cancelButtonText) { + params.cancelButtonText = cancelButtonText; + } + await JobService.getSlackApprovalPayload({ workspace, ...params, From 88696ec29e318b24fca5fe53db7644ddde9f9053 Mon Sep 17 00:00:00 2001 From: centdix <40307056+centdix@users.noreply.github.com> Date: Wed, 25 Feb 2026 19:47:59 +0100 Subject: [PATCH 06/18] internal: wmdev config (#8096) * use gemini * linked repos --- .wmdev.yaml | 4 ++++ .workmux.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.wmdev.yaml b/.wmdev.yaml index 0707f107f6..376c0229c0 100644 --- a/.wmdev.yaml +++ b/.wmdev.yaml @@ -73,3 +73,7 @@ profiles: 3) Upload to asciinema.org: XDG_DATA_HOME=/tmp/.local/share \ asciinema upload --server-url https://asciinema.org /tmp/demo.cast + +linkedRepos: + - repo: windmill-labs/windmill-ee-private + alias: ee \ No newline at end of file diff --git a/.workmux.yaml b/.workmux.yaml index ec97aad3cf..c4adfdb991 100644 --- a/.workmux.yaml +++ b/.workmux.yaml @@ -11,7 +11,7 @@ worktree_prefix: "" window_prefix: "wm-" auto_name: - model: "claude-sonnet-4.6" + model: "gemini-2.5-flash-lite" system_prompt: | Generate a concise git branch name based on the task description. From 36d5a59ed52caf9d0f9dd966806ef95af6e6b6db Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Wed, 25 Feb 2026 22:51:47 +0100 Subject: [PATCH 07/18] chore: bump Svelte ecosystem to latest Vite 7-compatible versions (#8099) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * update: bump Svelte ecosystem to latest Vite 7-compatible versions Bump svelte (5.39→5.53), @sveltejs/kit (2.49→2.53), vite-plugin-svelte (6.2.1→6.2.4), svelte-check (4.3→4.4), @sveltejs/package (2.5.4→2.5.7). Stays on vite-plugin-svelte 6.x to avoid requiring Vite 8. Fix DucklakeSettings.svelte missing lang="ts" on instance script tag (new compiler rejects import type syntax in plain JS blocks). Fix getCurrentModel race condition where changeMode was called reactively before copilot info loaded, causing "No model selected" error on init. Co-Authored-By: Claude Opus 4.6 * fix: move early return guard before mode assignment in changeMode Avoid inconsistent state where this.mode is set to SCRIPT but systemMessage/tools/helpers are stale from the previous mode. Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Claude Opus 4.6 --- frontend/package-lock.json | 177 ++++++++---------- frontend/package.json | 10 +- frontend/src/lib/aiStore.ts | 173 ++++++++--------- .../copilot/chat/AIChatManager.svelte.ts | 5 +- .../workspaceSettings/DucklakeSettings.svelte | 2 +- 5 files changed, 180 insertions(+), 187 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 886cbdaa22..769d68610c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -102,9 +102,9 @@ "@melt-ui/svelte": "^0.86.2", "@playwright/test": "^1.57.0", "@sveltejs/adapter-static": "^3.0.6", - "@sveltejs/kit": "^2.49.2", - "@sveltejs/package": "^2.3.7", - "@sveltejs/vite-plugin-svelte": "^6.2.1", + "@sveltejs/kit": "^2.53.2", + "@sveltejs/package": "^2.5.7", + "@sveltejs/vite-plugin-svelte": "^6.2.4", "@tailwindcss/forms": "^0.5.3", "@tailwindcss/typography": "^0.5.8", "@types/d3": "^7.4.0", @@ -131,9 +131,9 @@ "prettier-plugin-svelte": "^3.3.3", "style-to-object": "^0.4.1", "stylelint-config-recommended": "^13.0.0", - "svelte": "^5.38.0", + "svelte": "^5.53.5", "svelte-awesome-color-picker": "^3.0.4", - "svelte-check": "^4.0.0", + "svelte-check": "^4.4.3", "svelte-fast-check": "^0.4.5", "svelte-floating-ui": "^1.5.8", "svelte-highlight": "^7.6.0", @@ -835,7 +835,6 @@ "version": "1.7.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz", "integrity": "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -847,7 +846,6 @@ "version": "1.7.1", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -858,7 +856,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1348,7 +1345,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.0.tgz", "integrity": "sha512-Fq6DJW+Bb5jaWE69/qOE0D1TUN9+6uWhCeZpdnSBk14pjLcCWR7Q8n49PTSPHazM37JqrsdpEthXy2xn6jWWiA==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1503,7 +1499,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1520,7 +1515,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1537,7 +1531,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1554,7 +1547,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1571,7 +1563,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1588,7 +1579,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1605,7 +1595,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1622,7 +1611,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1639,7 +1627,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1656,7 +1643,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1673,7 +1659,6 @@ "cpu": [ "wasm32" ], - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1690,7 +1675,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1707,7 +1691,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2102,9 +2085,9 @@ } }, "node_modules/@sveltejs/kit": { - "version": "2.49.2", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.49.2.tgz", - "integrity": "sha512-Vp3zX/qlwerQmHMP6x0Ry1oY7eKKRcOWGc2P59srOp4zcqyn+etJyQpELgOi4+ZSUgteX8Y387NuwruLgGXLUQ==", + "version": "2.53.2", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.53.2.tgz", + "integrity": "sha512-M+MqAvFve12T1HWws/2npP/s3hFtyjw3GB/OXW/8a1jZBk48qnvPJrtgE+VOMc3RnjUMxc4mv/vQ73nvj2uNMg==", "devOptional": true, "license": "MIT", "dependencies": { @@ -2113,13 +2096,12 @@ "@types/cookie": "^0.6.0", "acorn": "^8.14.1", "cookie": "^0.6.0", - "devalue": "^5.3.2", + "devalue": "^5.6.3", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", - "sade": "^1.8.1", - "set-cookie-parser": "^2.6.0", + "set-cookie-parser": "^3.0.0", "sirv": "^3.0.0" }, "bin": { @@ -2130,24 +2112,28 @@ }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", - "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0", + "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0", "svelte": "^4.0.0 || ^5.0.0-next.0", - "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0" + "typescript": "^5.3.3", + "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" }, "peerDependenciesMeta": { "@opentelemetry/api": { "optional": true + }, + "typescript": { + "optional": true } } }, "node_modules/@sveltejs/package": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/@sveltejs/package/-/package-2.5.4.tgz", - "integrity": "sha512-8+1hccAt0M3PPkHVPKH54Wc+cc1PNxRqCrICZiv/hEEto8KwbQVRghxNgTB4htIPyle+4CIB8RayTQH5zRQh9A==", + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/@sveltejs/package/-/package-2.5.7.tgz", + "integrity": "sha512-qqD9xa9H7TDiGFrF6rz7AirOR8k15qDK/9i4MIE8te4vWsv5GEogPks61rrZcLy+yWph+aI6pIj2MdoK3YI8AQ==", "dev": true, "license": "MIT", "dependencies": { - "chokidar": "^4.0.3", + "chokidar": "^5.0.0", "kleur": "^4.1.5", "sade": "^1.8.1", "semver": "^7.5.4", @@ -2163,17 +2149,47 @@ "svelte": "^3.44.0 || ^4.0.0 || ^5.0.0-next.1" } }, + "node_modules/@sveltejs/package/node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@sveltejs/package/node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@sveltejs/vite-plugin-svelte": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-6.2.1.tgz", - "integrity": "sha512-YZs/OSKOQAQCnJvM/P+F1URotNnYNeU3P2s4oIpzm1uFaqUEqRxUB0g5ejMjEb5Gjb9/PiBI5Ktrq4rUUF8UVQ==", + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-6.2.4.tgz", + "integrity": "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA==", "devOptional": true, "license": "MIT", "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", - "debug": "^4.4.1", "deepmerge": "^4.3.1", - "magic-string": "^0.30.17", + "magic-string": "^0.30.21", + "obug": "^2.1.0", "vitefu": "^1.1.1" }, "engines": { @@ -2313,7 +2329,6 @@ "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -2723,8 +2738,7 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/@types/unist": { "version": "3.0.3", @@ -3442,9 +3456,9 @@ "license": "Python-2.0" }, "node_modules/aria-query": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", - "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", + "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", "license": "Apache-2.0", "engines": { "node": ">= 0.4" @@ -4971,10 +4985,9 @@ } }, "node_modules/devalue": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.3.2.tgz", - "integrity": "sha512-UDsjUbpQn9kvm68slnrs+mfxwFkIflOhkanmyabZ8zOYk8SMEIbJ3TK+88g70hSIeytu4y18f0z/hYHMTrXIWw==", - "devOptional": true, + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.3.tgz", + "integrity": "sha512-nc7XjUU/2Lb+SvEFVGcWLiKkzfw8+qHI7zn8WYXKkLMgfGSHbgCEaR6bJpev8Cm6Rmrb19Gfd/tZvGqx9is3wg==", "license": "MIT" }, "node_modules/devlop": { @@ -5705,9 +5718,9 @@ } }, "node_modules/esrap": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.1.0.tgz", - "integrity": "sha512-yzmPNpl7TBbMRC5Lj2JlJZNPml0tzqoqP5B1JXycNUwtqma9AKCO0M2wHrdgsHcy1WRW7S9rJknAMtByg3usgA==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.3.tgz", + "integrity": "sha512-8fOS+GIGCQZl/ZIlhl59htOlms6U8NvX6ZYgYHpRU/b6tVSh3uHkOHZikl3D4cMbYM0JlpBe+p/BkZEi8J9XIQ==", "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" @@ -7193,7 +7206,7 @@ "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", - "dev": true, + "devOptional": true, "license": "MIT", "bin": { "jiti": "bin/jiti.js" @@ -7692,7 +7705,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7713,7 +7725,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7734,7 +7745,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7755,7 +7765,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7776,7 +7785,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7797,7 +7805,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7818,7 +7825,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7839,7 +7845,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7860,7 +7865,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7881,7 +7885,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7902,7 +7905,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -9234,7 +9236,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -9541,7 +9543,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", - "dev": true, + "devOptional": true, "funding": [ "https://github.com/sponsors/sxzz", "https://opencollective.com/debug" @@ -11566,7 +11568,7 @@ "version": "1.8.1", "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "mri": "^1.1.0" @@ -11615,9 +11617,9 @@ } }, "node_modules/set-cookie-parser": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", - "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.0.1.tgz", + "integrity": "sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q==", "devOptional": true, "license": "MIT" }, @@ -12430,21 +12432,23 @@ } }, "node_modules/svelte": { - "version": "5.39.12", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.39.12.tgz", - "integrity": "sha512-CEzwxFuEycokU8K8CE/OuwVbmei+ivu2HvBGYIdASfMa1hCRSNr4RRkzNSvbAvu6h+BOig2CsZTAEY+WKvwZpA==", + "version": "5.53.5", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.53.5.tgz", + "integrity": "sha512-YkqERnF05g8KLdDZwZrF8/i1eSbj6Eoat8Jjr2IfruZz9StLuBqo8sfCSzjosNKd+ZrQ8DkKZDjpO5y3ht1Pow==", "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", + "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", - "aria-query": "^5.3.1", + "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", + "devalue": "^5.6.3", "esm-env": "^1.2.1", - "esrap": "^2.1.0", + "esrap": "^2.2.2", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", @@ -12488,9 +12492,9 @@ } }, "node_modules/svelte-check": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.3.3.tgz", - "integrity": "sha512-RYP0bEwenDXzfv0P1sKAwjZSlaRyqBn0Fz1TVni58lqyEiqgwztTpmodJrGzP6ZT2aHl4MbTvWP6gbmQ3FOnBg==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.4.3.tgz", + "integrity": "sha512-4HtdEv2hOoLCEsSXI+RDELk9okP/4sImWa7X02OjMFFOWeSdFF3NFy3vqpw0z+eH9C88J9vxZfUXz/Uv2A1ANw==", "dev": true, "license": "MIT", "dependencies": { @@ -12529,21 +12533,6 @@ } } }, - "node_modules/svelte-check/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/svelte-eslint-parser": { "version": "0.43.0", "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.43.0.tgz", @@ -13274,7 +13263,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", diff --git a/frontend/package.json b/frontend/package.json index 61c6a994bd..b0922a809d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -27,9 +27,9 @@ "@melt-ui/svelte": "^0.86.2", "@playwright/test": "^1.57.0", "@sveltejs/adapter-static": "^3.0.6", - "@sveltejs/kit": "^2.49.2", - "@sveltejs/package": "^2.3.7", - "@sveltejs/vite-plugin-svelte": "^6.2.1", + "@sveltejs/kit": "^2.53.2", + "@sveltejs/package": "^2.5.7", + "@sveltejs/vite-plugin-svelte": "^6.2.4", "@tailwindcss/forms": "^0.5.3", "@tailwindcss/typography": "^0.5.8", "@types/d3": "^7.4.0", @@ -56,9 +56,9 @@ "prettier-plugin-svelte": "^3.3.3", "style-to-object": "^0.4.1", "stylelint-config-recommended": "^13.0.0", - "svelte": "^5.38.0", + "svelte": "^5.53.5", "svelte-awesome-color-picker": "^3.0.4", - "svelte-check": "^4.0.0", + "svelte-check": "^4.4.3", "svelte-fast-check": "^0.4.5", "svelte-floating-ui": "^1.5.8", "svelte-highlight": "^7.6.0", diff --git a/frontend/src/lib/aiStore.ts b/frontend/src/lib/aiStore.ts index 6b302d281f..8abc303e18 100644 --- a/frontend/src/lib/aiStore.ts +++ b/frontend/src/lib/aiStore.ts @@ -9,113 +9,116 @@ const USER_CUSTOM_PROMPTS_KEY = 'userCustomAIPrompts' const sessionModel = getLocalSetting(COPILOT_SESSION_MODEL_SETTING_NAME) const sessionProvider = getLocalSetting(COPILOT_SESSION_PROVIDER_SETTING_NAME) export const copilotSessionModel = writable( - sessionModel && sessionProvider - ? { - model: sessionModel, - provider: sessionProvider as AIProvider - } - : undefined + sessionModel && sessionProvider + ? { + model: sessionModel, + provider: sessionProvider as AIProvider + } + : undefined ) - export const copilotInfo = writable<{ - enabled: boolean - codeCompletionModel?: AIProviderModel - defaultModel?: AIProviderModel - aiModels: AIProviderModel[] - customPrompts?: Record - maxTokensPerModel?: Record + enabled: boolean + codeCompletionModel?: AIProviderModel + defaultModel?: AIProviderModel + aiModels: AIProviderModel[] + customPrompts?: Record + maxTokensPerModel?: Record }>({ - enabled: false, - codeCompletionModel: undefined, - defaultModel: undefined, - aiModels: [], - customPrompts: {}, - maxTokensPerModel: {} + enabled: false, + codeCompletionModel: undefined, + defaultModel: undefined, + aiModels: [], + customPrompts: {}, + maxTokensPerModel: {} }) export async function loadCopilot(workspace: string) { - workspaceAIClients.init(workspace) - try { - const info = await WorkspaceService.getCopilotInfo({ workspace }) - setCopilotInfo(info) - } catch (err) { - setCopilotInfo({}) - console.error('Could not get copilot info', err) - } + workspaceAIClients.init(workspace) + try { + const info = await WorkspaceService.getCopilotInfo({ workspace }) + setCopilotInfo(info) + } catch (err) { + setCopilotInfo({}) + console.error('Could not get copilot info', err) + } } export function setCopilotInfo(aiConfig: AIConfig) { - if (Object.keys(aiConfig.providers ?? {}).length > 0) { - const aiModels = Object.entries(aiConfig.providers ?? {}).flatMap( - ([provider, providerConfig]) => - providerConfig.models.map((m) => ({ model: m, provider: provider as AIProvider })) - ) + if (Object.keys(aiConfig.providers ?? {}).length > 0) { + const aiModels = Object.entries(aiConfig.providers ?? {}).flatMap( + ([provider, providerConfig]) => + providerConfig.models.map((m) => ({ model: m, provider: provider as AIProvider })) + ) - copilotSessionModel.update((model) => { - if ( - model && - !aiModels.some((m) => m.model === model.model && m.provider === model.provider) - ) { - return undefined - } - return model - }) + copilotSessionModel.update((model) => { + if ( + model && + !aiModels.some((m) => m.model === model.model && m.provider === model.provider) + ) { + return undefined + } + return model + }) - copilotInfo.set({ - enabled: true, - codeCompletionModel: aiConfig.code_completion_model, - defaultModel: aiConfig.default_model, - aiModels: aiModels, - customPrompts: aiConfig.custom_prompts ?? {}, - maxTokensPerModel: aiConfig.max_tokens_per_model ?? {} - }) - } else { - copilotSessionModel.set(undefined) + copilotInfo.set({ + enabled: true, + codeCompletionModel: aiConfig.code_completion_model, + defaultModel: aiConfig.default_model, + aiModels: aiModels, + customPrompts: aiConfig.custom_prompts ?? {}, + maxTokensPerModel: aiConfig.max_tokens_per_model ?? {} + }) + } else { + copilotSessionModel.set(undefined) - copilotInfo.set({ - enabled: false, - codeCompletionModel: undefined, - defaultModel: undefined, - aiModels: [], - customPrompts: {}, - maxTokensPerModel: {} - }) - } + copilotInfo.set({ + enabled: false, + codeCompletionModel: undefined, + defaultModel: undefined, + aiModels: [], + customPrompts: {}, + maxTokensPerModel: {} + }) + } } -export function getCurrentModel() { - const model = - get(copilotSessionModel) ?? get(copilotInfo).defaultModel ?? get(copilotInfo).aiModels[0] - if (!model) { - throw new Error('No model selected') - } - return model +export function getCurrentModel(): AIProviderModel { + const model = + get(copilotSessionModel) ?? get(copilotInfo).defaultModel ?? get(copilotInfo).aiModels[0] + if (!model) { + throw new Error('No model selected') + } + return model +} + +export function tryGetCurrentModel(): AIProviderModel | undefined { + return get(copilotSessionModel) ?? get(copilotInfo).defaultModel ?? get(copilotInfo).aiModels[0] } export function getUserCustomPrompts(): Record { - const stored = getLocalSetting(USER_CUSTOM_PROMPTS_KEY) - if (stored) { - try { - return JSON.parse(stored) - } catch (e) { - console.error('Failed to parse user custom prompts', e) - return {} - } - } - return {} + const stored = getLocalSetting(USER_CUSTOM_PROMPTS_KEY) + if (stored) { + try { + return JSON.parse(stored) + } catch (e) { + console.error('Failed to parse user custom prompts', e) + return {} + } + } + return {} } export function getCombinedCustomPrompt(mode: string): string | undefined { - const workspacePrompt = get(copilotInfo).customPrompts?.[mode] - const userPrompts = getUserCustomPrompts() - const userPrompt = userPrompts[mode] + const workspacePrompt = get(copilotInfo).customPrompts?.[mode] + const userPrompts = getUserCustomPrompts() + const userPrompt = userPrompts[mode] - const prompts = [workspacePrompt, userPrompt].filter((p) => p?.trim()) + const prompts = [workspacePrompt, userPrompt].filter((p) => p?.trim()) - if (prompts.length === 0) { - return undefined - } + if (prompts.length === 0) { + return undefined + } - return prompts.join('\n\n') + return prompts.join('\n\n') } diff --git a/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts b/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts index c2fa377ed1..9ba412fb4a 100644 --- a/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts +++ b/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts @@ -57,7 +57,7 @@ import { prepareApiSystemMessage, prepareApiUserMessage } from './api/core' import { getAnthropicCompletion, parseAnthropicCompletion } from './anthropic' import { getOpenAIResponsesCompletion, parseOpenAIResponsesCompletion } from './openai-responses' import type { ReviewChangesOpts } from './monaco-adapter' -import { getCurrentModel, getCombinedCustomPrompt } from '$lib/aiStore' +import { getCurrentModel, tryGetCurrentModel, getCombinedCustomPrompt } from '$lib/aiStore' // If the estimated token usage is greater than the model context window - the threshold, we delete the oldest message const MAX_TOKENS_THRESHOLD_PERCENTAGE = 0.05 @@ -229,11 +229,12 @@ class AIChatManager { closeScriptSettings?: boolean } ) { + if (mode === AIMode.SCRIPT && !tryGetCurrentModel()) return this.mode = mode this.pendingPrompt = pendingPrompt ?? '' if (mode === AIMode.SCRIPT) { - const customPrompt = getCombinedCustomPrompt(mode) const currentModel = getCurrentModel() + const customPrompt = getCombinedCustomPrompt(mode) const lang = this.scriptEditorOptions?.lang ?? 'bun' const context = this.contextManager.getSelectedContext() this.systemMessage = prepareScriptSystemMessage(currentModel, lang, {}, customPrompt) diff --git a/frontend/src/lib/components/workspaceSettings/DucklakeSettings.svelte b/frontend/src/lib/components/workspaceSettings/DucklakeSettings.svelte index 4a9137dfbd..ac8c20377d 100644 --- a/frontend/src/lib/components/workspaceSettings/DucklakeSettings.svelte +++ b/frontend/src/lib/components/workspaceSettings/DucklakeSettings.svelte @@ -51,7 +51,7 @@ } -