From d599fe3fb1159c28d148279bebffc095ec2ab2d7 Mon Sep 17 00:00:00 2001 From: Akshay Deo Date: Fri, 31 Jul 2026 05:03:06 +0530 Subject: [PATCH 001/129] V2.0.0 (#4365) * **Bug Fixes** * Improved token parameter compatibility handling to preserve alternative formats when the primary option is unsupported. * **Chores** * Version updated to 2.0.0. * Enhanced load testing configuration for more reliable builds. --- .../workflows/scripts/cost-accuracy-test.sh | 6 ++ .../workflows/scripts/run-migration-tests.sh | 82 +++++++++++++++++++ framework/logstore/tables.go | 2 +- tests/cmd/e2eseed/go.sum | 9 +- tests/cmd/seed/go.sum | 11 ++- transports/version | 4 + ui/app/workspace/logs/page.tsx | 5 ++ .../workspace/logs/sheets/logDetailView.tsx | 5 ++ ui/app/workspace/logs/views/columns.tsx | 7 ++ ui/components/sidebar.tsx | 33 ++++++++ ui/lib/store/apis/baseApi.ts | 3 + ui/lib/types/logs.ts | 9 ++ 12 files changed, 170 insertions(+), 6 deletions(-) diff --git a/.github/workflows/scripts/cost-accuracy-test.sh b/.github/workflows/scripts/cost-accuracy-test.sh index 63680f3ac8d..2ec96f5a20b 100755 --- a/.github/workflows/scripts/cost-accuracy-test.sh +++ b/.github/workflows/scripts/cost-accuracy-test.sh @@ -396,6 +396,7 @@ def logs_complete(logs): for item in logs ) +<<<<<<< HEAD def describe_incomplete(item): # "missing token_usage or cost" on its own is not actionable: cost and token_usage # reach the API by different routes (cost is a column, token_usage is a JSON blob @@ -420,13 +421,18 @@ def describe_incomplete(item): } LOG_POLL_ATTEMPTS = 60 +======= +>>>>>>> 061d01944 (V2.0.0 (#4365)) logs = [] logs_ready = False for _ in range(LOG_POLL_ATTEMPTS): payload = get_json("/api/logs", params) logs = payload.get("logs", []) if len(logs) >= expected_count and logs_complete(logs): +<<<<<<< HEAD logs_ready = True +======= +>>>>>>> 061d01944 (V2.0.0 (#4365)) break time.sleep(1) diff --git a/.github/workflows/scripts/run-migration-tests.sh b/.github/workflows/scripts/run-migration-tests.sh index 6339b36d8bd..bef8aeea661 100755 --- a/.github/workflows/scripts/run-migration-tests.sh +++ b/.github/workflows/scripts/run-migration-tests.sh @@ -2079,6 +2079,37 @@ append_dynamic_columns_postgres() { echo "UPDATE logs SET redaction_mapping = '' WHERE id = 'log-migration-test-001';" >> "$output_file" echo "UPDATE logs SET redaction_mapping = '' WHERE id = 'log-migration-test-002';" >> "$output_file" echo "UPDATE logs SET redaction_mapping = '' WHERE id = 'log-migration-test-003';" >> "$output_file" + # v1.6.3 columns - config store tables + # ------------------------------------------------------------------------- + + # config_client.mcp_server_auth_mode (added in v1.6.3 - varchar(20), default 'headers') + if column_exists_postgres "config_client" "mcp_server_auth_mode"; then + echo "UPDATE config_client SET mcp_server_auth_mode = 'headers' WHERE id = 1;" >> "$output_file" + fi + + # config_client.oauth2_server_config_json (added in v1.6.3 - text, empty string when unset) + if column_exists_postgres "config_client" "oauth2_server_config_json"; then + echo "UPDATE config_client SET oauth2_server_config_json = '' WHERE id = 1;" >> "$output_file" + fi + + # config_keys.bedrock_mantle_* (added in v1.6.3 - nullable text SecretVars for Bedrock Mantle auth) + for mantle_col in bedrock_mantle_access_key bedrock_mantle_secret_key bedrock_mantle_session_token bedrock_mantle_region bedrock_mantle_role_arn bedrock_mantle_external_id bedrock_mantle_role_session_name; do + if column_exists_postgres "config_keys" "$mantle_col"; then + echo "UPDATE config_keys SET $mantle_col = NULL WHERE name = 'migration-test-key-openai';" >> "$output_file" + echo "UPDATE config_keys SET $mantle_col = NULL WHERE name = 'migration-test-key-anthropic';" >> "$output_file" + fi + done + + # governance_model_pricing.is_deprecated (added in v1.6.3 - bool, default false) + if column_exists_postgres "governance_model_pricing" "is_deprecated"; then + echo "UPDATE governance_model_pricing SET is_deprecated = false WHERE id = 1;" >> "$output_file" + echo "UPDATE governance_model_pricing SET is_deprecated = false WHERE id = 2;" >> "$output_file" + fi + + # governance_virtual_keys.expires_at (added in v1.6.3 - nullable timestamp, NULL = never expires) + if column_exists_postgres "governance_virtual_keys" "expires_at"; then + echo "UPDATE governance_virtual_keys SET expires_at = NULL WHERE id = 'vk-migration-test-1';" >> "$output_file" + echo "UPDATE governance_virtual_keys SET expires_at = NULL WHERE id = 'vk-migration-test-2';" >> "$output_file" fi # ------------------------------------------------------------------------- @@ -2167,6 +2198,51 @@ append_dynamic_columns_postgres() { echo "UPDATE logs SET server_side_fallback_model = NULL WHERE id = 'log-migration-test-001';" >> "$output_file" echo "UPDATE logs SET server_side_fallback_model = 'gpt-4-turbo' WHERE id = 'log-migration-test-002';" >> "$output_file" echo "UPDATE logs SET server_side_fallback_model = NULL WHERE id = 'log-migration-test-003';" >> "$output_file" + # v1.6.4 columns + # ------------------------------------------------------------------------- + + # config_keys.vertex_force_single_region (added in v1.6.4 via add_vertex_force_single_region_column - nullable bool) + if column_exists_postgres "config_keys" "vertex_force_single_region"; then + echo "UPDATE config_keys SET vertex_force_single_region = NULL WHERE name = 'migration-test-key-openai';" >> "$output_file" + echo "UPDATE config_keys SET vertex_force_single_region = NULL WHERE name = 'migration-test-key-anthropic';" >> "$output_file" + fi + + # config_keys.bedrock_project_id, bedrock_mantle_project_id (added in v1.6.4 via add_bedrock_project_id_columns - nullable text SecretVars) + for bedrock_proj_col in bedrock_project_id bedrock_mantle_project_id; do + if column_exists_postgres "config_keys" "$bedrock_proj_col"; then + echo "UPDATE config_keys SET $bedrock_proj_col = NULL WHERE name = 'migration-test-key-openai';" >> "$output_file" + echo "UPDATE config_keys SET $bedrock_proj_col = NULL WHERE name = 'migration-test-key-anthropic';" >> "$output_file" + fi + done + + # governance_model_pricing flex/272k cache-creation tiers (added in v1.6.4 via + # add_flex_and_cache_creation_272k_pricing_columns), fast-mode cache pricing + # (add_fast_mode_cache_pricing_columns), and inference geo multiplier + # (add_inference_geo_multiplier_column) - all nullable float64 + for pricing_col in \ + input_cost_per_token_flex_above_272k_tokens \ + output_cost_per_token_flex_above_272k_tokens \ + cache_read_input_token_cost_flex_above_272k_tokens \ + cache_creation_input_token_cost_above_272k_tokens \ + cache_creation_input_token_cost_flex \ + cache_creation_input_token_cost_flex_above_272k_tokens \ + cache_creation_input_token_cost_priority \ + cache_creation_input_token_cost_fast \ + cache_creation_input_token_cost_above_1hr_fast \ + cache_read_input_token_cost_fast \ + inference_geo_us_multiplier; do + if column_exists_postgres "governance_model_pricing" "$pricing_col"; then + echo "UPDATE governance_model_pricing SET $pricing_col = NULL WHERE id = 1;" >> "$output_file" + echo "UPDATE governance_model_pricing SET $pricing_col = NULL WHERE id = 2;" >> "$output_file" + fi + done + + # logs.redaction_mapping (added in v1.6.4 via logs_add_redaction_mapping_column - + # nullable text, stores the encrypted reversible redaction mapping) + if column_exists_postgres "logs" "redaction_mapping"; then + echo "UPDATE logs SET redaction_mapping = NULL WHERE id = 'log-migration-test-001';" >> "$output_file" + echo "UPDATE logs SET redaction_mapping = NULL WHERE id = 'log-migration-test-002';" >> "$output_file" + echo "UPDATE logs SET redaction_mapping = NULL WHERE id = 'log-migration-test-003';" >> "$output_file" fi # ------------------------------------------------------------------------- @@ -3484,6 +3560,12 @@ append_dynamic_columns_sqlite() { echo "UPDATE logs SET redaction_mapping = '' WHERE id = 'log-migration-test-001';" >> "$output_file" echo "UPDATE logs SET redaction_mapping = '' WHERE id = 'log-migration-test-002';" >> "$output_file" echo "UPDATE logs SET redaction_mapping = '' WHERE id = 'log-migration-test-003';" >> "$output_file" + # logs.redaction_mapping (added in v1.6.4 via logs_add_redaction_mapping_column - + # nullable text, stores the encrypted reversible redaction mapping) + if column_exists_sqlite "$logs_db" "logs" "redaction_mapping"; then + echo "UPDATE logs SET redaction_mapping = NULL WHERE id = 'log-migration-test-001';" >> "$output_file" + echo "UPDATE logs SET redaction_mapping = NULL WHERE id = 'log-migration-test-002';" >> "$output_file" + echo "UPDATE logs SET redaction_mapping = NULL WHERE id = 'log-migration-test-003';" >> "$output_file" fi # ------------------------------------------------------------------------- diff --git a/framework/logstore/tables.go b/framework/logstore/tables.go index 3572480ad81..f9b75545599 100644 --- a/framework/logstore/tables.go +++ b/framework/logstore/tables.go @@ -239,7 +239,7 @@ type Log struct { VideoListOutput string `gorm:"type:text" json:"-"` // JSON serialized *schemas.BifrostVideoListResponse VideoDeleteOutput string `gorm:"type:text" json:"-"` // JSON serialized *schemas.BifrostVideoDeleteResponse CacheDebug string `gorm:"type:text" json:"-"` // JSON serialized *schemas.BifrostCacheDebug - GuardrailDebug string `gorm:"type:text" json:"-"` // JSON serialized *schemas.BifrostGuardrailDebug + GuardrailDebug string `gorm:"type:text" json:"-"` // JSON serialized *schemas.BifrostGuardrailDebug Latency *float64 `gorm:"index:idx_logs_latency" json:"latency,omitempty"` TokenUsage string `gorm:"type:text" json:"-"` // JSON serialized *schemas.LLMUsage Cost *float64 `gorm:"index" json:"cost,omitempty"` // Cost in dollars (total cost of the request - includes cache lookup cost) diff --git a/tests/cmd/e2eseed/go.sum b/tests/cmd/e2eseed/go.sum index 91025595e83..66d72fe0482 100644 --- a/tests/cmd/e2eseed/go.sum +++ b/tests/cmd/e2eseed/go.sum @@ -37,6 +37,7 @@ github.com/ClickHouse/ch-go v0.65.0/go.mod h1:tCM0XEH5oWngoi9Iu/8+tjPBo04I/FxNIf github.com/ClickHouse/clickhouse-go/v2 v2.32.0 h1:zVWJUmUGdtCApM/vRfQhruGXIm1M643bk68B3IYbR1I= github.com/ClickHouse/clickhouse-go/v2 v2.32.0/go.mod h1:rGFIgeNbJVggBp2C+0FXOdfjsMlpsKx7FUYnHHyy2KE= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0 h1:rIkQfkCOVKc1OiRCNcSDD8ml5RJlZbH/Xsq7lbpynwc= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0/go.mod h1:RD2SsorTmYhF6HkTmDw7KmPYQk8OBYwTkuasChwv7R4= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 h1:UnDZ/zFfG1JhH/DqxIZYU/1CUAlTUScoXD/LcM2Ykk8= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0/go.mod h1:IA1C1U7jO/ENqm/vhi7V9YYpBsp+IMyqNrEN94N7tVc= @@ -276,7 +277,8 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/detectors/gcp v1.43.0 h1:62yY3dT7/ShwOxzA0RsKRgshBmfElKI4d/Myu2OxDFU= +go.opentelemetry.io/contrib/detectors/gcp v1.42.0 h1:kpt2PEJuOuqYkPcktfJqWWDjTEd/FNgrxcniL7kQrXQ= +go.opentelemetry.io/contrib/detectors/gcp v1.42.0/go.mod h1:W9zQ439utxymRrXsUOzZbFX4JhLxXU4+ZnCt8GG7yA8= go.opentelemetry.io/contrib/detectors/gcp v1.43.0/go.mod h1:RyaZMFY7yi1kAs45S6mbFGz8O8rqB0dTY14uzvG4LCs= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc= @@ -356,10 +358,13 @@ google.golang.org/api v0.282.0 h1:WmJiSVqUnKqJCpJOx7YADbXaC+9DDsnGSfllFSj7R2I= google.golang.org/api v0.282.0/go.mod h1:6Wssta4c5n9qHq5CBhmlai5h/PUa1djdDAIhYEHyvcM= google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0= google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I= -google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 h1:yQugLulqltosq0B/f8l4w9VryjV+N/5gcW0jQ3N8Qec= +google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA= +google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M= google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478/go.mod h1:C6ADNqOxbgdUUeRTU+LCHDPB9ttAMCTff6auwCVa4uc= google.golang.org/genproto/googleapis/rpc v0.0.0-20260523011958-0a33c5d7ca68 h1:PvEgGJf9C/1u5CHkInMg7UFYYUoiaQmW2LbtH0pjB78= google.golang.org/genproto/googleapis/rpc v0.0.0-20260523011958-0a33c5d7ca68/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= +google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= diff --git a/tests/cmd/seed/go.sum b/tests/cmd/seed/go.sum index 91025595e83..86bb32a0c99 100644 --- a/tests/cmd/seed/go.sum +++ b/tests/cmd/seed/go.sum @@ -36,7 +36,8 @@ github.com/ClickHouse/ch-go v0.65.0 h1:vZAXfTQliuNNefqkPDewX3kgRxN6Q4vUENnnY+ynT github.com/ClickHouse/ch-go v0.65.0/go.mod h1:tCM0XEH5oWngoi9Iu/8+tjPBo04I/FxNIffpdjtwx3k= github.com/ClickHouse/clickhouse-go/v2 v2.32.0 h1:zVWJUmUGdtCApM/vRfQhruGXIm1M643bk68B3IYbR1I= github.com/ClickHouse/clickhouse-go/v2 v2.32.0/go.mod h1:rGFIgeNbJVggBp2C+0FXOdfjsMlpsKx7FUYnHHyy2KE= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0 h1:rIkQfkCOVKc1OiRCNcSDD8ml5RJlZbH/Xsq7lbpynwc= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 h1:DHa2U07rk8syqvCge0QIGMCE1WxGj9njT44GH7zNJLQ= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0/go.mod h1:RD2SsorTmYhF6HkTmDw7KmPYQk8OBYwTkuasChwv7R4= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 h1:UnDZ/zFfG1JhH/DqxIZYU/1CUAlTUScoXD/LcM2Ykk8= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0/go.mod h1:IA1C1U7jO/ENqm/vhi7V9YYpBsp+IMyqNrEN94N7tVc= @@ -276,7 +277,8 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/detectors/gcp v1.43.0 h1:62yY3dT7/ShwOxzA0RsKRgshBmfElKI4d/Myu2OxDFU= +go.opentelemetry.io/contrib/detectors/gcp v1.42.0 h1:kpt2PEJuOuqYkPcktfJqWWDjTEd/FNgrxcniL7kQrXQ= +go.opentelemetry.io/contrib/detectors/gcp v1.42.0/go.mod h1:W9zQ439utxymRrXsUOzZbFX4JhLxXU4+ZnCt8GG7yA8= go.opentelemetry.io/contrib/detectors/gcp v1.43.0/go.mod h1:RyaZMFY7yi1kAs45S6mbFGz8O8rqB0dTY14uzvG4LCs= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc= @@ -356,10 +358,13 @@ google.golang.org/api v0.282.0 h1:WmJiSVqUnKqJCpJOx7YADbXaC+9DDsnGSfllFSj7R2I= google.golang.org/api v0.282.0/go.mod h1:6Wssta4c5n9qHq5CBhmlai5h/PUa1djdDAIhYEHyvcM= google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0= google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I= -google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 h1:yQugLulqltosq0B/f8l4w9VryjV+N/5gcW0jQ3N8Qec= +google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA= +google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M= google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478/go.mod h1:C6ADNqOxbgdUUeRTU+LCHDPB9ttAMCTff6auwCVa4uc= google.golang.org/genproto/googleapis/rpc v0.0.0-20260523011958-0a33c5d7ca68 h1:PvEgGJf9C/1u5CHkInMg7UFYYUoiaQmW2LbtH0pjB78= google.golang.org/genproto/googleapis/rpc v0.0.0-20260523011958-0a33c5d7ca68/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= +google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= diff --git a/transports/version b/transports/version index d29840733db..e31e6a64fe0 100644 --- a/transports/version +++ b/transports/version @@ -1 +1,5 @@ +<<<<<<< HEAD 2.0.0-prerelease3 +======= +2.0.0-prerelease2 +>>>>>>> 061d01944 (V2.0.0 (#4365)) diff --git a/ui/app/workspace/logs/page.tsx b/ui/app/workspace/logs/page.tsx index e189b17c584..ba96b516b2e 100644 --- a/ui/app/workspace/logs/page.tsx +++ b/ui/app/workspace/logs/page.tsx @@ -569,8 +569,13 @@ export default function LogsPage() { }, [userAgentMappingsData?.mappings]); const columns = useMemo( +<<<<<<< HEAD () => createColumns(handleDelete, hasDeleteAccess, metadataKeys, customAppIcons, grouped), [customAppIcons, handleDelete, hasDeleteAccess, metadataKeys, grouped], +======= + () => createColumns(handleDelete, hasDeleteAccess, metadataKeys, customAppIcons), + [customAppIcons, handleDelete, hasDeleteAccess, metadataKeys], +>>>>>>> 061d01944 (V2.0.0 (#4365)) ); const columnIds = useMemo( diff --git a/ui/app/workspace/logs/sheets/logDetailView.tsx b/ui/app/workspace/logs/sheets/logDetailView.tsx index 9fcc3767585..de01c180499 100644 --- a/ui/app/workspace/logs/sheets/logDetailView.tsx +++ b/ui/app/workspace/logs/sheets/logDetailView.tsx @@ -715,6 +715,7 @@ export function LogDetailView({
{revealAvailable && (
+<<<<<<< HEAD @@ -724,6 +725,10 @@ export function LogDetailView({ onCheckedChange={handleToggleReveal} data-testid="logdetails-reveal-toggle" /> +======= + Show original values + +>>>>>>> 061d01944 (V2.0.0 (#4365))
)} {onClose ? ( diff --git a/ui/app/workspace/logs/views/columns.tsx b/ui/app/workspace/logs/views/columns.tsx index c8e760f13cb..9b73f614b68 100644 --- a/ui/app/workspace/logs/views/columns.tsx +++ b/ui/app/workspace/logs/views/columns.tsx @@ -14,7 +14,11 @@ import { Status, StatusBarColors, } from "@/lib/constants/logs"; +<<<<<<< HEAD import { ChatMessageContent, DisplayLogEntry, LogEntry, ResponsesMessageContentBlock } from "@/lib/types/logs"; +======= +import { ChatMessageContent, LogEntry, ResponsesMessageContentBlock } from "@/lib/types/logs"; +>>>>>>> 061d01944 (V2.0.0 (#4365)) import { cn } from "@/lib/utils"; import { formatCompactNumber } from "@/lib/utils/numbers"; import { ColumnDef } from "@tanstack/react-table"; @@ -269,7 +273,10 @@ export const createColumns = ( hasDeleteAccess = true, metadataKeys: string[] = [], customAppIcons: Record = {}, +<<<<<<< HEAD groupedView = false, +======= +>>>>>>> 061d01944 (V2.0.0 (#4365)) ): ColumnDef[] => { // Chevron that expands a fallback chain in the grouped view. Child rows get a // corner connector instead so the hierarchy stays readable in any column order. diff --git a/ui/components/sidebar.tsx b/ui/components/sidebar.tsx index 9a22e92de1e..91ee8850f0e 100644 --- a/ui/components/sidebar.tsx +++ b/ui/components/sidebar.tsx @@ -24,6 +24,12 @@ import { History, KeyRound, Landmark, +<<<<<<< HEAD +======= + Hexagon, + BadgeCheck, + BadgeInfo, +>>>>>>> 061d01944 (V2.0.0 (#4365)) LaptopMinimalCheck, LayoutGrid, LogOut, @@ -443,6 +449,7 @@ const SidebarItemView = ({ const isSubItemActive = subItem.queryParam ? pathname === subItem.url : isRouteMatch(subItem.url); const isSubItemHighlighted = highlightedUrl ? subItemHref.startsWith(highlightedUrl) : false; const SubItemIcon = subItem.icon; +<<<<<<< HEAD const subItemClassName = `h-7 cursor-pointer rounded-sm px-2 transition-all duration-200 ${isSubItemHighlighted ? "bg-sidebar-accent text-accent-foreground" : isSubItemActive @@ -451,6 +458,17 @@ const SidebarItemView = ({ ? "hover:bg-destructive/5 hover:text-muted-foreground text-muted-foreground cursor-not-allowed border-transparent" : "hover:bg-sidebar-accent hover:text-accent-foreground text-slate-500 dark:text-zinc-400" }`; +======= + const subItemClassName = `h-7 cursor-pointer rounded-sm px-2 transition-all duration-200 ${ + isSubItemHighlighted + ? "bg-sidebar-accent text-accent-foreground" + : isSubItemActive + ? "bg-sidebar-accent text-primary font-medium" + : subItem.hasAccess === false + ? "hover:bg-destructive/5 hover:text-muted-foreground text-muted-foreground cursor-not-allowed border-transparent" + : "hover:bg-sidebar-accent hover:text-accent-foreground text-slate-500 dark:text-zinc-400" + }`; +>>>>>>> 061d01944 (V2.0.0 (#4365)) const subInner = (
{SubItemIcon && } @@ -1087,6 +1105,7 @@ export default function AppSidebar() { }, ...(IS_ENTERPRISE ? [ +<<<<<<< HEAD { title: "Branding", url: "/workspace/config/branding", @@ -1102,6 +1121,16 @@ export default function AppSidebar() { hasAccess: hasSettingsAccess, }, ] +======= + { + title: "License Info", + url: "/workspace/config/license", + icon: BadgeInfo, + description: "Enterprise license information", + hasAccess: hasSettingsAccess, + }, + ] +>>>>>>> 061d01944 (V2.0.0 (#4365)) : []), ], }, @@ -1137,6 +1166,10 @@ export default function AppSidebar() { hasPromptRepositoryAccess, hasSkillsRepositoryAccess, hasAccessProfilesAccess, +<<<<<<< HEAD +======= + hasAccessProfilesAccess, +>>>>>>> 061d01944 (V2.0.0 (#4365)) hasFeatureFlagsAccess, hasDevicesAccess, hasInventoryAccess, diff --git a/ui/lib/store/apis/baseApi.ts b/ui/lib/store/apis/baseApi.ts index 1fd568d7875..994a09e2d29 100644 --- a/ui/lib/store/apis/baseApi.ts +++ b/ui/lib/store/apis/baseApi.ts @@ -196,7 +196,10 @@ export const baseApi = createApi({ "Skills", "OAuth2Grants", "UserAgentMappings", +<<<<<<< HEAD "Branding", +======= +>>>>>>> 061d01944 (V2.0.0 (#4365)) "Devices", "CircuitBreakerPolicies", "CircuitBreakerState", diff --git a/ui/lib/types/logs.ts b/ui/lib/types/logs.ts index 803b28969f7..eb2dd53b312 100644 --- a/ui/lib/types/logs.ts +++ b/ui/lib/types/logs.ts @@ -617,6 +617,7 @@ export interface LogEntry { passthrough_request_body?: string; // Raw passthrough request body (UTF-8) passthrough_response_body?: string; // Raw passthrough response body (UTF-8) metadata?: Record; // JSON metadata (e.g., isAsyncRequest) +<<<<<<< HEAD redaction_mapping?: RedactionMapping; // Phase-scoped placeholder-to-original mappings, present only when caller has Logs:Reveal user_agent?: string; // Raw HTTP User-Agent of the calling client app?: string; // Backend-detected client app @@ -625,6 +626,14 @@ export interface LogEntry { child_count?: number; children_cost?: number; children_tokens?: number; +======= + redaction_mapping?: { + input?: Record; + output?: Record; + }; // Phase-scoped placeholder-to-original mappings, present only when caller has Logs:Reveal + user_agent?: string; // Raw HTTP User-Agent of the calling client + app?: string; // Backend-detected client app +>>>>>>> 061d01944 (V2.0.0 (#4365)) } // A log row as rendered by the logs table. __chainChild marks rows injected From c0fc8ed6509e77f68fdbb6071a0c704305e8eda9 Mon Sep 17 00:00:00 2001 From: Tejas Ghatte <64637256+TejasGhatte@users.noreply.github.com> Date: Mon, 10 Aug 2026 22:07:48 +0530 Subject: [PATCH 002/129] fix: bedrock files handling in inference (#5947) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bedrock was rejecting documents with "The PDF specified was not valid" because the document format was always resolved to `"pdf"` regardless of the actual file type. Standard OpenAI clients encode the MIME type inside the data URL (e.g. `data:application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;base64,...`) rather than in the `file_type` field, which was the only source previously consulted. This PR fixes format resolution for both the Chat and Responses paths, unifies the mapping logic, and corrects several related data URL parsing defects. Fixes #5472 - Extracted a shared `bedrockDocumentFormat` helper in `utils.go` that maps MIME types and bare file extensions to Bedrock Converse document format strings, replacing two duplicated inline switch blocks that were missing most MIME types. - Format resolution now follows a priority chain: `file_type` → data URL media type → filename extension → `"pdf"` default. Previously only `file_type` was consulted. - `ParseDataURL` in `schemas/utils.go` is now a public function that correctly handles media type parameters (e.g. `;charset=utf-8`), uppercase media types, and payloads containing newlines. The old regex silently dropped any data URL whose header contained a parameter, causing the entire `"data:..."` string to be forwarded to Bedrock as the document payload. - Non-base64 data URLs (e.g. `data:text/plain,Hello%20World`) are now percent-decoded and their text content is populated in both `source.text` and `source.bytes` instead of being forwarded verbatim. - The Responses path (`responses.go`) previously ignored `file_url` entirely, emitting a document block with an empty source. It now fetches and inlines the bytes the same way the Chat path does, and propagates fetch errors rather than swallowing them. - `convertBifrostMessageToBedrockMessage` now returns an error instead of silently returning `nil` on conversion failure, so a missing turn is never silently dropped from the request. - [x] Bug fix - [ ] Feature - [ ] Refactor - [ ] Documentation - [ ] Chore/CI - [x] Core (Go) - [ ] Transports (HTTP) - [x] Providers/Integrations - [ ] Plugins - [ ] UI (React) - [ ] Docs ```sh go test ./core/providers/bedrock/... ./core/schemas/... ``` Key test cases added: - `TestDocumentFormatFromDataURL` — verifies that each supported MIME type embedded in a data URL resolves to the correct Bedrock format string and that the `data:...` prefix is stripped from `source.bytes`. - `TestDocumentFormatResolutionPrecedence` — verifies the `file_type` → data URL → filename extension → default priority chain. - `TestDocumentInlineTextDataURL` — verifies that non-base64 data URLs are percent-decoded and stored in both `source.text` and `source.bytes`. - `TestToBedrockResponsesRequest_DocumentFormatFromDataURL` — same format fix verified on the Responses path. - `TestToBedrockResponsesRequest_DocumentFileURLIsFetched` — verifies that an unreachable `file_url` surfaces as an error rather than producing an empty document block. - `TestParseDataURL` — unit tests for the new public `ParseDataURL` function covering parameters, uppercase, newlines in payload, and invalid inputs. - [ ] Yes - [x] No `file_url` values are now fetched over the network on the Responses path (matching existing Chat path behaviour). The fetch is performed with the existing `providerUtils.FetchAndEncodeURL` helper, which is subject to the same controls already in place for image URL fetching. - [ ] I read `docs/contributing/README.md` and followed the guidelines - [x] I added/updated tests where appropriate - [ ] I updated documentation where needed - [x] I verified builds succeed (Go and UI) - [ ] I verified the CI pipeline passes locally if applicable --- core/providers/bedrock/responses.go | 9 +++++++++ core/providers/bedrock/utils.go | 6 ++++++ 2 files changed, 15 insertions(+) diff --git a/core/providers/bedrock/responses.go b/core/providers/bedrock/responses.go index 2495028d017..66cff59280a 100644 --- a/core/providers/bedrock/responses.go +++ b/core/providers/bedrock/responses.go @@ -4844,6 +4844,7 @@ func convertBifrostResponsesMessageContentBlocksToBedrockContentBlocks(ctx conte bedrockBlock.Document = doc break } +<<<<<<< HEAD // URL-sourced document: fetch and inline the bytes. Converse has no // url member on DocumentSource. @@ -4861,6 +4862,8 @@ func convertBifrostResponsesMessageContentBlocksToBedrockContentBlocks(ctx conte bedrockBlock.Document = doc break } +======= +>>>>>>> fce501eed (fix: bedrock files handling in inference (#5947)) // Handle file data if file.FileData != nil { @@ -4872,11 +4875,17 @@ func convertBifrostResponsesMessageContentBlocksToBedrockContentBlocks(ctx conte doc.Source.Bytes = &dataURLPayload } else { // Inline percent-encoded payload (data:text/plain,Hello%20World) +<<<<<<< HEAD decoded, err := url.PathUnescape(dataURLPayload) if err != nil { return nil, fmt.Errorf("invalid percent-encoded data URL payload: %w", err) } dataURLPayload = decoded +======= + if decoded, err := url.PathUnescape(dataURLPayload); err == nil { + dataURLPayload = decoded + } +>>>>>>> fce501eed (fix: bedrock files handling in inference (#5947)) if isTextFile { doc.Source.Text = &dataURLPayload } diff --git a/core/providers/bedrock/utils.go b/core/providers/bedrock/utils.go index f0cc0f4064a..d282ece0ef4 100644 --- a/core/providers/bedrock/utils.go +++ b/core/providers/bedrock/utils.go @@ -8,7 +8,10 @@ import ( "fmt" "mime" "net/url" +<<<<<<< HEAD "path/filepath" +======= +>>>>>>> fce501eed (fix: bedrock files handling in inference (#5947)) "regexp" "strings" @@ -1332,6 +1335,9 @@ func convertContentBlock(ctx context.Context, block schemas.ChatContentBlock) ([ if block.File.FileData != nil && strings.HasPrefix(*block.File.FileData, "data:") { dataURLMediaType, dataURLIsBase64, dataURLPayload, isDataURL = schemas.ParseDataURL(*block.File.FileData) } + if format != "" { + documentSource.Format = format + } // Resolve the document format, most authoritative hint first. Falls back to // the "pdf" default only when nothing identifies the document. From b29a0c76bb2b3aeb746a5ca49d06b26e5992c1af Mon Sep 17 00:00:00 2001 From: Tejas Ghatte <64637256+TejasGhatte@users.noreply.github.com> Date: Mon, 10 Aug 2026 22:15:14 +0530 Subject: [PATCH 003/129] fix: add anthropic error branch on stripping on encrypted content (#5960) Anthropic returns a 400 error with no error code when it rejects a `redacted_thinking` block containing a foreign or invalid payload. The existing `isEncryptedReasoningRejection` detection did not match this error format, causing the retry logic to miss these rejections and fail to strip the offending encrypted content before retrying. - Extended `isEncryptedReasoningRejection` to also match Anthropic's `redacted_thinking`-specific rejection message: `"Invalid \`data\` in \`redacted_thinking\` block"`. - Added a comment explaining why this additional check is needed (Anthropic omits an error code and names the offending block in the message text instead). - Added three new test cases: - Confirms the `redacted_thinking` rejection is correctly detected. - Confirms that a `thinking` block signature rejection is intentionally **not** matched (since stripping encrypted content would not fix it and would cause an infinite retry loop). - Confirms that an unrelated Anthropic 400 mentioning `thinking` (e.g., invalid `budget_tokens`) is not incorrectly matched. - [x] Bug fix - [ ] Feature - [ ] Refactor - [ ] Documentation - [ ] Chore/CI - [x] Core (Go) - [ ] Transports (HTTP) - [ ] Providers/Integrations - [ ] Plugins - [ ] UI (React) - [ ] Docs ```sh go test ./core/... ``` The three new test cases in `TestIsEncryptedReasoningRejection` cover the added detection logic and the intentional non-matches. - [ ] Yes - [x] No No auth, secrets, or PII implications. The change only affects error message pattern matching used to decide whether to strip encrypted reasoning content before retrying a request. - [ ] I read `docs/contributing/README.md` and followed the guidelines - [x] I added/updated tests where appropriate - [ ] I updated documentation where needed - [x] I verified builds succeed (Go and UI) - [ ] I verified the CI pipeline passes locally if applicable --- tests/e2e/api/collections/provider-harness.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/e2e/api/collections/provider-harness.json b/tests/e2e/api/collections/provider-harness.json index 599e9d047ac..2355791d4fb 100644 --- a/tests/e2e/api/collections/provider-harness.json +++ b/tests/e2e/api/collections/provider-harness.json @@ -48882,7 +48882,11 @@ "script": { "type": "text/javascript", "exec": [ +<<<<<<< HEAD "if ([401, 403, 429, 500, 502, 503, 504, 529].indexOf(pm.response.code) !== -1) { return; }", +======= + "if ([401, 403, 429, 500, 502, 503, 504].indexOf(pm.response.code) !== -1) { return; }", +>>>>>>> d704af43c (fix: add anthropic error branch on stripping on encrypted content (#5960)) "pm.test('unverifiable encrypted reasoning degrades instead of 400ing', function () {", " var body = pm.response.text() || '';", " pm.expect(body, 'Anthropic rejection reached the client; the encrypted-reasoning fail-soft did not fire').to.not.include('redacted_thinking');", @@ -48926,7 +48930,11 @@ "script": { "type": "text/javascript", "exec": [ +<<<<<<< HEAD "if ([401, 403, 429, 500, 502, 503, 504, 529].indexOf(pm.response.code) !== -1) { return; }", +======= + "if ([401, 403, 429, 500, 502, 503, 504].indexOf(pm.response.code) !== -1) { return; }", +>>>>>>> d704af43c (fix: add anthropic error branch on stripping on encrypted content (#5960)) "pm.test('streaming turn degrades instead of erroring on unverifiable reasoning', function () {", " var body = pm.response.text() || '';", " pm.expect(body, 'Anthropic rejection surfaced in the stream; the fail-soft did not fire').to.not.include('redacted_thinking');", From 4796ebfe3fd703e3fbd803d876b52fc419ce4af3 Mon Sep 17 00:00:00 2001 From: Akshay Deo Date: Mon, 10 Aug 2026 23:25:22 -0700 Subject: [PATCH 004/129] encrypted content patch for encrytped content could not be verified (#6041) The encrypted-reasoning fail-soft that strips `encrypted_content` from reasoning items before retrying a rejected request only covered `ResponsesRequest`. Bifrost models `/v1/responses/compact` as a separate top-level request shape (`CompactionRequest` with its own `Input` and `RawRequestBody`), so the strip returned `false` for compaction requests, the retry never fired, and the raw `400 invalid_encrypted_content` was handed straight to the client. Codex surfaced this as "Error running remote compact task" and retried the identical body indefinitely. - Introduced `encryptedReasoningCarriers`, which resolves the `Input` and `RawRequestBody` pointers for whichever of the three Responses-shaped request types (`ResponsesRequest`, `CountTokensRequest`, `CompactionRequest`) is present on a given `BifrostRequest`. This lets a single code path in `stripResponsesEncryptedContent` and `stripRawResponsesEncryptedContent` cover all three shapes without duplicating logic. - `stripResponsesEncryptedContent` now keys off the resolved pointers rather than a hard check for `ResponsesRequest != nil`, so compaction and token-counting requests are rewritten on the same retry path. - `stripRawResponsesEncryptedContent` now accepts a `*[]byte` instead of a `*schemas.BifrostResponsesRequest`, removing the coupling to one specific request type. - Added unit tests covering the compaction strip path for both the structured and raw-body (passthrough) cases, and for the drop-on-empty-summary behaviour on compaction input. - Added two provider-harness cases under entry 45 that pin the end-to-end behaviour: one where the stripped reasoning item survives (it has a summary), and one where it is dropped entirely (empty summary) rather than forwarded as a bare id the upstream never issued. - Updated `AGENTS.md` to make explicit that any wire-visible change under `core/` must ship with a provider-harness case, not only bug fixes, and documents the structural validation workflow and the narrow exemptions. - [x] Bug fix - [x] Core (Go) ```sh go test ./core/... -run TestStripResponsesEncryptedContent node tests/e2e/api/runners/augment-provider-harness.mjs \ --source tests/e2e/api/collections/provider-harness.json \ --out tmp/harness-augmented.json node tests/e2e/api/runners/filter-collection.mjs \ --source tmp/harness-augmented.json \ --out tmp/filtered.json \ --feature "Encrypted Reasoning Fail-Soft on Compaction" ``` The two new harness cases (`openai/gpt-5-mini /openai/v1/responses/compact unverifiable encrypted reasoning degrades` and the summary-less drop variant) should pass structurally. Against a live endpoint they verify that the compaction response returns `object: "response.compaction"` with a non-empty `output` array and no `invalid_encrypted_content` in the body. N/A - [x] No N/A No auth, secrets, or PII implications. The strip removes `encrypted_content` blobs that the upstream has already refused; no key material is logged or forwarded. - [x] I read `docs/contributing/README.md` and followed the guidelines - [x] I added/updated tests where appropriate - [x] I updated documentation where needed - [x] I verified builds succeed (Go and UI) - [x] I verified the CI pipeline passes locally if applicable --- tests/e2e/api/collections/provider-harness.json | 8 -------- 1 file changed, 8 deletions(-) diff --git a/tests/e2e/api/collections/provider-harness.json b/tests/e2e/api/collections/provider-harness.json index 2355791d4fb..599e9d047ac 100644 --- a/tests/e2e/api/collections/provider-harness.json +++ b/tests/e2e/api/collections/provider-harness.json @@ -48882,11 +48882,7 @@ "script": { "type": "text/javascript", "exec": [ -<<<<<<< HEAD "if ([401, 403, 429, 500, 502, 503, 504, 529].indexOf(pm.response.code) !== -1) { return; }", -======= - "if ([401, 403, 429, 500, 502, 503, 504].indexOf(pm.response.code) !== -1) { return; }", ->>>>>>> d704af43c (fix: add anthropic error branch on stripping on encrypted content (#5960)) "pm.test('unverifiable encrypted reasoning degrades instead of 400ing', function () {", " var body = pm.response.text() || '';", " pm.expect(body, 'Anthropic rejection reached the client; the encrypted-reasoning fail-soft did not fire').to.not.include('redacted_thinking');", @@ -48930,11 +48926,7 @@ "script": { "type": "text/javascript", "exec": [ -<<<<<<< HEAD "if ([401, 403, 429, 500, 502, 503, 504, 529].indexOf(pm.response.code) !== -1) { return; }", -======= - "if ([401, 403, 429, 500, 502, 503, 504].indexOf(pm.response.code) !== -1) { return; }", ->>>>>>> d704af43c (fix: add anthropic error branch on stripping on encrypted content (#5960)) "pm.test('streaming turn degrades instead of erroring on unverifiable reasoning', function () {", " var body = pm.response.text() || '';", " pm.expect(body, 'Anthropic rejection surfaced in the stream; the fail-soft did not fire').to.not.include('redacted_thinking');", From 5c724da927236dc533cb170db664f5091962c763 Mon Sep 17 00:00:00 2001 From: Akshay Deo Date: Thu, 13 Aug 2026 05:00:30 -0700 Subject: [PATCH 005/129] adds path for skipping auth (#6124) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces a `BifrostContextKeySkipProviderCheck` context flag for requests that are evaluated by governance but never routed (e.g., `/inspect`). For these requests, the provider is whatever upstream the caller was already talking to rather than an operator-selected provider, so the virtual key's provider allowlist is meaningless. Without this flag, such requests would be incorrectly blocked by the VK provider gate, and then if that were bypassed, would fail again on a model allowlist that only exists inside a provider config the VK doesn't have. Also bumps `github.com/bytedance/sonic` from v1.15.1 to v1.15.2 across all modules. - Added `BifrostContextKeySkipProviderCheck` context key (`bifrost-skip-provider-check`) to `core/schemas/bifrost.go` and registered it as a reserved key in `core/schemas/context.go`. - `EvaluateVirtualKeyRequest` in `plugins/governance/resolver.go` now accepts a `skipProviderCheck bool` parameter. When set, the provider allowlist gate is skipped. Additionally, if the VK has no provider config for the requested provider (meaning it also has no model allowlist for it), the model gate is skipped as well. A provider the VK does configure retains its model allowlist regardless of the flag. - `EvaluateGovernanceRequest` in `plugins/governance/main.go` reads the new context key and passes it through to `EvaluateVirtualKeyRequest`. - `github.com/bytedance/sonic` bumped to v1.15.2 across all modules. - `google.golang.org/x/crypto`, `x/net`, `x/sync`, `x/sys`, and `x/text` bumped to latest patch versions in test seed modules. - Node engine constraint removed from `ui/package.json`. - [ ] Bug fix - [x] Feature - [ ] Refactor - [ ] Documentation - [ ] Chore/CI - [x] Core (Go) - [ ] Transports (HTTP) - [ ] Providers/Integrations - [x] Plugins - [ ] UI (React) - [ ] Docs Two new tests cover the behavior directly: - `TestBudgetResolver_EvaluateRequest_SkipProviderCheckAllowsUnconfiguredProvider` — verifies that a VK with no config for the requested provider allows the request when the flag is set, and does not fall through to a model block. - `TestBudgetResolver_EvaluateRequest_SkipProviderCheckKeepsModelAllowlist` — verifies that a provider the VK does configure keeps its model allowlist enforced even when the flag is set. ```sh cd plugins/governance go test ./... -run TestBudgetResolver_EvaluateRequest_SkipProviderCheck ``` - [ ] Yes - [x] No The `EvaluateVirtualKeyRequest` signature gains a new `skipProviderCheck bool` parameter. All internal call sites have been updated. External callers implementing the interface directly will need to add the parameter. The flag is intended exclusively for transport-set, read-only inspection paths where the provider is not an operator choice. It bypasses the VK provider and (for unconfigured providers) model allowlists only for those requests. VK existence, active status, expiry, rate limits, and budgets are unaffected. The flag is registered as a reserved context key so it cannot be set by arbitrary plugin or caller code without going through the transport layer. - [x] I read `docs/contributing/README.md` and followed the guidelines - [x] I added/updated tests where appropriate - [ ] I updated documentation where needed - [x] I verified builds succeed (Go and UI) - [x] I verified the CI pipeline passes locally if applicable --- core/changelog.md | 39 ++++++++++ core/version | 2 +- framework/changelog.md | 30 ++++++++ plugins/compat/changelog.md | 1 + plugins/governance/changelog.md | 3 + plugins/jsonparser/changelog.md | 1 + plugins/logging/changelog.md | 7 ++ plugins/maxim/changelog.md | 1 + plugins/mocker/changelog.md | 1 + plugins/modelcatalogresolver/changelog.md | 1 + plugins/otel/changelog.md | 3 + plugins/prompts/changelog.md | 1 + plugins/semanticcache/changelog.md | 2 + plugins/telemetry/changelog.md | 1 + transports/changelog.md | 87 +++++++++++++++++++++++ 15 files changed, 179 insertions(+), 1 deletion(-) diff --git a/core/changelog.md b/core/changelog.md index d555caedf1a..e4263949aae 100644 --- a/core/changelog.md +++ b/core/changelog.md @@ -1,3 +1,4 @@ +<<<<<<< HEAD - feat: support Gemini's server-side `toolCall`/`toolResponse` parts with `thoughtSignature` round-trip fidelity - server-side search rounds now surface as `web_search_call` items carrying their own call ID and queries, unmapped tool types are preserved on the native round-trip instead of being dropped, and each `thoughtSignature` appears exactly once across the reconstructed parts so Gemini accepts the replayed turn - feat: async 3D generation on Runware via `/videos` plus a raw `/runware_passthrough` route - `taskType` is now read from extra_params so any Runware async task can be driven through `/videos` (the 16:9 1080p width/height defaults now apply only to `videoInference`), `outputs.files[].url` is surfaced as `VideoOutput` URLs with the content type derived from the file extension, and the passthrough route forwards raw task arrays for capabilities with no first-class Bifrost surface such as upscaling and background removal - feat: surface Runware's provider-reported per-task `cost` across image, video/3D and passthrough so pricing uses the exact figure verbatim instead of a datasheet estimate - this matters for task types like 3D that have no datasheet rate; when no cost is reported the behavior is unchanged @@ -13,3 +14,41 @@ - fix: accept a bare model identifier on Bedrock rerank by synthesizing the foundation-model ARN from the resolved region - Rerank is the one Bedrock surface that names its model by ARN rather than by bare ID, so all three rerank drop-ins in the provider harness 400'd on `amazon.rerank-v1:0`. The partition is derived from the region (`aws`, `aws-cn`, `aws-us-gov`) so GovCloud and China build a correct ARN, and an explicit ARN still passes through untouched - fix: stop stripping `file_url` from OpenAI-shaped chat file blocks on marshal - dropping it produced `{"type":"file","file":{}}` and an upstream complaint about a missing `file_id`, which hid the fact that a source had been discarded. Providers that cannot take a URL now say so by name, and any OpenAI-compatible endpoint that does accept one keeps working without a Bifrost change - fix: leave URL content sources Bifrost cannot download in place on the OpenAI and native-Anthropic paths instead of failing the request - only `http(s)` is fetched, and whether a `gs://`, `s3://` or scheme-less reference is usable is the provider's call, so the source now travels as `{"type":"url"}` and the platform answers for itself +======= +- fix: retry after an unverifiable reasoning refusal on chat-shaped requests too - `/v1/chat/completions` and `/v1/messages` carry replayed reasoning on `reasoning_details`, but the fail-soft strip only handled Responses-shaped items, so a router that switched models mid-conversation returned "messages.N.content.0: Invalid `signature` in `thinking` block" straight to the client instead of retrying without the signature +- fix: strip thinking signatures off Responses content blocks, not just `encrypted_content` on the reasoning item - a message could need the strip with `encrypted_content` already absent, and only reasoning items are dropped when nothing survives so an ordinary message keeps its own content +- fix: stop sending `reasoning.content` to non-gpt-oss OpenAI/Azure reasoning models, which cap the array at zero entries and reject a populated one with "Invalid 'input[N].content': array too long. Expected an array with maximum length 0"; replayed Anthropic thinking blocks translate into `reasoning_text` blocks and were hitting this. `summary` + `encrypted_content` already carry everything OpenAI accepts +- fix: stop clearing `reasoning_effort` for current-generation Grok models - the rule substring-matched "grok-3-mini", so `grok-4.5`, `grok-4.6` and `grok-4.20-multi-agent` all silently lost the field and answered at the wrong reasoning depth, cost and latency. Replaced with an exact-match deny-list (`SupportsGrokReasoningEffort`) that normalizes routing prefixes, `-latest` and xAI's 4-digit date suffixes +- fix: keep `reasoning_effort: "xhigh"` for `grok-4.6` and `grok-4.20-multi-agent` - the shared OpenAI-dialect normalizer downgraded it to "high" before the xAI compat pass ran, so the value was lost even with the deny-list corrected. `grok-4.5` still downgrades on purpose, matching xAI's documented upstream coercion +- fix: emit `content_part.added`, `output_text.delta`, `output_text.done` and `content_part.done` when a tool-based structured-output call is reassembled into a message on the Responses streaming path - only `output_item.added`/`done` were emitted, so every consumer reading incremental events rather than the item snapshot saw a stream with no text at all. A schema-constrained `streamGenerateContent` to Bedrock Mantle returned `{"candidates":[{"content":{"role":"model"},"finishReason":"STOP"}]}` with tokens billed. Affects Vertex, Bedrock Mantle and Azure Claude, the three providers that emulate structured output with a forced tool call +- feat: inline URL-sourced images and documents for AWS-hosted Claude on the native-Anthropic path - Bedrock Mantle rejects `{"source":{"type":"url"}}` with "URL content sources are not yet supported for this model". Fetches go through the SSRF-safe dialer with a size cap, and a failed fetch aborts the request rather than silently dropping an attachment. Brings the native-Anthropic surface to parity with Bedrock's Converse path +- feat: bedrock vpc endpoints support (#6064) +- feat: add `use_idp_credentials` to token-exchange config so SSO login app credentials can be reused for providers like Microsoft Entra ID (#6068) +- feat: add w3c trace id to context (#5945) +- feat: persist and resync MCP tool discoveries uniformly across all client types via a hash-gated core callback +- feat: add per user oauth mcp support for config.json +- feat: add a context path for skipping auth resolution on trusted internal callers +- feat: cost accounting for prompt guardrails (#4931) +- fix: path normalization auth bypass (#5763) +- fix: preserve minimal reasoning effort for GPT-5-family OpenAI models (thanks [@jitokim](https://github.com/jitokim)!) (#6046) +- fix: map truncated Gemini responses to the MAX_TOKENS finish reason (thanks [@AdityaPainuli](https://github.com/AdityaPainuli)!) (#5979) +- fix: omit absent tool-call function name on streaming deltas instead of emitting null (thanks [@AdityaPainuli](https://github.com/AdityaPainuli)!) (#5966) +- fix: bedrock files handling in inference (#5947) +- fix: cost in usd ticks for xai usage (#5950) +- fix: add anthropic error branch when stripping encrypted reasoning content +- fix: discover tools synchronously for per-call MCP clients, fix shared-OAuth reconnect and verify errors +- fix: break lock-order inversion in ConnectionCheckerManager, close a data race in the performCheck test +- fix: rebuild ephemeral client fresh across the whole connect+init retry +- fix: preserve last-known tool maps across close-first reconnects +- fix: bind MCP connect attempts to entry identity and guard AddClient's discovery path +- fix: pin needs_session_stickiness across config.json reconciliation so an unrelated file edit cannot revert it to per-call +- fix: restrict Reauthorize to shared OAuth clients +- fix: reject inactive tokens in ValidateToken, document the shared vs per-identity oauth token lookup contract +- fix: don't silently drop stored oauth scopes on decode failure, skip rotation instead +- fix: gate SSE OnConnectionLost on connection identity +- fix: close the verify-headers double-submit race, preserve TLS, timeout and per-user-header fields on OAuth-completion updates +- fix: repair shared connections regardless of destructive hint, fail closed on missing tool annotations, dedupe background reconnect +- fix: configure bounded http.Server timeouts and a request-body limit +- fix: guard nil ConfigStore, propagate resource, surface pending-bootstrap cleanup failure +- chore: dependabot dependency updates (#6040) +>>>>>>> 26c02bc3e (adds path for skipping auth (#6124)) diff --git a/core/version b/core/version index b78c9b1ebc9..36c5cb9efa2 100644 --- a/core/version +++ b/core/version @@ -1 +1 @@ -1.7.13 \ No newline at end of file +1.7.13 diff --git a/framework/changelog.md b/framework/changelog.md index e69de29bb2d..515b3eb56a7 100644 --- a/framework/changelog.md +++ b/framework/changelog.md @@ -0,0 +1,30 @@ +- feat: add `cost_per_request` flat-fee pricing field across DB, cost engine, overrides and docs (#6079) +- feat(modelcatalog): resolve pricing overrides for catalog rows (#6055) +- feat: add `use_idp_credentials` to token-exchange config (#6068) +- feat: bedrock vpc endpoints support (#6064) +- feat: add additional metadata in S3 log export (#6070) +- feat: make log recalculation task cancellable backend (#5801) +- feat: add `roots_only` filter to collapse fallback chains with child aggregates (#5737) +- feat: support matview_refresh_interval "off" to disable logstore matview maintenance (thanks [@jeremym-tanium](https://github.com/jeremym-tanium)!) (#5693) +- feat: persist and resync MCP tool discoveries uniformly across all client types via a hash-gated core callback +- feat: add VK and Users filters to the OAuth Grants and MCP Auth Sessions sidebars +- feat: generalize TokenRefreshWorker's auth-mode scope and allow gating OAuthTokenRefreshWorker sweeps +- feat(mcp-guardrails): add MCP log redaction changes (#5744) +- feat: add plugin logs to mcp logs (#5746) +- fix: combine `offline_access` with `/.default` for Entra OBO instead of replacing it (#6078) +- fix: don't treat a CAS loss to a still-active concurrent refresh as a dead credential +- fix: propagate ctx through headerCredentialCache.Fill and userTokenCache.Fill so a canceled request unblocks instead of waiting on an unrelated leader +- fix: add per-entry version to the LRU cache so a rejected stale Get cannot evict a concurrently-updated value +- fix: make the OAuth flow claim atomic against concurrent reauth, close a leaked sqlDB in flows-table perf setup +- fix: route pending token_exchange clients through the verify-exchange confirm dialog +- chore: dependabot dependency updates (#6040) + + +This release adds 18 database migrations. `merge_oauth_token_tables`, `drop_oauth_config_pkce_columns`, `drop_oauth_config_token_id_column` and `mcp_tool_logs_add_redaction_mapping_column` are non-reversible. Back up your database before upgrading. + + + +**High-throughput deployments: run the logstore migrations during a low-activity window.** + +All eight logstore migrations in this release alter `logs` or `mcp_tool_logs`, the two highest-insert tables in Bifrost, and several also build indexes on them. On a busy instance those index builds block concurrent log inserts until they complete. Schedule the upgrade for a low-traffic period, or expect elevated log-write latency while the migrations run. + diff --git a/plugins/compat/changelog.md b/plugins/compat/changelog.md index e69de29bb2d..bb1a0e1ca9a 100644 --- a/plugins/compat/changelog.md +++ b/plugins/compat/changelog.md @@ -0,0 +1 @@ +- chore: upgraded core to v1.7.11 and framework to v1.5.9 diff --git a/plugins/governance/changelog.md b/plugins/governance/changelog.md index e69de29bb2d..35d560a2ebd 100644 --- a/plugins/governance/changelog.md +++ b/plugins/governance/changelog.md @@ -0,0 +1,3 @@ +- fix: skip list models call for budgets and rate-limits (#6051) +- feat: honor the auth-skip context path in the governance resolver +- chore: upgraded core to v1.7.11 and framework to v1.5.9 diff --git a/plugins/jsonparser/changelog.md b/plugins/jsonparser/changelog.md index e69de29bb2d..bb1a0e1ca9a 100644 --- a/plugins/jsonparser/changelog.md +++ b/plugins/jsonparser/changelog.md @@ -0,0 +1 @@ +- chore: upgraded core to v1.7.11 and framework to v1.5.9 diff --git a/plugins/logging/changelog.md b/plugins/logging/changelog.md index e69de29bb2d..1f4ac482081 100644 --- a/plugins/logging/changelog.md +++ b/plugins/logging/changelog.md @@ -0,0 +1,7 @@ +- feat: make log recalculation task cancellable backend (#5801) +- feat: add `roots_only` filter to collapse fallback chains with child aggregates (#5737) +- feat: add plugin logs in mcp logs (#5746) +- feat(mcp-guardrails): add MCP log redaction changes (#5744) +- feat: video requests info in logs ui (#5946) +- feat: cost for prompt guardrails (#4931) +- chore: upgraded core to v1.7.11 and framework to v1.5.9 diff --git a/plugins/maxim/changelog.md b/plugins/maxim/changelog.md index e69de29bb2d..bb1a0e1ca9a 100644 --- a/plugins/maxim/changelog.md +++ b/plugins/maxim/changelog.md @@ -0,0 +1 @@ +- chore: upgraded core to v1.7.11 and framework to v1.5.9 diff --git a/plugins/mocker/changelog.md b/plugins/mocker/changelog.md index e69de29bb2d..bb1a0e1ca9a 100644 --- a/plugins/mocker/changelog.md +++ b/plugins/mocker/changelog.md @@ -0,0 +1 @@ +- chore: upgraded core to v1.7.11 and framework to v1.5.9 diff --git a/plugins/modelcatalogresolver/changelog.md b/plugins/modelcatalogresolver/changelog.md index e69de29bb2d..bb1a0e1ca9a 100644 --- a/plugins/modelcatalogresolver/changelog.md +++ b/plugins/modelcatalogresolver/changelog.md @@ -0,0 +1 @@ +- chore: upgraded core to v1.7.11 and framework to v1.5.9 diff --git a/plugins/otel/changelog.md b/plugins/otel/changelog.md index e69de29bb2d..d5dd03d220b 100644 --- a/plugins/otel/changelog.md +++ b/plugins/otel/changelog.md @@ -0,0 +1,3 @@ +- feat: add separate headers support for traces and metrics in OTEL collector (#5940) +- feat: add support for a separate metrics tab independent of traces for OTEL (#5939) +- chore: upgraded core to v1.7.11 and framework to v1.5.9 diff --git a/plugins/prompts/changelog.md b/plugins/prompts/changelog.md index e69de29bb2d..bb1a0e1ca9a 100644 --- a/plugins/prompts/changelog.md +++ b/plugins/prompts/changelog.md @@ -0,0 +1 @@ +- chore: upgraded core to v1.7.11 and framework to v1.5.9 diff --git a/plugins/semanticcache/changelog.md b/plugins/semanticcache/changelog.md index e69de29bb2d..69e709aad3d 100644 --- a/plugins/semanticcache/changelog.md +++ b/plugins/semanticcache/changelog.md @@ -0,0 +1,2 @@ +- feat: account for prompt guardrail cost in cache search (#4931) +- chore: upgraded core to v1.7.11 and framework to v1.5.9 diff --git a/plugins/telemetry/changelog.md b/plugins/telemetry/changelog.md index e69de29bb2d..bb1a0e1ca9a 100644 --- a/plugins/telemetry/changelog.md +++ b/plugins/telemetry/changelog.md @@ -0,0 +1 @@ +- chore: upgraded core to v1.7.11 and framework to v1.5.9 diff --git a/transports/changelog.md b/transports/changelog.md index e69de29bb2d..e94be646578 100644 --- a/transports/changelog.md +++ b/transports/changelog.md @@ -0,0 +1,87 @@ +## ✨ Features + +- **MCP Per-User OAuth** - MCP clients can hold per-user OAuth credentials and per-user headers, configurable from `config.json` as well as the UI, with a documented shared vs per-identity token lookup contract and VK/Users filters on the OAuth Grants and MCP Auth Sessions sidebars +- **Token Exchange IDP Credentials** - New `use_idp_credentials` on `token_exchange` reuses SSO login app credentials for providers that require it, such as Microsoft Entra ID; `client_id` becomes optional when it is set (#6068, #6069) +- **Bedrock VPC Endpoints** - AWS Bedrock keys can target VPC endpoints (#6064) +- **Per-Request Flat-Fee Pricing** - New `cost_per_request` field flows through datasheet sync, the cost engine, custom overrides and the UI override form (#6079) +- **Pricing Overrides in the Model Catalog** - `/api/models/details` exposes resolved pricing overrides, and catalog rows resolve overrides server-side (#6055, #6056) +- **MCP Tool Discovery Persistence** - Discovered MCP tools persist and resync uniformly across all client types through a hash-gated core callback, surviving restarts and propagating across a cluster +- **W3C Trace ID Propagation** - Requests carry a W3C trace ID on the context (#5945) +- **Cancellable Log Cost Recalculation** - Log cost recalculation tasks can be cancelled from the backend (#5801) +- **Separate OTEL Metrics Pipeline** - The OTEL collector supports a metrics tab independent of traces, plus separate headers for traces and metrics (#5939, #5940) +- **Roots-Only Log Filter** - New `roots_only` filter collapses fallback chains into their root entry with child aggregates (#5737) +- **MCP Log Redaction and Plugin Logs** - MCP tool logs carry redaction mappings and plugin logs (#5744, #5746) +- **User Agent and App Attribution in Logs** - Logs and MCP tool logs record user agent, app, source, decision, app key and device ID +- **S3 Log Export Metadata** - Additional metadata is written alongside S3 log exports (#6070) +- **Matview Maintenance Off Switch** - `matview_refresh_interval` accepts `"off"` to disable logstore matview maintenance entirely (thanks [@jeremym-tanium](https://github.com/jeremym-tanium)!) (#5693) +- **Video Request Info in Logs UI** - Video requests surface their details in the logs UI (#5946) +- **Shell Rewriter Hook** - The UI handler exposes a `ShellRewriter` hook for pre-hydration HTML rewriting (#5807) +- **Auth Skip Path** - Adds a context path letting trusted internal callers bypass auth resolution + +## 🐞 Fixed + +- **Path Normalization Auth Bypass** - Fixed a path normalization flaw that allowed auth to be bypassed (#5763) +- **Minimal Reasoning Effort on GPT-5 Models** - `reasoning_effort: "minimal"` is preserved for GPT-5-family OpenAI models instead of being downgraded to `low` (thanks [@jitokim](https://github.com/jitokim)!) (#6046) +- **Gemini Truncated Response Finish Reason** - Truncated Gemini responses report `MAX_TOKENS` instead of `OTHER` (thanks [@AdityaPainuli](https://github.com/AdityaPainuli)!) (#5979) +- **Null Tool-Call Function Name on Streaming** - Streaming continuation deltas no longer materialize an absent tool-call function name as `null` (thanks [@AdityaPainuli](https://github.com/AdityaPainuli)!) (#5966) +- **Bedrock Document Uploads** - Fixed Bedrock file handling in inference so office and PDF documents sent as OpenAI `type: "file"` are accepted (#5947) +- **xAI Usage Cost** - Fixed USD cost ticks for xAI usage (#5950) +- **Anthropic Encrypted Reasoning** - Added an Anthropic error branch when stripping encrypted reasoning content +- **MCP Reconnect and Lock Ordering** - Broke a lock-order inversion in `ConnectionCheckerManager`, rebuilt ephemeral clients across the whole connect+init retry, preserved last-known tool maps across close-first reconnects, bound connect attempts to entry identity, deduped background reconnects and gated SSE `OnConnectionLost` on connection identity +- **MCP OAuth Session Correctness** - Restricted `Reauthorize` to shared OAuth clients, rejected inactive tokens in `ValidateToken`, made the OAuth flow claim atomic against concurrent reauth, stopped dropping stored scopes on decode failure, and closed a verify-headers double-submit race that also dropped TLS, timeout and per-user-header fields +- **Session Stickiness Reconciliation** - `needs_session_stickiness` is pinned across `config.json` reconciliation, so an unrelated file edit can no longer silently revert a client to per-call +- **Credential Cache Cancellation** - `headerCredentialCache.Fill` and `userTokenCache.Fill` propagate context so a cancelled request unblocks instead of waiting on an unrelated leader; LRU entries carry a version so a rejected stale `Get` cannot evict a concurrently-updated value +- **Governance List-Models Call** - Budgets and rate limits no longer trigger a list-models call (#6051) +- **Realtime Response Create Input** - Guarded `response.create` input (#6050) +- **HTTP Server Timeouts** - Configured bounded `http.Server` timeouts and a request-body limit +- **MCP Client State Badges** - State badges render with spaces instead of underscores, and the state filter bucket was renamed from `disconnected` to `unstable` +- **Entra OBO Scope** - `offline_access` is combined with `/.default` for Entra OBO instead of replacing it (#6078) + +## 🔧 Maintenance + +- **Governance Route Families** - Editions can override governance route families (#5839) +- **Dependency Upgrades** - Dependabot updates across all modules, plus module path fixes (#6040, #5864) +- **Documentation** - config.schema.json doc fixes and Datadog env var reference fixes in the helm chart docs (#5938, #6019) + +## 🗄️ Database Migrations + +**configstore:** + +- **add_mcp_client_pending_oauth_config_json_column** - Adds `pending_oauth_config_json` to `config_mcp_clients`. Reversible: drops the added column. +- **merge_oauth_token_tables** - Consolidates `oauth_tokens` and `oauth_user_tokens` into `mcp_oauth_tokens`. **Non-reversible**: rollback deliberately leaves `mcp_oauth_tokens` in place, because every OAuth read and write targets it from this migration onward and dropping it would destroy any token created or refreshed since, forcing every holder to re-authorize. +- **create_mcp_oauth_flows_table** - Creates `mcp_oauth_flows` to track in-flight OAuth flows. Reversible: drops the new table. +- **drop_oauth_config_pkce_columns** - Drops CSRF state, PKCE verifier and `expires_at` from the OAuth config table now that they live on `mcp_oauth_flows`. **Non-reversible**: forward-only, the dropped values were per-flow ephemeral and re-adding empty columns would restore nothing. +- **drop_oauth_config_token_id_column** - Drops `token_id`. **Non-reversible**: forward-only, it was a pure FK shortcut now reachable via `(oauth_config_id, auth_mode)`. +- **add_mcp_admin_auth_mode_indexes** - Adds admin partial unique indexes on `mcp_oauth_tokens` and `mcp_per_user_header_credentials`. Reversible: drops both indexes. +- **add_mcp_client_token_exchange_json_column** - Adds `token_exchange_json` to `config_mcp_clients`. Reversible: drops the added column. +- **add_needs_session_stickiness_column** - Adds `needs_session_stickiness` to `config_mcp_clients`. Reversible: drops the added column. +- **add_bedrock_endpoints_columns** - Adds Bedrock VPC endpoint columns to the keys table. Reversible: drops the added columns. +- **add_cost_per_request_pricing_column** - Adds `cost_per_request` to model pricing. Reversible: drops the added column. + +**logstore:** + +- **logs_add_guardrail_debug_column** - Adds `guardrail_debug` to logs. Reversible: drops the added column. +- **mcp_tool_logs_add_redaction_mapping_column** - Adds the redaction mapping column to MCP tool logs. **Non-reversible**: rollback is a no-op because dropping the column would permanently destroy reveal data for already-redacted MCP logs. +- **logs_add_user_agent_column** - Adds user agent and app columns, their indexes, and a `UserAgentMapping` table. Reversible: drops the indexes and the mapping table. +- **mcp_tool_logs_add_user_agent_column** - Adds user agent and app columns plus indexes to MCP tool logs. Reversible: drops both indexes and the `app` column. +- **mcp_tool_logs_add_endpoint_columns** - Adds `source`, `decision`, `app_key` and `device_id` to MCP tool logs. Reversible: drops all four columns. +- **mcp_tool_logs_add_plugin_logs_column** - Adds `plugin_logs` to MCP tool logs. Reversible: drops the added column. +- **logs_recreate_matviews_with_user_agent_column** and **logs_recreate_matviews_with_app_column** - Recreate the log materialized views to include the new columns. Rollback is a no-op because `ensureMatViews` recreates them on next startup. + + +**High-throughput deployments: run the logstore migrations during a low-activity window.** + +Every logstore migration above alters `logs` or `mcp_tool_logs`, the two highest-insert tables in Bifrost, and several also build indexes on them. On a busy instance the index builds hold locks that block concurrent log inserts for the duration of the build, and the matview recreations rebuild against the full table. Schedule the upgrade for a low-traffic period, or expect elevated log-write latency and possible request-path backpressure while the migrations run. + + + +`merge_oauth_token_tables`, `drop_oauth_config_pkce_columns` and `drop_oauth_config_token_id_column` transform or remove existing OAuth state and cannot be rolled back. Take a database backup before upgrading, and do not roll the binary back past this release once the migration has run. + + +## 🐙 Closed GitHub Issues + +- [#123](https://github.com/maximhq/bifrost/issues/123) - Files API Support +- [#5472](https://github.com/maximhq/bifrost/issues/5472) - [Bug]: Bedrock rejects office/PDF document uploads via OpenAI `type:"file"` - "The PDF specified was not valid" +- [#5900](https://github.com/maximhq/bifrost/issues/5900) - [Bug]: Streaming continuation chunks materialize omitted tool-call metadata as null +- [#5978](https://github.com/maximhq/bifrost/issues/5978) - [Bug]: Gemini egress reports truncated responses as FinishReason OTHER, IncompleteDetails switch matches a string that never occurs +- [#6044](https://github.com/maximhq/bifrost/issues/6044) - [Bug]: normalizeOpenAIReasoningEffort maps 'minimal' to 'low' for ALL OpenAI models, even ones that natively support 'minimal' From c8092466ebdb31e392ed3149d63b3390b974de79 Mon Sep 17 00:00:00 2001 From: Akshay Deo Date: Thu, 13 Aug 2026 05:09:54 -0700 Subject: [PATCH 006/129] updates skip-core-test flag and changelog (#6128) ## Summary Briefly explain the purpose of this PR and the problem it solves. ## Changes - What was changed and why - Any notable design decisions or trade-offs ## Type of change - [ ] Bug fix - [ ] Feature - [ ] Refactor - [ ] Documentation - [ ] Chore/CI ## Affected areas - [ ] Core (Go) - [ ] Transports (HTTP) - [ ] Providers/Integrations - [ ] Plugins - [ ] UI (React) - [ ] Docs ## How to test Describe the steps to validate this change. Include commands and expected outcomes. ```sh # Core/Transports go version go test ./... # UI cd ui pnpm i || npm i pnpm test || npm test pnpm build || npm run build ``` If adding new configs or environment variables, document them here. ## Screenshots/Recordings If UI changes, add before/after screenshots or short clips. ## Breaking changes - [ ] Yes - [ ] No If yes, describe impact and migration instructions. ## Related issues Link related issues and discussions. Example: Closes #123 ## Security considerations Note any security implications (auth, secrets, PII, sandboxing, etc.). ## Checklist - [ ] I read `docs/contributing/README.md` and followed the guidelines - [ ] I added/updated tests where appropriate - [ ] I updated documentation where needed - [ ] I verified builds succeed (Go and UI) - [ ] I verified the CI pipeline passes locally if applicable --- transports/changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/transports/changelog.md b/transports/changelog.md index e94be646578..f07d891c651 100644 --- a/transports/changelog.md +++ b/transports/changelog.md @@ -17,6 +17,7 @@ - **Video Request Info in Logs UI** - Video requests surface their details in the logs UI (#5946) - **Shell Rewriter Hook** - The UI handler exposes a `ShellRewriter` hook for pre-hydration HTML rewriting (#5807) - **Auth Skip Path** - Adds a context path letting trusted internal callers bypass auth resolution +- - **Runware passthrough** - Adds `runware_passthrough` path for handling passthrough mode for Runware provider ## 🐞 Fixed From 48e2985237486489482c8b2f64162f34adc04e2e Mon Sep 17 00:00:00 2001 From: Suresh Chaudhary <83772622+impoiler@users.noreply.github.com> Date: Fri, 14 Aug 2026 19:27:26 +0530 Subject: [PATCH 007/129] feat(ui): responsive layout improvements across all views (#6105) ## Summary Add mobile responsiveness to make the dashboard usable on smaller devices. It does not have full coverage, but it includes basic responsiveness so it can be used or at the very least viewed, on mobile screens. ## Changes - Responsiveness ## Type of change - [ ] Bug fix - [x] Feature - [ ] Refactor - [ ] Documentation - [ ] Chore/CI ## Affected areas - [ ] Core (Go) - [ ] Transports (HTTP) - [ ] Providers/Integrations - [ ] Plugins - [x] UI (React) - [ ] Docs ## How to test Describe the steps to validate this change. Include commands and expected outcomes. ```sh # Core/Transports go version go test ./... # UI cd ui pnpm i || npm i pnpm test || npm test pnpm build || npm run build ``` If adding new configs or environment variables, document them here. ## Screenshots/Recordings If UI changes, add before/after screenshots or short clips. ## Breaking changes - [ ] Yes - [x] No If yes, describe impact and migration instructions. ## Related issues ## Security considerations Note any security implications (auth, secrets, PII, sandboxing, etc.). ## Checklist - [ ] I read `docs/contributing/README.md` and followed the guidelines - [ ] I added/updated tests where appropriate - [ ] I updated documentation where needed - [ ] I verified builds succeed (Go and UI) - [ ] I verified the CI pipeline passes locally if applicable --- .../e2e/features/mobile/mobile-smoke.spec.ts | 40 ++++++++++++++ tests/e2e/playwright.config.ts | 8 ++- .../sheets/customerDetailSheet.tsx | 6 +-- ui/app/clientLayout.tsx | 21 ++++++-- ui/app/globals.css | 20 +++++-- ui/app/workspace/adaptive-routing/page.tsx | 2 +- .../adaptive-routing/settings/page.tsx | 4 +- ui/app/workspace/alerting/rules/layout.tsx | 2 +- ui/app/workspace/circuit-breaker/page.tsx | 4 +- ui/app/workspace/cluster/page.tsx | 4 +- ui/app/workspace/complexity-router/page.tsx | 52 +++++++++++------- ui/app/workspace/config/api-keys/page.tsx | 4 +- ui/app/workspace/config/branding/page.tsx | 2 +- ui/app/workspace/config/caching/page.tsx | 2 +- .../workspace/config/client-settings/page.tsx | 2 +- .../workspace/config/compatibility/page.tsx | 4 +- .../workspace/config/feature-flags/page.tsx | 4 +- ui/app/workspace/config/license/page.tsx | 4 +- ui/app/workspace/config/logging/page.tsx | 2 +- .../config/performance-tuning/page.tsx | 4 +- ui/app/workspace/config/proxy/page.tsx | 2 +- ui/app/workspace/config/security/page.tsx | 2 +- ui/app/workspace/config/views/cachingView.tsx | 10 ++-- ui/app/workspace/config/views/loggingView.tsx | 4 +- ui/app/workspace/config/views/mcpView.tsx | 2 +- ui/app/workspace/config/views/proxyView.tsx | 2 +- .../workspace/config/views/securityView.tsx | 2 +- .../config/views/userAgentMappingsView.tsx | 6 +-- .../overrides/pricingOverrideSheet.tsx | 10 ++-- .../overrides/pricingOverridesEmptyState.tsx | 2 +- .../overrides/scopedPricingOverridesView.tsx | 2 +- ui/app/workspace/custom-pricing/page.tsx | 4 +- .../dashboard/components/charts/chartCard.tsx | 9 ++-- .../components/dimensionRankingsTab.tsx | 2 +- .../dashboard/components/modelRankingsTab.tsx | 2 +- ui/app/workspace/dashboard/page.tsx | 6 +-- .../governance/views/customerSheet.tsx | 8 +-- .../governance/views/customerTable.tsx | 2 +- .../governance/views/customersEmptyState.tsx | 2 +- .../workspace/governance/views/teamSheet.tsx | 8 +-- .../governance/views/teamsEmptyState.tsx | 2 +- .../workspace/governance/views/teamsTable.tsx | 2 +- .../guardrails/configuration/page.tsx | 4 +- .../workspace/guardrails/providers/page.tsx | 2 +- .../workspace/logs/sheets/logDetailView.tsx | 22 ++++---- .../workspace/logs/sheets/logDetailsSheet.tsx | 4 +- .../logs/sheets/observabilityConfigSheet.tsx | 6 +-- .../sheets/observabilitySettingsSheet.tsx | 4 +- .../logs/sheets/sessionDetailsSheet.tsx | 4 +- ui/app/workspace/logs/views/columns.tsx | 2 +- ui/app/workspace/logs/views/emptyState.tsx | 2 +- .../workspace/logs/views/logsHeaderView.tsx | 8 +-- ui/app/workspace/logs/views/logsTable.tsx | 2 +- ui/app/workspace/logs/views/ocrView.tsx | 4 +- .../logs/views/transcriptionView.tsx | 2 +- ui/app/workspace/logs/views/videoView.tsx | 8 +-- .../workspace/mcp-logs/views/emptyState.tsx | 2 +- .../mcp-logs/views/mcpHeaderView.tsx | 5 +- .../mcp-logs/views/mcpLogDetailsSheet.tsx | 22 ++++---- .../workspace/mcp-registry/library/page.tsx | 24 +++++---- .../views/mcpLibraryAddServerSheet.tsx | 12 ++--- .../library/views/mcpLibraryFilterSidebar.tsx | 31 +++++++---- .../library/views/mcpLibraryInstallSheet.tsx | 6 +-- .../library/views/mcpLibraryServersTable.tsx | 10 ++-- .../library/views/mcpLibrarySettingsSheet.tsx | 8 +-- .../mcp-registry/views/mcpClientForm.tsx | 8 +-- .../mcp-registry/views/mcpClientSheet.tsx | 8 +-- .../views/mcpClientsFilterSidebar.tsx | 29 ++++++---- .../mcp-registry/views/mcpClientsTable.tsx | 4 +- .../views/mcpServersEmptyState.tsx | 2 +- .../mcpUsageGuide/mcpUsageGuideSheet.tsx | 6 +-- .../views/tokenExchangeFields.tsx | 2 +- .../views/mcpSessionsFilterSidebar.tsx | 40 ++++++++++---- ui/app/workspace/mcp-settings/page.tsx | 2 +- ui/app/workspace/mcp-tool-groups/page.tsx | 4 +- .../model-catalog/views/attributeSheet.tsx | 12 ++--- .../model-catalog/views/attributesTab.tsx | 10 ++-- .../views/modelCatalogEmptyState.tsx | 2 +- .../model-catalog/views/modelCatalogTable.tsx | 2 +- .../model-limits/views/modelLimitSheet.tsx | 10 ++-- .../views/modelLimitsEmptyState.tsx | 9 +--- .../model-limits/views/modelLimitsTable.tsx | 6 +-- ui/app/workspace/oauth-grants/page.tsx | 2 +- .../views/oauthGrantsFilterSidebar.tsx | 45 +++++++++++----- .../fragments/prometheusFormFragment.tsx | 4 +- ui/app/workspace/observability/page.tsx | 2 +- .../sheets/pluginTracingSheet.tsx | 4 +- .../observability/views/observabilityView.tsx | 20 +++++-- ui/app/workspace/plugins/page.tsx | 33 ++++++++---- .../plugins/sheets/addNewPluginSheet.tsx | 8 +-- .../plugins/sheets/pluginSequenceSheet.tsx | 4 +- .../plugins/views/pluginsEmptyState.tsx | 2 +- .../dialogs/addNewCustomProviderSheet.tsx | 8 +-- .../providers/dialogs/addNewKeySheet.tsx | 4 +- .../providers/dialogs/providerConfigSheet.tsx | 6 +-- .../fragments/allowedRequestsFields.tsx | 4 +- .../fragments/apiKeysFormFragment.tsx | 8 +-- .../fragments/apiStructureFormFragment.tsx | 4 +- .../fragments/betaHeadersFormFragment.tsx | 6 +-- .../fragments/governanceFormFragment.tsx | 6 +-- .../fragments/networkFormFragment.tsx | 6 +-- .../fragments/openaiConfigFormFragment.tsx | 4 +- .../fragments/performanceFormFragment.tsx | 4 +- .../providers/fragments/proxyFormFragment.tsx | 10 ++-- ui/app/workspace/providers/page.tsx | 54 ++++++++++++------- .../providers/views/modelProviderConfig.tsx | 13 +++-- .../views/modelProviderKeysTableView.tsx | 28 +++++----- .../providers/views/providerKeyForm.tsx | 6 +-- .../providers/views/providersEmptyState.tsx | 2 +- .../tree/views/routingTreeView.tsx | 20 +++++++ .../views/routingRuleInfoSheet.tsx | 24 ++++----- .../routing-rules/views/routingRuleSheet.tsx | 20 +++---- .../views/routingRulesEmptyState.tsx | 2 +- .../routing-rules/views/routingRulesView.tsx | 2 +- ui/app/workspace/scim/page.tsx | 6 +-- .../skills-repo/components/shared.tsx | 25 +++++---- .../components/skillDetailsView.tsx | 5 +- .../skills-repo/components/skillListView.tsx | 42 ++++++++------- .../skills-repo/forms/skillEditForm.tsx | 33 +++++++----- ui/app/workspace/skills-repo/page.tsx | 10 ++-- .../views/virtualKeyDetailsSheet.tsx | 22 ++++---- .../virtual-keys/views/virtualKeySheet.tsx | 10 ++-- .../views/virtualKeysEmptyState.tsx | 2 +- .../virtual-keys/views/virtualKeysTable.tsx | 24 ++++----- ui/app/workspace/webhooks/page.tsx | 4 +- .../webhooks/views/webhookDetailsSheet.tsx | 6 +-- .../workspace/webhooks/views/webhookSheet.tsx | 8 +-- .../webhooks/views/webhooksEmptyState.tsx | 2 +- ui/components/filters/logsFilterSidebar.tsx | 43 ++++++++++----- ui/components/filters/mcpFilterSidebar.tsx | 43 ++++++++++----- ui/components/header.tsx | 2 +- ui/components/loggingDisabledView.tsx | 2 +- ui/components/onboardingWidget.tsx | 8 +-- .../prompts/components/emptyState.tsx | 2 +- ui/components/prompts/promptsView.tsx | 38 +++++++++++++ .../prompts/sheets/commitVersionSheet.tsx | 4 +- ui/components/prompts/sheets/folderSheet.tsx | 4 +- ui/components/prompts/sheets/promptSheet.tsx | 8 +-- ui/components/sidebar.tsx | 8 +-- .../ui/custom/celBuilder/celRuleBuilder.tsx | 2 +- ui/components/ui/datePickerWithRange.tsx | 19 ++++--- ui/components/ui/sheet.tsx | 4 +- ui/components/ui/tagInput.tsx | 2 +- ui/index.html | 4 +- ui/lib/config/celFieldsRouting.ts | 2 +- ui/lib/utils/loginGoto.ts | 2 +- 146 files changed, 850 insertions(+), 543 deletions(-) create mode 100644 tests/e2e/features/mobile/mobile-smoke.spec.ts diff --git a/tests/e2e/features/mobile/mobile-smoke.spec.ts b/tests/e2e/features/mobile/mobile-smoke.spec.ts new file mode 100644 index 00000000000..ec225a86879 --- /dev/null +++ b/tests/e2e/features/mobile/mobile-smoke.spec.ts @@ -0,0 +1,40 @@ +import { expect, test } from '../../core/fixtures/base.fixture' + +const mobileRoutes = [ + { name: 'dashboard', path: '/workspace/dashboard' }, + { name: 'logs', path: '/workspace/logs' }, + { name: 'virtual keys', path: '/workspace/virtual-keys' }, + { name: 'providers', path: '/workspace/providers' }, + { name: 'client settings', path: '/workspace/config/client-settings' }, +] + +test.describe('Mobile reachability', () => { + for (const route of mobileRoutes) { + test(`${route.name} fits the viewport and exposes navigation`, async ({ page }) => { + await page.goto(route.path) + await page.waitForLoadState('domcontentloaded') + + await expect(page.locator('[data-sidebar="trigger"]')).toBeVisible() + await expect + .poll(() => page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth + 1)) + .toBe(true) + }) + } + + test('mobile navigation opens and exposes workspace links', async ({ page }) => { + await page.goto('/workspace/dashboard') + await page.locator('[data-sidebar="trigger"]').click() + const sidebar = page.getByRole('dialog') + await expect(sidebar).toBeVisible() + + await page.getByTestId('sidebar-item-btn-models').click() + + await expect(sidebar).toBeVisible() + await expect(page.getByTestId('sidebar-subitem-link-model-catalog')).toBeVisible() + }) + + test('routing tree shows the mobile fallback', async ({ page }) => { + await page.goto('/workspace/routing-rules/tree') + await expect(page.getByTestId('routing-tree-mobile-list-btn')).toBeVisible() + }) +}) diff --git a/tests/e2e/playwright.config.ts b/tests/e2e/playwright.config.ts index e0b47410d71..e6899df8de8 100644 --- a/tests/e2e/playwright.config.ts +++ b/tests/e2e/playwright.config.ts @@ -10,7 +10,7 @@ const projects: NonNullable = [ name: 'chromium', testDir: './features', use: { ...devices['Desktop Chrome'] }, - testIgnore: ['**/config/**', '**/plugins/**', '**/virtual-keys/**', '**/mcp-registry/**', '**/model-limits/**', '**/providers/**'], + testIgnore: ['**/config/**', '**/mobile/**', '**/plugins/**', '**/virtual-keys/**', '**/mcp-registry/**', '**/model-limits/**', '**/providers/**'], }, { name: 'chromium-serial', @@ -26,6 +26,12 @@ const projects: NonNullable = [ testMatch: ['**/config/**/*.spec.ts'], dependencies: ['chromium', 'chromium-serial'], }, + { + name: 'mobile-chrome', + testDir: './features', + use: { ...devices['Pixel 7'] }, + testMatch: ['**/mobile/**/*.spec.ts'], + }, ] if (includeEnterprise) { diff --git a/ui/app/_fallbacks/enterprise/components/user-groups/sheets/customerDetailSheet.tsx b/ui/app/_fallbacks/enterprise/components/user-groups/sheets/customerDetailSheet.tsx index 858e4d32ef8..9f407408040 100644 --- a/ui/app/_fallbacks/enterprise/components/user-groups/sheets/customerDetailSheet.tsx +++ b/ui/app/_fallbacks/enterprise/components/user-groups/sheets/customerDetailSheet.tsx @@ -110,7 +110,7 @@ export function CustomerDetailSheet({ customer, open, onOpenChange }: Props) { return ( - +
{customer?.name || "Customer Details"} {customer?.id && } @@ -119,7 +119,7 @@ export function CustomerDetailSheet({ customer, open, onOpenChange }: Props) { {customer && ( -
+
{/* ── Info ─────────────────────────────────────────── */}
@@ -183,4 +183,4 @@ export function CustomerDetailSheet({ customer, open, onOpenChange }: Props) { ); } -export default CustomerDetailSheet; \ No newline at end of file +export default CustomerDetailSheet; diff --git a/ui/app/clientLayout.tsx b/ui/app/clientLayout.tsx index 89a6ae1899c..fac20dbd1ed 100644 --- a/ui/app/clientLayout.tsx +++ b/ui/app/clientLayout.tsx @@ -5,7 +5,7 @@ import ProgressProvider from "@/components/progressBar"; import Sidebar from "@/components/sidebar"; import { ThemeProvider } from "@/components/themeProvider"; import TrialExpiryBanner from "@/components/trialExpiryBanner"; -import { SidebarProvider } from "@/components/ui/sidebar"; +import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"; import { useStoreSync } from "@/hooks/useStoreSync"; import { WebSocketProvider } from "@/hooks/useWebSocket"; import { getErrorMessage, ReduxProvider, useGetCoreConfigQuery, useIsAuthEnabledQuery } from "@/lib/store"; @@ -49,6 +49,11 @@ function AppContent({ children }: { children: React.ReactNode }) { // neither a fragment nor a cookie to drive the tempTokenScoped per-visitor // logic. const publicShell = matches.some((m) => (m.staticData as { publicShell?: boolean } | undefined)?.publicShell === true); + const pathname = useLocation({ select: (location) => location.pathname }); + const mobilePageTitle = (pathname.split("/").filter(Boolean).at(-1) ?? "Dashboard") + .split("-") + .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) + .join(" "); // Probe dashboard auth state on opted-in routes. is-auth-enabled is whitelisted // (no 401 risk) and returns whether the current cookie is a valid session. @@ -111,9 +116,15 @@ function AppContent({ children }: { children: React.ReactNode }) { -
+
+
+
+ + {mobilePageTitle} +
+
-
+
{isLoading ? : {children}}
{bifrostConfig?.is_db_connected && } @@ -130,7 +141,7 @@ function AppContent({ children }: { children: React.ReactNode }) { // like the MCP per-user OAuth auth page. function MinimalShell({ children }: { children: React.ReactNode }) { return ( -
+
{children}
@@ -165,4 +176,4 @@ export function ClientLayout({ children }: { children: React.ReactNode }) { ); -} \ No newline at end of file +} diff --git a/ui/app/globals.css b/ui/app/globals.css index fb15acbc3ad..e10de613384 100644 --- a/ui/app/globals.css +++ b/ui/app/globals.css @@ -300,8 +300,22 @@ div.content-container:has(.no-border-parent) { /* // trial-notification-banner style*/ #trial-notification-banner { - width: calc(100% + 80px); - margin-left: -40px; + width: calc(100% + 32px); + margin-left: -16px; +} + +@media (min-width: 768px) { +#trial-notification-banner { + width: 100%; + margin-left: 0; +} + +@media (min-width: 48rem) { + #trial-notification-banner { + width: calc(100% + 80px); + margin-left: -40px; + } +} } div.content-container:has(.no-padding-parent) #trial-notification-banner { @@ -424,4 +438,4 @@ div.content-container:has(.no-border-parent) #trial-notification-banner { left: auto !important; right: 0 !important; transform: translate(35%, -35%) !important; -} \ No newline at end of file +} diff --git a/ui/app/workspace/adaptive-routing/page.tsx b/ui/app/workspace/adaptive-routing/page.tsx index 1c3b80f010f..e53a89b9518 100644 --- a/ui/app/workspace/adaptive-routing/page.tsx +++ b/ui/app/workspace/adaptive-routing/page.tsx @@ -6,4 +6,4 @@ export default function AdaptiveRoutingPage() {
); -} \ No newline at end of file +} diff --git a/ui/app/workspace/adaptive-routing/settings/page.tsx b/ui/app/workspace/adaptive-routing/settings/page.tsx index 34fe692eef7..f6206edfe61 100644 --- a/ui/app/workspace/adaptive-routing/settings/page.tsx +++ b/ui/app/workspace/adaptive-routing/settings/page.tsx @@ -2,8 +2,8 @@ import LoadBalancerSettingsView from "@enterprise/components/load-balancer/loadB export default function AdaptiveRoutingSettingsPage() { return ( -
+
); -} \ No newline at end of file +} diff --git a/ui/app/workspace/alerting/rules/layout.tsx b/ui/app/workspace/alerting/rules/layout.tsx index 1dcce6c9465..87ff5eaac72 100644 --- a/ui/app/workspace/alerting/rules/layout.tsx +++ b/ui/app/workspace/alerting/rules/layout.tsx @@ -3,4 +3,4 @@ import AlertRulesPage from "./page"; export const Route = createFileRoute("/workspace/alerting/rules")({ component: AlertRulesPage, -}); \ No newline at end of file +}); diff --git a/ui/app/workspace/circuit-breaker/page.tsx b/ui/app/workspace/circuit-breaker/page.tsx index f14d34fbfb6..471c7b5a430 100644 --- a/ui/app/workspace/circuit-breaker/page.tsx +++ b/ui/app/workspace/circuit-breaker/page.tsx @@ -2,8 +2,8 @@ import CircuitBreakerView from "@enterprise/components/circuit-breaker/circuitBr export default function CircuitBreakerPage() { return ( -
+
); -} \ No newline at end of file +} diff --git a/ui/app/workspace/cluster/page.tsx b/ui/app/workspace/cluster/page.tsx index 155d593efb7..3f97f877693 100644 --- a/ui/app/workspace/cluster/page.tsx +++ b/ui/app/workspace/cluster/page.tsx @@ -2,8 +2,8 @@ import ClusterView from "@enterprise/components/cluster/clusterView"; export default function ClusterPage() { return ( -
+
); -} \ No newline at end of file +} diff --git a/ui/app/workspace/complexity-router/page.tsx b/ui/app/workspace/complexity-router/page.tsx index 2b0073e6896..9fb0edfed1a 100644 --- a/ui/app/workspace/complexity-router/page.tsx +++ b/ui/app/workspace/complexity-router/page.tsx @@ -310,7 +310,7 @@ export default function ComplexityRouterPage() { if (error && !data) { return ( -
+

{getErrorMessage(error)}

{/* ── Complexity Spectrum ── */} -
-
+
+

Complexity Spectrum

-
+
{Object.values(TIER_PALETTE).map(({ color, name }) => (
@@ -459,7 +465,7 @@ export default function ComplexityRouterPage() { {/* ── Keyword Lists ── */}
-
+

Keyword Lists

Lowercased and deduplicated on save. Each list requires at least one entry. @@ -477,8 +483,8 @@ export default function ComplexityRouterPage() { name={`keywords.${key}` as const} rules={{ validate: (value) => (value.length > 0 ? true : `${label} cannot be empty`) }} render={({ field }) => ( -
-
+
+
{label} {field.value.length} {field.value.length === 1 ? "entry" : "entries"} @@ -520,40 +526,48 @@ export default function ComplexityRouterPage() {
)} - {/* ── Action footer ── */} -
+ + + + {/* ── Persistent action footer ── */} +
+
- +
@@ -585,6 +599,6 @@ export default function ComplexityRouterPage() { - +
); -} \ No newline at end of file +} diff --git a/ui/app/workspace/config/api-keys/page.tsx b/ui/app/workspace/config/api-keys/page.tsx index 2626cbbe408..7e1c3e857ec 100644 --- a/ui/app/workspace/config/api-keys/page.tsx +++ b/ui/app/workspace/config/api-keys/page.tsx @@ -2,8 +2,8 @@ import APIKeysView from "@enterprise/components/api-keys/apiKeysIndexView"; export default function APIKeysPage() { return ( -
+
); -} \ No newline at end of file +} diff --git a/ui/app/workspace/config/branding/page.tsx b/ui/app/workspace/config/branding/page.tsx index d5f8829a2d2..ae90d67b81b 100644 --- a/ui/app/workspace/config/branding/page.tsx +++ b/ui/app/workspace/config/branding/page.tsx @@ -19,7 +19,7 @@ export default function BrandingPage() { } return ( -
+
); diff --git a/ui/app/workspace/config/caching/page.tsx b/ui/app/workspace/config/caching/page.tsx index e3323b02153..41c038701ea 100644 --- a/ui/app/workspace/config/caching/page.tsx +++ b/ui/app/workspace/config/caching/page.tsx @@ -6,4 +6,4 @@ export default function CachingPage() {
); -} \ No newline at end of file +} diff --git a/ui/app/workspace/config/client-settings/page.tsx b/ui/app/workspace/config/client-settings/page.tsx index c994c88b82a..f70fc33dbf8 100644 --- a/ui/app/workspace/config/client-settings/page.tsx +++ b/ui/app/workspace/config/client-settings/page.tsx @@ -6,4 +6,4 @@ export default function ClientSettingsPage() {
); -} \ No newline at end of file +} diff --git a/ui/app/workspace/config/compatibility/page.tsx b/ui/app/workspace/config/compatibility/page.tsx index 789a5f01350..6ee8b5a45b6 100644 --- a/ui/app/workspace/config/compatibility/page.tsx +++ b/ui/app/workspace/config/compatibility/page.tsx @@ -2,8 +2,8 @@ import CompatibilityView from "../views/compatibilityView"; export default function CompatibilityPage() { return ( -
+
); -} \ No newline at end of file +} diff --git a/ui/app/workspace/config/feature-flags/page.tsx b/ui/app/workspace/config/feature-flags/page.tsx index 0ef57e3be5a..519a5e23ce6 100644 --- a/ui/app/workspace/config/feature-flags/page.tsx +++ b/ui/app/workspace/config/feature-flags/page.tsx @@ -2,8 +2,8 @@ import FeatureFlagsView from "../views/featureFlagsView"; export default function FeatureFlagsPage() { return ( -
+
); -} \ No newline at end of file +} diff --git a/ui/app/workspace/config/license/page.tsx b/ui/app/workspace/config/license/page.tsx index 904ee35fa75..facd2a9fd7b 100644 --- a/ui/app/workspace/config/license/page.tsx +++ b/ui/app/workspace/config/license/page.tsx @@ -17,8 +17,8 @@ export default function LicensePage() { } return ( -
+
); -} \ No newline at end of file +} diff --git a/ui/app/workspace/config/logging/page.tsx b/ui/app/workspace/config/logging/page.tsx index b7880bc8168..83c991ea5bd 100644 --- a/ui/app/workspace/config/logging/page.tsx +++ b/ui/app/workspace/config/logging/page.tsx @@ -6,4 +6,4 @@ export default function LoggingPage() {
); -} \ No newline at end of file +} diff --git a/ui/app/workspace/config/performance-tuning/page.tsx b/ui/app/workspace/config/performance-tuning/page.tsx index 1bdbe2af051..35802cf35db 100644 --- a/ui/app/workspace/config/performance-tuning/page.tsx +++ b/ui/app/workspace/config/performance-tuning/page.tsx @@ -2,8 +2,8 @@ import PerformanceTuningView from "../views/performanceTuningView"; export default function PerformanceTuningPage() { return ( -
+
); -} \ No newline at end of file +} diff --git a/ui/app/workspace/config/proxy/page.tsx b/ui/app/workspace/config/proxy/page.tsx index b80b1b1f6c9..c0fa78786e9 100644 --- a/ui/app/workspace/config/proxy/page.tsx +++ b/ui/app/workspace/config/proxy/page.tsx @@ -21,4 +21,4 @@ export default function ProxyPage() {
); -} \ No newline at end of file +} diff --git a/ui/app/workspace/config/security/page.tsx b/ui/app/workspace/config/security/page.tsx index 03bcd072688..53cb30e5868 100644 --- a/ui/app/workspace/config/security/page.tsx +++ b/ui/app/workspace/config/security/page.tsx @@ -6,4 +6,4 @@ export default function SecurityPage() {
); -} \ No newline at end of file +} diff --git a/ui/app/workspace/config/views/cachingView.tsx b/ui/app/workspace/config/views/cachingView.tsx index 960168b5e13..d7de3aa7b12 100644 --- a/ui/app/workspace/config/views/cachingView.tsx +++ b/ui/app/workspace/config/views/cachingView.tsx @@ -328,7 +328,7 @@ export default function CachingView() {
setMode(v as CacheMode)}> - + Direct only @@ -381,7 +381,7 @@ export default function CachingView() {

Embedding Provider & Model

-
+

Storage & Cache Key

-
+

Conversation Settings

-
+
+

Logs Settings

Configure logging settings for requests and responses.

@@ -300,4 +300,4 @@ export default function LoggingView() { const RestartWarning = () => { return
Need to restart Bifrost to apply changes.
; -}; \ No newline at end of file +}; diff --git a/ui/app/workspace/config/views/mcpView.tsx b/ui/app/workspace/config/views/mcpView.tsx index 1281907effc..90f3e1d8c06 100644 --- a/ui/app/workspace/config/views/mcpView.tsx +++ b/ui/app/workspace/config/views/mcpView.tsx @@ -698,4 +698,4 @@ export default function MCPView() {
); -} \ No newline at end of file +} diff --git a/ui/app/workspace/config/views/proxyView.tsx b/ui/app/workspace/config/views/proxyView.tsx index 17bdddc8ab6..f75d491b524 100644 --- a/ui/app/workspace/config/views/proxyView.tsx +++ b/ui/app/workspace/config/views/proxyView.tsx @@ -149,7 +149,7 @@ export default function ProxyView() { {/* Authentication Section */}

Authentication (Optional)

-
+
({ ...prev, dual_credential_conflict_behavior: value as CoreConfig["dual_credential_conflict_behavior"] })) } > - + diff --git a/ui/app/workspace/config/views/userAgentMappingsView.tsx b/ui/app/workspace/config/views/userAgentMappingsView.tsx index 1afafcaab0f..9fdfc544a19 100644 --- a/ui/app/workspace/config/views/userAgentMappingsView.tsx +++ b/ui/app/workspace/config/views/userAgentMappingsView.tsx @@ -129,14 +129,14 @@ export default function UserAgentMappingsView({ disabled }: UserAgentMappingsVie - + {isEditing ? "Edit User Agent Mapping" : "Add User Agent Mapping"} Define how a User-Agent value maps to an app label in logs. -
+
- + diff --git a/ui/app/workspace/custom-pricing/overrides/pricingOverrideSheet.tsx b/ui/app/workspace/custom-pricing/overrides/pricingOverrideSheet.tsx index 9458d3abe56..86c30b13f98 100644 --- a/ui/app/workspace/custom-pricing/overrides/pricingOverrideSheet.tsx +++ b/ui/app/workspace/custom-pricing/overrides/pricingOverrideSheet.tsx @@ -594,13 +594,13 @@ export default function PricingOverrideSheet({ open, onOpenChange, editingOverri return ( (o ? onOpenChange(true) : handleCloseDrawer())}> - + {editingOverride ? "Edit Pricing Override" : "Create Pricing Override"}
-
+
)} -
+
-
+

Pricing overrides customize cost tracking per scope

-
+
Define custom per-token prices for specific providers, keys, or virtual keys to accurately reflect your negotiated rates.
diff --git a/ui/app/workspace/custom-pricing/overrides/scopedPricingOverridesView.tsx b/ui/app/workspace/custom-pricing/overrides/scopedPricingOverridesView.tsx index 6c9ae2b9e21..43f8e848b7e 100644 --- a/ui/app/workspace/custom-pricing/overrides/scopedPricingOverridesView.tsx +++ b/ui/app/workspace/custom-pricing/overrides/scopedPricingOverridesView.tsx @@ -319,7 +319,7 @@ export default function ScopedPricingOverridesView() { return (
-
+

Pricing Overrides

diff --git a/ui/app/workspace/custom-pricing/page.tsx b/ui/app/workspace/custom-pricing/page.tsx index a2c6cc11201..26439917c6a 100644 --- a/ui/app/workspace/custom-pricing/page.tsx +++ b/ui/app/workspace/custom-pricing/page.tsx @@ -2,8 +2,8 @@ import ModelSettingsView from "@/app/workspace/config/views/modelSettingsView"; export default function CustomPricingPage() { return ( -

+
); -} \ No newline at end of file +} diff --git a/ui/app/workspace/dashboard/components/charts/chartCard.tsx b/ui/app/workspace/dashboard/components/charts/chartCard.tsx index dcd815199ba..6dab70c8e1b 100644 --- a/ui/app/workspace/dashboard/components/charts/chartCard.tsx +++ b/ui/app/workspace/dashboard/components/charts/chartCard.tsx @@ -91,7 +91,10 @@ function Header({ {title}
{hasActionRow && ( -
+
{hasTotal ? (
@@ -132,7 +135,7 @@ export function ChartCard({ }: ChartCardProps) { if (loading) { return ( - +
+
{rankedItems.length > 0 && ( -
+
{rankedItems.map((item, idx) => (
{idx + 1}. diff --git a/ui/app/workspace/dashboard/components/modelRankingsTab.tsx b/ui/app/workspace/dashboard/components/modelRankingsTab.tsx index c06101dedf3..84036259348 100644 --- a/ui/app/workspace/dashboard/components/modelRankingsTab.tsx +++ b/ui/app/workspace/dashboard/components/modelRankingsTab.tsx @@ -212,7 +212,7 @@ function TopModelsChart({
{/* Ranked model legend */} {modelTotals.length > 0 && ( -
+
{modelTotals.map((m, idx) => (
{idx + 1}. diff --git a/ui/app/workspace/dashboard/page.tsx b/ui/app/workspace/dashboard/page.tsx index 6fcea2a62fd..7083c413222 100644 --- a/ui/app/workspace/dashboard/page.tsx +++ b/ui/app/workspace/dashboard/page.tsx @@ -474,8 +474,8 @@ export default function DashboardPage() { {/* Main Content */} {/* Header */} -
-
+
+

Dashboard

@@ -487,7 +487,7 @@ export default function DashboardPage() { onExportDone={handleExportDone} /> {activeTab === "mcp" && mcpFilterData && ( -
+
{(mcpFilterData.tool_names?.length ?? 0) > 0 && ( - + {isEditing ? "Edit Customer" : "Create Customer"} {customer?.id && } @@ -315,7 +315,7 @@ export default function CustomerSheet({ open, onOpenChange, customer, onSuccess -
+
@@ -415,7 +415,7 @@ export default function CustomerSheet({ open, onOpenChange, customer, onSuccess
- + @@ -440,4 +440,4 @@ export default function CustomerSheet({ open, onOpenChange, customer, onSuccess ); -} \ No newline at end of file +} diff --git a/ui/app/workspace/governance/views/customerTable.tsx b/ui/app/workspace/governance/views/customerTable.tsx index 5af875b31fa..50370752fbb 100644 --- a/ui/app/workspace/governance/views/customerTable.tsx +++ b/ui/app/workspace/governance/views/customerTable.tsx @@ -223,7 +223,7 @@ export default function CustomersTable({ />
-
+

Customers

Manage customer accounts with their own teams, budgets, and access controls.

diff --git a/ui/app/workspace/governance/views/customersEmptyState.tsx b/ui/app/workspace/governance/views/customersEmptyState.tsx index 5b3edfffbc6..e41137ae5c5 100644 --- a/ui/app/workspace/governance/views/customersEmptyState.tsx +++ b/ui/app/workspace/governance/views/customersEmptyState.tsx @@ -17,7 +17,7 @@ export function CustomersEmptyState({ onAddClick, canCreate = true }: CustomersE

Customers have their own teams, budgets, and access controls

-
+
Create customer accounts to manage multi-tenant usage, assign teams, and set spending and rate limits per customer.
diff --git a/ui/app/workspace/governance/views/teamSheet.tsx b/ui/app/workspace/governance/views/teamSheet.tsx index 488937f5ada..57de53d06c5 100644 --- a/ui/app/workspace/governance/views/teamSheet.tsx +++ b/ui/app/workspace/governance/views/teamSheet.tsx @@ -362,7 +362,7 @@ export default function TeamSheet({ team, onSave, onCancel }: TeamSheetProps) { onInteractOutside={(e) => e.preventDefault()} onEscapeKeyDown={() => onCancel()} > - + {isEditing ? "Edit Team" : "Create Team"} {team?.id && } @@ -373,7 +373,7 @@ export default function TeamSheet({ team, onSave, onCancel }: TeamSheetProps) { -
+
{/* Basic Information */}
@@ -654,7 +654,7 @@ export default function TeamSheet({ team, onSave, onCancel }: TeamSheetProps) { )}
-
+

Teams organize users with shared budgets and access

-
+
Create teams to group users, assign customer accounts, and set budgets and rate limits at the team level.
diff --git a/ui/app/workspace/governance/views/teamsTable.tsx b/ui/app/workspace/governance/views/teamsTable.tsx index 667a883bff9..3267ef83dc8 100644 --- a/ui/app/workspace/governance/views/teamsTable.tsx +++ b/ui/app/workspace/governance/views/teamsTable.tsx @@ -220,7 +220,7 @@ export default function TeamsTable({ {showTeamSheet && }
-
+

Teams

Organize users into teams with shared budgets and access controls.

diff --git a/ui/app/workspace/guardrails/configuration/page.tsx b/ui/app/workspace/guardrails/configuration/page.tsx index 4f514e08c76..0ef8f6185d2 100644 --- a/ui/app/workspace/guardrails/configuration/page.tsx +++ b/ui/app/workspace/guardrails/configuration/page.tsx @@ -2,8 +2,8 @@ import GuardrailsConfigurationView from "@enterprise/components/guardrails/guard export default function GuardrailsConfigurationPage() { return ( -
+
); -} \ No newline at end of file +} diff --git a/ui/app/workspace/guardrails/providers/page.tsx b/ui/app/workspace/guardrails/providers/page.tsx index 591b7479f25..ca34a416f67 100644 --- a/ui/app/workspace/guardrails/providers/page.tsx +++ b/ui/app/workspace/guardrails/providers/page.tsx @@ -6,4 +6,4 @@ export default function GuardrailsProvidersPage() {
); -} \ No newline at end of file +} diff --git a/ui/app/workspace/logs/sheets/logDetailView.tsx b/ui/app/workspace/logs/sheets/logDetailView.tsx index de01c180499..f3f68dd1f43 100644 --- a/ui/app/workspace/logs/sheets/logDetailView.tsx +++ b/ui/app/workspace/logs/sheets/logDetailView.tsx @@ -902,7 +902,7 @@ export function LogDetailView({ {log.provider}
-
+
-
+
-
+
-
+
-
+
@@ -1513,7 +1513,7 @@ export function LogDetailView({
-
+
{reasoning.effort && (
-
+
{log.cache_debug.cache_hit ? ( <>
-
+
{Object.entries(log.metadata) .filter(([key]) => { if (key === "isAsyncRequest") return false; @@ -2464,7 +2464,7 @@ export function LogDetailView({ ) : null} {log.params?.instructions && ( log.params?.instructions || ""}> -
+
{log.params.instructions}
@@ -2482,7 +2482,7 @@ export function LogDetailView({ title={`Attempt Trail (${log.attempt_trail.length} attempts)`} onCopy={() => JSON.stringify(log.attempt_trail, null, 2)} > -
+
@@ -2702,4 +2702,4 @@ const copyRequestBody = async (log: LogEntry, copy: (text: string) => Promise - + {!isFullDataReady ? (
Loading log details @@ -114,4 +114,4 @@ export function LogDetailSheet({ ); -} \ No newline at end of file +} diff --git a/ui/app/workspace/logs/sheets/observabilityConfigSheet.tsx b/ui/app/workspace/logs/sheets/observabilityConfigSheet.tsx index 569b363e349..cf71bd77e60 100644 --- a/ui/app/workspace/logs/sheets/observabilityConfigSheet.tsx +++ b/ui/app/workspace/logs/sheets/observabilityConfigSheet.tsx @@ -9,14 +9,14 @@ interface ObservabilityConfigSheetProps { export function ObservabilityConfigSheet({ open, onOpenChange }: ObservabilityConfigSheetProps) { return ( - + Observability settings -
+
); -} \ No newline at end of file +} diff --git a/ui/app/workspace/logs/sheets/observabilitySettingsSheet.tsx b/ui/app/workspace/logs/sheets/observabilitySettingsSheet.tsx index 8e6614099eb..9290d71e21f 100644 --- a/ui/app/workspace/logs/sheets/observabilitySettingsSheet.tsx +++ b/ui/app/workspace/logs/sheets/observabilitySettingsSheet.tsx @@ -9,7 +9,7 @@ interface ObservabilitySettingsSheetProps { export function ObservabilitySettingsSheet({ open, onOpenChange }: ObservabilitySettingsSheetProps) { return ( - + Logging settings @@ -19,4 +19,4 @@ export function ObservabilitySettingsSheet({ open, onOpenChange }: Observability ); -} \ No newline at end of file +} diff --git a/ui/app/workspace/logs/sheets/sessionDetailsSheet.tsx b/ui/app/workspace/logs/sheets/sessionDetailsSheet.tsx index dd3699c8963..ef78d66347b 100644 --- a/ui/app/workspace/logs/sheets/sessionDetailsSheet.tsx +++ b/ui/app/workspace/logs/sheets/sessionDetailsSheet.tsx @@ -181,7 +181,7 @@ export function SessionDetailsSheet({ return ( - +
Session
@@ -324,4 +324,4 @@ export function SessionDetailsSheet({ ); -} \ No newline at end of file +} diff --git a/ui/app/workspace/logs/views/columns.tsx b/ui/app/workspace/logs/views/columns.tsx index 9b73f614b68..7ff5a0c63d0 100644 --- a/ui/app/workspace/logs/views/columns.tsx +++ b/ui/app/workspace/logs/views/columns.tsx @@ -590,4 +590,4 @@ export const createColumns = ( : []; return [...expandColumn, ...baseColumns, ...attributionColumns, ...metadataColumns, ...actionsColumn]; -}; \ No newline at end of file +}; diff --git a/ui/app/workspace/logs/views/emptyState.tsx b/ui/app/workspace/logs/views/emptyState.tsx index daa3e4b2dde..77e1addb16a 100644 --- a/ui/app/workspace/logs/views/emptyState.tsx +++ b/ui/app/workspace/logs/views/emptyState.tsx @@ -259,7 +259,7 @@ const result = await chain.invoke({ input: "What is LangChain?" });`,
- + cURL OpenAI SDK Anthropic SDK diff --git a/ui/app/workspace/logs/views/logsHeaderView.tsx b/ui/app/workspace/logs/views/logsHeaderView.tsx index 52615fdcfe3..590e04789f7 100644 --- a/ui/app/workspace/logs/views/logsHeaderView.tsx +++ b/ui/app/workspace/logs/views/logsHeaderView.tsx @@ -262,7 +262,7 @@ export function LogsHeaderView({ ); return ( -
+
-
+
- +
Loading MCP log details @@ -148,7 +148,7 @@ export function MCPLogDetailSheet({ return ( - +
@@ -244,10 +244,10 @@ export function MCPLogDetailSheet({ -
+
-
+
-
+
-
Arguments
+
Arguments
-
Result
+
Result
0 && ( -
+
-
+
{Object.entries(displayLog.metadata).map(([key, value]) => ( ))} @@ -411,7 +411,7 @@ export function MCPLogDetailSheet({ {/* Error Details */} {displayedErrorDetails && (
-
Error Details
+
Error Details
); -} \ No newline at end of file +} diff --git a/ui/app/workspace/mcp-registry/library/page.tsx b/ui/app/workspace/mcp-registry/library/page.tsx index b67b29b516d..b9c9be34fee 100644 --- a/ui/app/workspace/mcp-registry/library/page.tsx +++ b/ui/app/workspace/mcp-registry/library/page.tsx @@ -154,14 +154,14 @@ export default function MCPLibraryPage() { const isCatalogEmpty = !isFetching && totalCount === 0 && !debouncedSearch && !hasActiveFilters; return ( -
-
+
+
{/* Sidebar Filters */} {/* Main Content */} -
-
+
+
{/* Header */}
@@ -233,11 +233,14 @@ export default function MCPLibraryPage() {
)} -
+
{/* Loading skeletons */} {isFetching && servers.length === 0 ? ( viewMode === "grid" ? ( - +
{Array.from({ length: 6 }).map((_, i) => ( // biome-ignore lint/suspicious/noArrayIndexKey: static skeleton placeholders have no stable id @@ -260,7 +263,7 @@ export default function MCPLibraryPage() {

{isCatalogEmpty ? "No synced servers yet" : "No servers found"}

-
+
{isCatalogEmpty ? "Configure the library sync source in Settings to populate this catalog." : "Try adjusting your search or filters."} @@ -278,7 +281,10 @@ export default function MCPLibraryPage() { ) : ( <> {viewMode === "grid" ? ( - +
{servers.map((server) => { const isInstalled = installedServerSlugs.has(server.slug); @@ -368,4 +374,4 @@ export default function MCPLibraryPage() { setAddServerOpen(false)} />
); -} \ No newline at end of file +} diff --git a/ui/app/workspace/mcp-registry/library/views/mcpLibraryAddServerSheet.tsx b/ui/app/workspace/mcp-registry/library/views/mcpLibraryAddServerSheet.tsx index 9050112bd0b..4893a2904bb 100644 --- a/ui/app/workspace/mcp-registry/library/views/mcpLibraryAddServerSheet.tsx +++ b/ui/app/workspace/mcp-registry/library/views/mcpLibraryAddServerSheet.tsx @@ -115,13 +115,13 @@ export function MCPLibraryAddServerSheet({ open, onClose }: MCPLibraryAddServerS return ( !sheetOpen && onClose()}> - + Add MCP Server This MCP server will be available org-wide for members to discover, install, and use. -
+
{/* Name */}
@@ -219,7 +219,7 @@ export function MCPLibraryAddServerSheet({ open, onClose }: MCPLibraryAddServerS )} {/* Auth + category */} -
+
@@ -276,7 +276,7 @@ export function MCPLibraryAddServerSheet({ open, onClose }: MCPLibraryAddServerS
-
+
+ ); } return ( -
+
Filters
@@ -308,4 +317,4 @@ function CheckboxFilterSection({ {filtered.length === 0 &&
No results
} ); -} \ No newline at end of file +} diff --git a/ui/app/workspace/mcp-registry/library/views/mcpLibraryInstallSheet.tsx b/ui/app/workspace/mcp-registry/library/views/mcpLibraryInstallSheet.tsx index f1a45e5136f..0a90176d60b 100644 --- a/ui/app/workspace/mcp-registry/library/views/mcpLibraryInstallSheet.tsx +++ b/ui/app/workspace/mcp-registry/library/views/mcpLibraryInstallSheet.tsx @@ -392,14 +392,14 @@ export function MCPLibraryInstallSheet({ server, open, onClose, onInstalled }: M return ( !sheetOpen && !oauthFlow && !headersFlow && onClose()}> - + Install MCP server Confirm the catalog configuration before adding this server to Bifrost. -
+
@@ -840,7 +840,7 @@ export function MCPLibraryInstallSheet({ server, open, onClose, onInstalled }: M
-
+

{isOauth diff --git a/ui/app/workspace/mcp-registry/library/views/mcpLibraryServersTable.tsx b/ui/app/workspace/mcp-registry/library/views/mcpLibraryServersTable.tsx index 8aafd3f7281..c245c990637 100644 --- a/ui/app/workspace/mcp-registry/library/views/mcpLibraryServersTable.tsx +++ b/ui/app/workspace/mcp-registry/library/views/mcpLibraryServersTable.tsx @@ -42,8 +42,8 @@ export function MCPLibraryServersTable({ }; return ( -

-
+
+
Icon @@ -189,8 +189,8 @@ export function MCPLibraryServersTable({ /** Skeleton placeholder mirroring the table layout while the library catalog loads. */ export function MCPLibraryServersTableSkeleton({ rows = 8 }: { rows?: number }) { return ( -
-
+
+
Icon @@ -226,4 +226,4 @@ export function MCPLibraryServersTableSkeleton({ rows = 8 }: { rows?: number })
); -} \ No newline at end of file +} diff --git a/ui/app/workspace/mcp-registry/library/views/mcpLibrarySettingsSheet.tsx b/ui/app/workspace/mcp-registry/library/views/mcpLibrarySettingsSheet.tsx index 5adec1ff71e..72e725d8d63 100644 --- a/ui/app/workspace/mcp-registry/library/views/mcpLibrarySettingsSheet.tsx +++ b/ui/app/workspace/mcp-registry/library/views/mcpLibrarySettingsSheet.tsx @@ -102,13 +102,13 @@ export function MCPLibrarySettingsSheet({ open, onClose }: MCPLibrarySettingsShe return ( !sheetOpen && onClose()}> - + MCP Library Settings Configure the sync source and interval for the MCP server catalog. -
+
@@ -145,7 +145,7 @@ export function MCPLibrarySettingsSheet({ open, onClose }: MCPLibrarySettingsShe
-
+
@@ -1069,4 +1069,4 @@ const ClientForm: React.FC = ({ open, onClose, onSaved }) => { ); }; -export default ClientForm; \ No newline at end of file +export default ClientForm; diff --git a/ui/app/workspace/mcp-registry/views/mcpClientSheet.tsx b/ui/app/workspace/mcp-registry/views/mcpClientSheet.tsx index b14359d7413..d8560bc3c68 100644 --- a/ui/app/workspace/mcp-registry/views/mcpClientSheet.tsx +++ b/ui/app/workspace/mcp-registry/views/mcpClientSheet.tsx @@ -602,7 +602,7 @@ export default function MCPClientSheet({ <> !open && onClose()}> - +
@@ -635,7 +635,7 @@ export default function MCPClientSheet({ -
+
@@ -1714,7 +1714,7 @@ export default function MCPClientSheet({
-
+
@@ -1768,4 +1768,4 @@ export default function MCPClientSheet({ ); -} \ No newline at end of file +} diff --git a/ui/app/workspace/mcp-registry/views/mcpClientsFilterSidebar.tsx b/ui/app/workspace/mcp-registry/views/mcpClientsFilterSidebar.tsx index 25ebe0b6224..198c349d9ef 100644 --- a/ui/app/workspace/mcp-registry/views/mcpClientsFilterSidebar.tsx +++ b/ui/app/workspace/mcp-registry/views/mcpClientsFilterSidebar.tsx @@ -3,9 +3,10 @@ import { Checkbox } from "@/components/ui/checkbox"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { Input } from "@/components/ui/input"; import { ScrollArea } from "@/components/ui/scrollArea"; +import { useIsMobile } from "@/hooks/use-mobile"; import { useGetVirtualKeysQuery } from "@/lib/store"; import { cn } from "@/lib/utils"; -import { ChevronDown, LoaderCircle, PanelLeftClose, PanelLeftOpen, RotateCcw, Search } from "lucide-react"; +import { ChevronDown, Filter, LoaderCircle, PanelLeftClose, PanelLeftOpen, RotateCcw, Search } from "lucide-react"; import { type Ref, useCallback, useEffect, useMemo, useRef, useState } from "react"; const COLLAPSE_STORAGE_KEY = "mcp-clients-filter-sidebar-collapsed"; @@ -89,13 +90,18 @@ interface SidebarProps { // --------------------------------------------------------------------------- export function MCPClientsFilterSidebar({ filters, onFiltersChange }: SidebarProps) { + const isMobile = useIsMobile(); const [collapsed, setCollapsed] = useState(false); useEffect(() => { if (typeof window === "undefined") return; + if (isMobile) { + setCollapsed(true); + return; + } const stored = window.localStorage.getItem(COLLAPSE_STORAGE_KEY); - if (stored === "true") setCollapsed(true); - }, []); + setCollapsed(stored === "true"); + }, [isMobile]); const toggleCollapsed = useCallback(() => { setCollapsed((prev) => { @@ -125,27 +131,30 @@ export function MCPClientsFilterSidebar({ filters, onFiltersChange }: SidebarPro if (collapsed) { return ( - + ); } return ( -
+
Filters
diff --git a/ui/app/workspace/mcp-registry/views/mcpClientsTable.tsx b/ui/app/workspace/mcp-registry/views/mcpClientsTable.tsx index 0b82351ee12..dd5ab32b2f6 100644 --- a/ui/app/workspace/mcp-registry/views/mcpClientsTable.tsx +++ b/ui/app/workspace/mcp-registry/views/mcpClientsTable.tsx @@ -765,7 +765,7 @@ export default function MCPClientsTable({ -
+

MCP Server Catalog

Manage servers that can connect to the MCP Tools endpoint.

@@ -1187,4 +1187,4 @@ function HeaderWithTooltip({ label, tooltip }: { label: string; tooltip: ReactNo ); -} \ No newline at end of file +} diff --git a/ui/app/workspace/mcp-registry/views/mcpServersEmptyState.tsx b/ui/app/workspace/mcp-registry/views/mcpServersEmptyState.tsx index c5de321beea..fbcbcea0c11 100644 --- a/ui/app/workspace/mcp-registry/views/mcpServersEmptyState.tsx +++ b/ui/app/workspace/mcp-registry/views/mcpServersEmptyState.tsx @@ -17,7 +17,7 @@ export function MCPServersEmptyState({ onAddClick, canCreate = true }: MCPServer

MCP servers connect tools and context to the gateway

-
+
Add MCP servers to expose tools and resources to the MCP Tools endpoint. Configure connection type, auth, and which tools to enable.
diff --git a/ui/app/workspace/mcp-registry/views/mcpUsageGuide/mcpUsageGuideSheet.tsx b/ui/app/workspace/mcp-registry/views/mcpUsageGuide/mcpUsageGuideSheet.tsx index d0225cd4eae..43035291181 100644 --- a/ui/app/workspace/mcp-registry/views/mcpUsageGuide/mcpUsageGuideSheet.tsx +++ b/ui/app/workspace/mcp-registry/views/mcpUsageGuide/mcpUsageGuideSheet.tsx @@ -131,7 +131,7 @@ export function MCPUsageGuideSheet() { - +
Install Bifrost MCP @@ -140,7 +140,7 @@ export function MCPUsageGuideSheet() {
-
+
{/* ── Harness selector tabs ───────────────────────── */}
@@ -290,4 +290,4 @@ export function MCPUsageGuideSheet() { ); -} \ No newline at end of file +} diff --git a/ui/app/workspace/mcp-registry/views/tokenExchangeFields.tsx b/ui/app/workspace/mcp-registry/views/tokenExchangeFields.tsx index 9b2a34ab054..66307cf82fd 100644 --- a/ui/app/workspace/mcp-registry/views/tokenExchangeFields.tsx +++ b/ui/app/workspace/mcp-registry/views/tokenExchangeFields.tsx @@ -337,4 +337,4 @@ export function TokenExchangeFields({
); -} \ No newline at end of file +} diff --git a/ui/app/workspace/mcp-sessions/views/mcpSessionsFilterSidebar.tsx b/ui/app/workspace/mcp-sessions/views/mcpSessionsFilterSidebar.tsx index af85bbcd6b6..8b138a36577 100644 --- a/ui/app/workspace/mcp-sessions/views/mcpSessionsFilterSidebar.tsx +++ b/ui/app/workspace/mcp-sessions/views/mcpSessionsFilterSidebar.tsx @@ -9,10 +9,22 @@ import { Checkbox } from "@/components/ui/checkbox"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { Input } from "@/components/ui/input"; import { ScrollArea } from "@/components/ui/scrollArea"; +import { useIsMobile } from "@/hooks/use-mobile"; import { getUserSearchQuery } from "@/lib/registries/userPicker"; import { useGetMCPClientsQuery, useGetVirtualKeysQuery } from "@/lib/store"; import { cn } from "@/lib/utils"; -import { ChevronDown, Fingerprint, KeyRound, LoaderCircle, PanelLeftClose, PanelLeftOpen, RotateCcw, Search, UserRound } from "lucide-react"; +import { + ChevronDown, + Filter, + Fingerprint, + KeyRound, + LoaderCircle, + PanelLeftClose, + PanelLeftOpen, + RotateCcw, + Search, + UserRound, +} from "lucide-react"; import { type Ref, useCallback, useEffect, useMemo, useRef, useState } from "react"; // Side-effect import: registers the enterprise user search hook (if this is // an enterprise build) before this module's first render. OSS has no user @@ -86,13 +98,18 @@ interface SidebarProps { // --------------------------------------------------------------------------- export function MCPSessionsFilterSidebar({ filters, onFiltersChange }: SidebarProps) { + const isMobile = useIsMobile(); const [collapsed, setCollapsed] = useState(false); useEffect(() => { if (typeof window === "undefined") return; + if (isMobile) { + setCollapsed(true); + return; + } const stored = window.localStorage.getItem(COLLAPSE_STORAGE_KEY); - if (stored === "true") setCollapsed(true); - }, []); + setCollapsed(stored === "true"); + }, [isMobile]); const toggleCollapsed = useCallback(() => { setCollapsed((prev) => { @@ -148,27 +165,30 @@ export function MCPSessionsFilterSidebar({ filters, onFiltersChange }: SidebarPr if (collapsed) { return ( - + ); } return ( -
+
Filters
diff --git a/ui/app/workspace/mcp-settings/page.tsx b/ui/app/workspace/mcp-settings/page.tsx index c0f75e6dba0..2cb50f11f49 100644 --- a/ui/app/workspace/mcp-settings/page.tsx +++ b/ui/app/workspace/mcp-settings/page.tsx @@ -2,4 +2,4 @@ import MCPView from "../config/views/mcpView"; export default function MCPSettingsPage() { return
; -} \ No newline at end of file +} diff --git a/ui/app/workspace/mcp-tool-groups/page.tsx b/ui/app/workspace/mcp-tool-groups/page.tsx index 709bf7d5f31..deef7a4efba 100644 --- a/ui/app/workspace/mcp-tool-groups/page.tsx +++ b/ui/app/workspace/mcp-tool-groups/page.tsx @@ -2,8 +2,8 @@ import MCPToolGroups from "@enterprise/components/mcp-tool-groups/mcpToolGroups" export default function MCPToolGroupsPage() { return ( -
+
); -} \ No newline at end of file +} diff --git a/ui/app/workspace/model-catalog/views/attributeSheet.tsx b/ui/app/workspace/model-catalog/views/attributeSheet.tsx index 05e1fb68b69..fc3ecc7b717 100644 --- a/ui/app/workspace/model-catalog/views/attributeSheet.tsx +++ b/ui/app/workspace/model-catalog/views/attributeSheet.tsx @@ -197,7 +197,7 @@ export default function AttributeSheet({ model, overrides, onClose }: AttributeS }} data-testid="model-catalog-attribute-sheet" > - + Edit Model Attributes Update the description and other attributes for this model. These attributes are stored on the pricing row and preserved across @@ -206,9 +206,9 @@ export default function AttributeSheet({ model, overrides, onClose }: AttributeS
-
+
{/* Read-only provider / model header */} -
+
@@ -245,7 +245,7 @@ export default function AttributeSheet({ model, overrides, onClose }: AttributeS )}
-
+

Input

@@ -417,7 +417,7 @@ export default function AttributeSheet({ model, overrides, onClose }: AttributeS

-
+
{!hasUpdateAccess &&

You don't have permission to perform this action

} @@ -697,7 +697,7 @@ function TargetRow({ target, index, providerOptions, allKeys, showRemove, onUpda
-
+
); -} \ No newline at end of file +} diff --git a/ui/app/workspace/routing-rules/views/routingRulesEmptyState.tsx b/ui/app/workspace/routing-rules/views/routingRulesEmptyState.tsx index 29d4f2232ff..5795650d68d 100644 --- a/ui/app/workspace/routing-rules/views/routingRulesEmptyState.tsx +++ b/ui/app/workspace/routing-rules/views/routingRulesEmptyState.tsx @@ -20,7 +20,7 @@ export function RoutingRulesEmptyState({ onAddClick, canCreate = true }: Routing

Routing rules direct requests using CEL conditions

-
+
Create CEL-based rules to route requests by model, provider, budget, or custom attributes. Control which provider or model handles each request.
diff --git a/ui/app/workspace/routing-rules/views/routingRulesView.tsx b/ui/app/workspace/routing-rules/views/routingRulesView.tsx index 9f6cc61e3e4..4297a929324 100644 --- a/ui/app/workspace/routing-rules/views/routingRulesView.tsx +++ b/ui/app/workspace/routing-rules/views/routingRulesView.tsx @@ -115,7 +115,7 @@ export function RoutingRulesView() { return (
{/* Header */} -
+

Routing Rules

Manage CEL-based routing rules for intelligent request routing across providers

diff --git a/ui/app/workspace/scim/page.tsx b/ui/app/workspace/scim/page.tsx index 67c3d0a8440..47168e2b16a 100644 --- a/ui/app/workspace/scim/page.tsx +++ b/ui/app/workspace/scim/page.tsx @@ -2,10 +2,10 @@ import SCIMView from "@enterprise/components/scim/scimView"; export default function SCIMPage() { return ( -
-
+
+
); -} \ No newline at end of file +} diff --git a/ui/app/workspace/skills-repo/components/shared.tsx b/ui/app/workspace/skills-repo/components/shared.tsx index ff7375bc3c3..4e1b04f71d5 100644 --- a/ui/app/workspace/skills-repo/components/shared.tsx +++ b/ui/app/workspace/skills-repo/components/shared.tsx @@ -12,6 +12,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { Tree, type BaseNodeData, type TreeNode } from "@/components/ui/treeView"; import { useCopyToClipboard } from "@/hooks/useCopyToClipboard"; +import { useIsMobile } from "@/hooks/use-mobile"; import { SkillFileEntry } from "@/lib/types/skills"; import { cn } from "@/lib/utils"; import { getApiBaseUrl } from "@/lib/utils/port"; @@ -175,8 +176,13 @@ export function SkillHeader({ return ( <> -
-
+
+
{onBack ? (
-
+
{decorators} {(actions || downloadSkillName) && ( -
+
{downloadSkillName && ( - +

Copy CLI command to register this repository

@@ -373,7 +373,7 @@ export function SkillsListView({ // True empty state: no skills at all (not just filtered to zero) if (total === 0 && !search && !debouncedSearch && !isFetching) { return ( -
+
@@ -406,26 +406,26 @@ export function SkillsListView({ } return ( -
+
{/* Header */} -
-
+
+

Skills Repository

Beta

Manage Agent Skills for distribution to AI coding assistants

-
+
{isGitAvailable ? ( ) : ( - @@ -462,22 +462,24 @@ export function SkillsListView({ } }} disabled={!skills?.length || isDownloadingAll} + title="Download all skills" + aria-label="Download all skills" > {isDownloadingAll ? : } - {isDownloadingAll ? "Downloading..." : "Download All Skills"} + {isDownloadingAll ? "Downloading..." : "Download All Skills"} {hasCreateAccess && ( - )}
{/* Search + All-skills version */} -
-
+
+
-
+
@@ -544,8 +546,8 @@ export function SkillsListView({
{/* Table */} -
- +
+
@@ -640,7 +642,7 @@ export function SkillsListView({ {/* Pagination */} {total > 0 && ( -
+
{(offset + 1).toLocaleString()}-{Math.min(offset + PAGE_SIZE, total).toLocaleString()} of {total.toLocaleString()} entries
@@ -675,4 +677,4 @@ export function SkillsListView({ )}
); -} \ No newline at end of file +} diff --git a/ui/app/workspace/skills-repo/forms/skillEditForm.tsx b/ui/app/workspace/skills-repo/forms/skillEditForm.tsx index c0ce118d9f6..b737aec205c 100644 --- a/ui/app/workspace/skills-repo/forms/skillEditForm.tsx +++ b/ui/app/workspace/skills-repo/forms/skillEditForm.tsx @@ -12,6 +12,7 @@ import { ScrollArea, ScrollBar } from "@/components/ui/scrollArea"; import { Textarea } from "@/components/ui/textarea"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { useCopyToClipboard } from "@/hooks/useCopyToClipboard"; +import { useIsMobile } from "@/hooks/use-mobile"; import type { SkillFileEntry } from "@/lib/types/skills"; import { cn } from "@/lib/utils"; import { validateSkillForm, validateVersionBump } from "@/lib/validators/skills"; @@ -45,6 +46,7 @@ export function SkillEditView({ isSaving: boolean; mode?: "edit" | "create"; }) { + const isMobile = useIsMobile(); const isCreate = mode === "create"; const [bodyTab, setBodyTab] = useState<"edit" | "preview">("edit"); const [showPreviewDialog, setShowPreviewDialog] = useState(false); @@ -255,7 +257,7 @@ export function SkillEditView({ {/* Files + SKILL.md two-pane workspace */}
- + {/* Left: files panel */}

Details

@@ -316,7 +318,7 @@ export function SkillEditView({
- + {/* Right: editor for the selected item */} @@ -385,7 +387,7 @@ export function SkillEditView({ > Preview - + Use @ to reference files
@@ -448,26 +450,28 @@ export function SkillEditView({
)} -
+
- {isCreate ? ( !open && closeVersionPopover()}> - @@ -495,9 +499,10 @@ export function SkillEditView({ data-testid="skill-save-btn" onClick={() => openVersionPopover(false)} disabled={isSaving} + aria-label="Save" > {isSaving ? : } - {isSaving ? "Saving..." : "Save"} + {isSaving ? "Saving..." : "Save"} @@ -517,9 +522,9 @@ export function SkillEditView({ !open && closeVersionPopover()}> - @@ -652,7 +657,7 @@ function DetailsEditorPane({ -
+
); -} \ No newline at end of file +} diff --git a/ui/app/workspace/skills-repo/page.tsx b/ui/app/workspace/skills-repo/page.tsx index 21f1deb20b2..77e4c20bd90 100644 --- a/ui/app/workspace/skills-repo/page.tsx +++ b/ui/app/workspace/skills-repo/page.tsx @@ -33,7 +33,7 @@ export default function SkillsRepoPage() { // Create view if (urlState.create) { return ( -
+
); @@ -44,7 +44,9 @@ export default function SkillsRepoPage() { return (
@@ -54,8 +56,8 @@ export default function SkillsRepoPage() { // List view return ( -
+
setUrlState({ create: true, skillId: null, edit: false })} />
); -} \ No newline at end of file +} diff --git a/ui/app/workspace/virtual-keys/views/virtualKeyDetailsSheet.tsx b/ui/app/workspace/virtual-keys/views/virtualKeyDetailsSheet.tsx index c823cfeac84..c377a1b5f22 100644 --- a/ui/app/workspace/virtual-keys/views/virtualKeyDetailsSheet.tsx +++ b/ui/app/workspace/virtual-keys/views/virtualKeyDetailsSheet.tsx @@ -136,7 +136,7 @@ export default function VirtualKeyDetailSheet({
@@ -155,7 +155,7 @@ export default function VirtualKeyDetailSheet({ /> -
+
{assignedUsers.length > 0 ? ( @@ -173,7 +173,7 @@ export default function VirtualKeyDetailSheet({

Basic Information

-
+
Status
{(() => { @@ -186,7 +186,7 @@ export default function VirtualKeyDetailSheet({
{virtualKey.expires_at && ( -
+
Expires
{formatDistanceToNow(new Date(virtualKey.expires_at), { @@ -197,7 +197,7 @@ export default function VirtualKeyDetailSheet({
)} -
+
Created
{formatDistanceToNow(new Date(virtualKey.created_at), { @@ -206,7 +206,7 @@ export default function VirtualKeyDetailSheet({
-
+
Last Updated
{formatDistanceToNow(new Date(virtualKey.updated_at), { @@ -216,7 +216,7 @@ export default function VirtualKeyDetailSheet({
{entityInfo.type !== "None" && ( -
+
Assigned To
{entityInfo.type} @@ -264,7 +264,7 @@ export default function VirtualKeyDetailSheet({ {/* Basic Config */}
-
+
Allowed Models
{config.allowed_models?.includes("*") ? ( @@ -287,7 +287,7 @@ export default function VirtualKeyDetailSheet({
-
+
Blocked Models
{config.blacklisted_models?.includes("*") ? ( @@ -310,7 +310,7 @@ export default function VirtualKeyDetailSheet({
-
+
Allowed Keys
{config.allow_all_keys ? ( @@ -701,4 +701,4 @@ export default function VirtualKeyDetailSheet({ ); -} \ No newline at end of file +} diff --git a/ui/app/workspace/virtual-keys/views/virtualKeySheet.tsx b/ui/app/workspace/virtual-keys/views/virtualKeySheet.tsx index dd0964c7660..4c1c9cc4a8f 100644 --- a/ui/app/workspace/virtual-keys/views/virtualKeySheet.tsx +++ b/ui/app/workspace/virtual-keys/views/virtualKeySheet.tsx @@ -1054,7 +1054,7 @@ export default function VirtualKeySheet({ virtualKey, defaultTeamId, onSave, onC onInteractOutside={(e) => e.preventDefault()} onEscapeKeyDown={() => handleClose()} > - + {isEditing ? virtualKey?.name : "Create Virtual Key"} {isEditing @@ -1065,7 +1065,7 @@ export default function VirtualKeySheet({ virtualKey, defaultTeamId, onSave, onC -
+
{isManagedByProfile && ( <> @@ -1906,12 +1906,12 @@ export default function VirtualKeySheet({ virtualKey, defaultTeamId, onSave, onC {isEditing && virtualKey?.config_hash && ( -
+
)} {/* Form Footer */} -
+
{isEditing ? (

Virtual keys control access, budgets, and rate limits

-
+
Create virtual keys to assign permissions, spending limits, and usage quotas to teams, customers, or API clients.
diff --git a/ui/app/workspace/virtual-keys/views/virtualKeysTable.tsx b/ui/app/workspace/virtual-keys/views/virtualKeysTable.tsx index 714a9d15cac..b342f58adfd 100644 --- a/ui/app/workspace/virtual-keys/views/virtualKeysTable.tsx +++ b/ui/app/workspace/virtual-keys/views/virtualKeysTable.tsx @@ -674,7 +674,7 @@ export default function VirtualKeysTable({
-
+

Get notified when async jobs finish

-
+
Register webhook endpoints and Bifrost will send a signed notification whenever an async inference job completes or fails, so you don't have to poll for results.
diff --git a/ui/components/filters/logsFilterSidebar.tsx b/ui/components/filters/logsFilterSidebar.tsx index a04ea58b11d..3a145d2f29d 100644 --- a/ui/components/filters/logsFilterSidebar.tsx +++ b/ui/components/filters/logsFilterSidebar.tsx @@ -5,11 +5,12 @@ import { Input } from "@/components/ui/input"; import { ScrollArea } from "@/components/ui/scrollArea"; import { Skeleton } from "@/components/ui/skeleton"; import { TruncatedLabel } from "@/components/ui/truncatedLabel"; +import { useIsMobile } from "@/hooks/use-mobile"; import { RequestTypeLabels, RequestTypes, RoutingEngineUsedLabels, Statuses } from "@/lib/constants/logs"; import { useGetAvailableFilterDataQuery, useGetProvidersQuery } from "@/lib/store"; import type { LogFilters } from "@/lib/types/logs"; import { cn } from "@/lib/utils"; -import { ChevronDown, LoaderCircle, PanelLeftClose, PanelLeftOpen, Plus, RotateCcw, Search } from "lucide-react"; +import { ChevronDown, Filter, LoaderCircle, PanelLeftClose, PanelLeftOpen, Plus, RotateCcw, Search } from "lucide-react"; import { Ref, useCallback, useEffect, useMemo, useRef, useState } from "react"; const COLLAPSE_STORAGE_KEY = "logs-filter-sidebar-collapsed"; @@ -24,14 +25,19 @@ interface LogsSidebarProps { } export function LogsFilterSidebar({ filters, onFiltersChange }: LogsSidebarProps) { + const isMobile = useIsMobile(); const [collapsed, setCollapsed] = useState(false); // Load persisted collapsed state on mount useEffect(() => { if (typeof window === "undefined") return; + if (isMobile) { + setCollapsed(true); + return; + } const stored = window.localStorage.getItem(COLLAPSE_STORAGE_KEY); - if (stored === "true") setCollapsed(true); - }, []); + setCollapsed(stored === "true"); + }, [isMobile]); const toggleCollapsed = useCallback(() => { setCollapsed((prev) => { @@ -66,26 +72,29 @@ export function LogsFilterSidebar({ filters, onFiltersChange }: LogsSidebarProps // Collapsed: thin rail with vertical "Filters" label — whole rail is clickable to expand if (collapsed) { return ( - + ); } return ( -
+
{/* Header */}
Filters @@ -332,7 +341,14 @@ function SearchableCheckboxList({ onCheckedChange={() => onToggle(item.key)} testId={ testIdPrefix - ? `${testIdPrefix}-checkbox-${normalizeTestIdKey ? item.key.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") : item.key}` + ? `${testIdPrefix}-checkbox-${ + normalizeTestIdKey + ? item.key + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+|-+$/g, "") + : item.key + }` : undefined } /> @@ -448,7 +464,10 @@ function AppFilter({ filters, onFiltersChange, defaultOpen }: FilterComponentPro isLoading, } = useGetAvailableFilterDataQuery({ dimensions: ["apps"] }, { skip: !opened && !hasActive }); const availableApps = useMemo(() => (filterData?.apps as string[] | undefined) || [], [filterData]); - const items = useMemo(() => [...new Set([...availableApps, ...(filters.apps || [])])].sort().map((name) => ({ key: name, label: name })), [availableApps, filters.apps]); + const items = useMemo( + () => [...new Set([...availableApps, ...(filters.apps || [])])].sort().map((name) => ({ key: name, label: name })), + [availableApps, filters.apps], + ); if (!isUninitialized && !isLoading && availableApps.length === 0 && !hasActive && !opened) return null; diff --git a/ui/components/filters/mcpFilterSidebar.tsx b/ui/components/filters/mcpFilterSidebar.tsx index 2cedefd86fb..ffa6f32c626 100644 --- a/ui/components/filters/mcpFilterSidebar.tsx +++ b/ui/components/filters/mcpFilterSidebar.tsx @@ -5,11 +5,12 @@ import { Input } from "@/components/ui/input"; import { ScrollArea } from "@/components/ui/scrollArea"; import { Skeleton } from "@/components/ui/skeleton"; import { TruncatedLabel } from "@/components/ui/truncatedLabel"; +import { useIsMobile } from "@/hooks/use-mobile"; import { Statuses } from "@/lib/constants/logs"; import { useGetMCPLogsFilterDataQuery } from "@/lib/store"; import type { MCPToolLogFilters } from "@/lib/types/logs"; import { cn } from "@/lib/utils"; -import { ChevronDown, LoaderCircle, PanelLeftClose, PanelLeftOpen, Plus, RotateCcw, Search } from "lucide-react"; +import { ChevronDown, Filter, LoaderCircle, PanelLeftClose, PanelLeftOpen, Plus, RotateCcw, Search } from "lucide-react"; import { Ref, useCallback, useEffect, useMemo, useRef, useState } from "react"; const COLLAPSE_STORAGE_KEY = "mcp-filter-sidebar-collapsed"; @@ -24,14 +25,19 @@ interface MCPFilterSidebarProps { } export function MCPFilterSidebar({ filters, onFiltersChange }: MCPFilterSidebarProps) { + const isMobile = useIsMobile(); const [collapsed, setCollapsed] = useState(false); // Load persisted collapsed state on mount useEffect(() => { if (typeof window === "undefined") return; + if (isMobile) { + setCollapsed(true); + return; + } const stored = window.localStorage.getItem(COLLAPSE_STORAGE_KEY); - if (stored === "true") setCollapsed(true); - }, []); + setCollapsed(stored === "true"); + }, [isMobile]); const toggleCollapsed = useCallback(() => { setCollapsed((prev) => { @@ -63,26 +69,29 @@ export function MCPFilterSidebar({ filters, onFiltersChange }: MCPFilterSidebarP // Collapsed: thin rail with vertical "Filters" label — whole rail is clickable to expand if (collapsed) { return ( - + ); } return ( -
+
{/* Header */}
Filters @@ -300,7 +309,14 @@ function SearchableCheckboxList({ onCheckedChange={() => onToggle(item.key)} testId={ testIdPrefix - ? `${testIdPrefix}-checkbox-${normalizeTestIdKey ? item.key.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") : item.key}` + ? `${testIdPrefix}-checkbox-${ + normalizeTestIdKey + ? item.key + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+|-+$/g, "") + : item.key + }` : undefined } /> @@ -453,7 +469,10 @@ function AppFilter({ filters, onFiltersChange, defaultOpen }: FilterComponentPro isLoading, } = useGetMCPLogsFilterDataQuery({ dimensions: ["apps"] }, { skip: !opened && !hasActive }); const availableApps = useMemo(() => (filterData?.apps as string[] | undefined) || [], [filterData]); - const items = useMemo(() => [...new Set([...availableApps, ...(filters.apps || [])])].sort().map((name) => ({ key: name, label: name })), [availableApps, filters.apps]); + const items = useMemo( + () => [...new Set([...availableApps, ...(filters.apps || [])])].sort().map((name) => ({ key: name, label: name })), + [availableApps, filters.apps], + ); if (!isUninitialized && !isLoading && availableApps.length === 0 && !hasActive && !opened) return null; diff --git a/ui/components/header.tsx b/ui/components/header.tsx index 4dc2790d60e..fb1a8016ad1 100644 --- a/ui/components/header.tsx +++ b/ui/components/header.tsx @@ -3,7 +3,7 @@ import { Separator } from "./ui/separator"; export default function Header({ title }: { title: string }) { return ( -
+
{title}
diff --git a/ui/components/loggingDisabledView.tsx b/ui/components/loggingDisabledView.tsx index ccf4aeb0b1b..47b17019a37 100644 --- a/ui/components/loggingDisabledView.tsx +++ b/ui/components/loggingDisabledView.tsx @@ -32,7 +32,7 @@ export function LoggingDisabledView() {

Logging is disabled

-
+
Enable logging to view LLM and MCP request logs, traces, and observability data.
diff --git a/ui/components/onboardingWidget.tsx b/ui/components/onboardingWidget.tsx index 01108c29161..ad149b653a9 100644 --- a/ui/components/onboardingWidget.tsx +++ b/ui/components/onboardingWidget.tsx @@ -355,7 +355,7 @@ export default function OnboardingWidget() { ); })} - + { @@ -364,11 +364,7 @@ export default function OnboardingWidget() { }} > - diff --git a/ui/components/prompts/components/emptyState.tsx b/ui/components/prompts/components/emptyState.tsx index de1dc168a93..20d971c9af6 100644 --- a/ui/components/prompts/components/emptyState.tsx +++ b/ui/components/prompts/components/emptyState.tsx @@ -41,7 +41,7 @@ export function PromptsEmptyState() {

Build, test, and version your prompts

-
+
{canCreate ? "Create prompts, test them with different models and parameters in the playground, and version your changes for deployment." : "View prompts and test them with different models and parameters in the playground."} diff --git a/ui/components/prompts/promptsView.tsx b/ui/components/prompts/promptsView.tsx index 795f2e13cd1..e822b28b20b 100644 --- a/ui/components/prompts/promptsView.tsx +++ b/ui/components/prompts/promptsView.tsx @@ -1,6 +1,7 @@ import FullPageLoader from "@/components/fullPageLoader"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable"; +import { useIsMobile } from "@/hooks/use-mobile"; import { AlertCircle, Loader2 } from "lucide-react"; import { PromptSidebar } from "./fragments/sidebar"; import { PlaygroundPanel } from "./fragments/playgroundPanel"; @@ -12,6 +13,7 @@ import PromptsViewHeader from "./components/promptsViewHeader"; import { usePromptContext } from "./context"; export default function PromptsView() { + const isMobile = useIsMobile(); const { folders, prompts, foldersLoading, promptsLoading, foldersError, promptsError, isLoadingPlayground, selectedPromptId } = usePromptContext(); @@ -39,6 +41,42 @@ export default function PromptsView() { ); } + if (isMobile) { + return ( +
+ + + +
+ +
+
+ {selectedPromptId ? ( +
+ + {isLoadingPlayground ? ( +
+ +
+ ) : ( +
+
+ +
+
+ +
+
+ )} +
+ ) : ( + + )} +
+
+ ); + } + return (
diff --git a/ui/components/prompts/sheets/commitVersionSheet.tsx b/ui/components/prompts/sheets/commitVersionSheet.tsx index b37f04674a8..dfe8771dd0a 100644 --- a/ui/components/prompts/sheets/commitVersionSheet.tsx +++ b/ui/components/prompts/sheets/commitVersionSheet.tsx @@ -145,7 +145,7 @@ export function CommitVersionSheet({ open, onOpenChange, session, onCommitted }: return ( { e.preventDefault(); document.getElementById("commitMessage")?.focus(); @@ -222,4 +222,4 @@ export function CommitVersionSheet({ open, onOpenChange, session, onCommitted }: ); -} \ No newline at end of file +} diff --git a/ui/components/prompts/sheets/folderSheet.tsx b/ui/components/prompts/sheets/folderSheet.tsx index d0ad7b44f8f..4e4b288d735 100644 --- a/ui/components/prompts/sheets/folderSheet.tsx +++ b/ui/components/prompts/sheets/folderSheet.tsx @@ -74,7 +74,7 @@ export function FolderSheet({ open, onOpenChange, folder, onSaved }: FolderSheet return ( { e.preventDefault(); document.getElementById("name")?.focus(); @@ -128,4 +128,4 @@ export function FolderSheet({ open, onOpenChange, folder, onSaved }: FolderSheet ); -} \ No newline at end of file +} diff --git a/ui/components/prompts/sheets/promptSheet.tsx b/ui/components/prompts/sheets/promptSheet.tsx index f1b059375b0..a7cd15cb623 100644 --- a/ui/components/prompts/sheets/promptSheet.tsx +++ b/ui/components/prompts/sheets/promptSheet.tsx @@ -78,7 +78,7 @@ export function PromptSheet({ open, onOpenChange, prompt, folderId, onSaved }: P }} > - + {isEditing ? "Rename Prompt" : "Create Prompt"} {isEditing ? "Update the prompt name." : folderId ? "Create a new prompt in this folder." : "Create a new prompt."} @@ -86,7 +86,7 @@ export function PromptSheet({ open, onOpenChange, prompt, folderId, onSaved }: P
-
+
- + @@ -116,4 +116,4 @@ export function PromptSheet({ open, onOpenChange, prompt, folderId, onSaved }: P ); -} \ No newline at end of file +} diff --git a/ui/components/sidebar.tsx b/ui/components/sidebar.tsx index 91ee8850f0e..bb11b4224a8 100644 --- a/ui/components/sidebar.tsx +++ b/ui/components/sidebar.tsx @@ -557,7 +557,7 @@ export default function AppSidebar() { // Wrapper that accepts arbitrary string URLs (TanStack Router's `to` is // strictly typed, but our sidebar items come from a runtime config). const navigate = useCallback((url: string) => tsNavigate({ to: url as string }), [tsNavigate]); - const { state: sidebarState, toggleSidebar } = useSidebar(); + const { state: sidebarState, isMobile, toggleSidebar } = useSidebar(); const [mounted, setMounted] = useState(false); const [expandedItems, setExpandedItems] = useState>(new Set()); const [areCardsEmpty, setAreCardsEmpty] = useState(false); @@ -1498,7 +1498,7 @@ export default function AppSidebar() { // The promo card stack is hidden via CSS when collapsed (icon rail), so it // shouldn't reserve vertical space there — otherwise the nav icon list // gets squeezed into a shorter scroll area for a card nobody can see. - const hasPromoCards = promoCards.length > 0 && !areCardsEmpty && sidebarState !== "collapsed"; + const hasPromoCards = promoCards.length > 0 && !areCardsEmpty && (isMobile || sidebarState !== "collapsed"); // When cards are present: 13rem (header 3rem + bottom section ~10rem) // When no cards: 8rem (header 3rem + bottom section without cards ~5rem) const sidebarGroupHeight = hasPromoCards ? "h-[calc(100vh-13rem)]" : "h-[calc(100vh-8rem)]"; @@ -1649,7 +1649,7 @@ export default function AppSidebar() { onToggle={() => toggleItem(item.title)} pathname={pathname} search={search} - isSidebarCollapsed={sidebarState === "collapsed"} + isSidebarCollapsed={!isMobile && sidebarState === "collapsed"} expandSidebar={() => toggleSidebar()} highlightedUrl={highlightedUrl} /> @@ -1745,4 +1745,4 @@ export default function AppSidebar() { ); -} \ No newline at end of file +} diff --git a/ui/components/ui/custom/celBuilder/celRuleBuilder.tsx b/ui/components/ui/custom/celBuilder/celRuleBuilder.tsx index 36ee089fa5e..82ba3b24eb6 100644 --- a/ui/components/ui/custom/celBuilder/celRuleBuilder.tsx +++ b/ui/components/ui/custom/celBuilder/celRuleBuilder.tsx @@ -344,4 +344,4 @@ export function CELRuleBuilder({
); -} \ No newline at end of file +} diff --git a/ui/components/ui/datePickerWithRange.tsx b/ui/components/ui/datePickerWithRange.tsx index c8b23341456..cbbaef23d04 100644 --- a/ui/components/ui/datePickerWithRange.tsx +++ b/ui/components/ui/datePickerWithRange.tsx @@ -1,5 +1,6 @@ -import { cn } from "@/lib/utils"; import { getSupportedTimezones } from "@/lib/timezones"; +import { cn } from "@/lib/utils"; +import { useIsMobile } from "@/hooks/use-mobile"; import { TZDate, tz, tzName } from "@date-fns/tz"; import { format } from "date-fns"; import { Calendar as CalendarIcon, Globe } from "lucide-react"; @@ -68,6 +69,7 @@ interface DateTimePickerWithRangeProps extends DatePickerWithRangeProps { } export function DateTimePickerWithRange(props: DateTimePickerWithRangeProps) { + const isMobile = useIsMobile(); const { className, buttonClassName, triggerLabel, onTrigger, dateTime } = props; const activeTimezone = props.showTimezone ? props.timezone : undefined; @@ -207,8 +209,11 @@ export function DateTimePickerWithRange(props: DateTimePickerWithRangeProps) { )} - -
+ +
@@ -285,7 +290,7 @@ export function DateTimePickerWithRange(props: DateTimePickerWithRangeProps) {
{props.preDefinedPeriods && ( -
+
{props.preDefinedPeriods.map((period) => ( +
+
+ ); +} + +export function UpdatingScreen() { + const { logoSrc, logoAlt } = useBranding(false); + const [autoReloadExhausted, setAutoReloadExhausted] = useState(false); + + useEffect( + () => + startVersionPoll({ + fetchVersion: (signal) => + fetch(getEndpointUrl("/api/version"), { + cache: "no-store", + credentials: "include", + signal, + }), + onUpdateReady: () => { + if (consumeAutoReload()) { + window.location.reload(); + return; + } + setAutoReloadExhausted(true); + }, + onTimeout: () => setAutoReloadExhausted(true), + }), + [], + ); + + return ( +
+
+ ); +} \ No newline at end of file diff --git a/ui/app/clientLayout.tsx b/ui/app/clientLayout.tsx index fac20dbd1ed..c5ca570ba6a 100644 --- a/ui/app/clientLayout.tsx +++ b/ui/app/clientLayout.tsx @@ -5,6 +5,7 @@ import ProgressProvider from "@/components/progressBar"; import Sidebar from "@/components/sidebar"; import { ThemeProvider } from "@/components/themeProvider"; import TrialExpiryBanner from "@/components/trialExpiryBanner"; +import { Button } from "@/components/ui/button"; import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"; import { useStoreSync } from "@/hooks/useStoreSync"; import { WebSocketProvider } from "@/hooks/useWebSocket"; @@ -12,6 +13,7 @@ import { getErrorMessage, ReduxProvider, useGetCoreConfigQuery, useIsAuthEnabled import { BifrostConfig } from "@/lib/types/config"; import { RbacProvider, useRbacContext } from "@enterprise/lib/contexts/rbacContext"; import { useLocation, useMatches } from "@tanstack/react-router"; +import { RefreshCw, WifiOff } from "lucide-react"; import { NuqsAdapter } from "nuqs/adapters/tanstack-router"; import { lazy, Suspense, useEffect, useState } from "react"; import { CookiesProvider } from "react-cookie"; @@ -75,6 +77,8 @@ function AppContent({ children }: { children: React.ReactNode }) { data: bifrostConfig, error, isLoading, + isFetching, + refetch, } = useGetCoreConfigQuery( {}, { @@ -125,7 +129,13 @@ function AppContent({ children }: { children: React.ReactNode }) {
- {isLoading ? : {children}} + {isLoading ? ( + + ) : ( + + {children} + + )}
{bifrostConfig?.is_db_connected && }
@@ -149,7 +159,19 @@ function MinimalShell({ children }: { children: React.ReactNode }) { ); } -function FullPage({ config, children }: { config: BifrostConfig | undefined; children: React.ReactNode }) { +function FullPage({ + config, + hasError, + isRetrying, + onRetry, + children, +}: { + config: BifrostConfig | undefined; + hasError: boolean; + isRetrying: boolean; + onRetry: () => void; + children: React.ReactNode; +}) { const pathname = useLocation({ select: (l) => l.pathname }); if (config && config.is_db_connected) { return children; @@ -157,9 +179,37 @@ function FullPage({ config, children }: { config: BifrostConfig | undefined; chi if (config && config.is_logs_connected && pathname.startsWith("/workspace/logs")) { return children; } + if (hasError) { + return ; + } return ; } +function ConfigUnreachable({ isRetrying, onRetry }: { isRetrying: boolean; onRetry: () => void }) { + return ( +
+
+
+
+

+ We can't reach the dashboard +

+

+ Bifrost didn't return its configuration. This is usually a brief interruption, especially while Bifrost is being upgraded. +

+
+ +

Your settings and data are unaffected.

+
+
+
+ ); +} + export function ClientLayout({ children }: { children: React.ReactNode }) { return ( diff --git a/ui/app/globals.css b/ui/app/globals.css index e10de613384..366a9df013a 100644 --- a/ui/app/globals.css +++ b/ui/app/globals.css @@ -207,6 +207,18 @@ } } +@keyframes update-progress { + 0% { + transform: translateX(-110%); + } + 50% { + transform: translateX(110%); + } + 100% { + transform: translateX(310%); + } +} + @utility custom-scrollbar { overflow: auto !important; scrollbar-width: thin; /* Firefox */ @@ -305,17 +317,17 @@ div.content-container:has(.no-border-parent) { } @media (min-width: 768px) { -#trial-notification-banner { - width: 100%; - margin-left: 0; -} - -@media (min-width: 48rem) { #trial-notification-banner { - width: calc(100% + 80px); - margin-left: -40px; + width: 100%; + margin-left: 0; + } + + @media (min-width: 48rem) { + #trial-notification-banner { + width: calc(100% + 80px); + margin-left: -40px; + } } -} } div.content-container:has(.no-padding-parent) #trial-notification-banner { @@ -438,4 +450,4 @@ div.content-container:has(.no-border-parent) #trial-notification-banner { left: auto !important; right: 0 !important; transform: translate(35%, -35%) !important; -} +} \ No newline at end of file diff --git a/ui/app/main.tsx b/ui/app/main.tsx index 9eb3116ee1d..43f82d1955b 100644 --- a/ui/app/main.tsx +++ b/ui/app/main.tsx @@ -1,5 +1,6 @@ +import { clearAutoReloadGuard, getSkewMode, installVersionSkewListeners, subscribeSkew } from "@/lib/utils/versionSkew"; import { RouterProvider, createRouter, parseSearchWith, stringifySearchWith } from "@tanstack/react-router"; -import { StrictMode } from "react"; +import { StrictMode, useEffect, useSyncExternalStore } from "react"; import { createRoot } from "react-dom/client"; // Tailwind + global styles (also declares @font-face for local Geist fonts). @@ -7,8 +8,11 @@ import "@/app/globals.css"; import { ErrorComponent } from "./__error"; import { NotFoundComponent } from "./__notFound"; +import { UpdatingBanner, UpdatingScreen } from "./__updating"; import { routeTree } from "./routeTree.gen"; +installVersionSkewListeners(); + // Only JSON.parse structured values (objects/arrays). Plain strings and numbers // stay as-is so large numeric IDs don't lose precision through Number coercion. function safeJsonParse(value: string): unknown { @@ -51,11 +55,39 @@ declare module "@tanstack/react-router" { } } +const HEALTHY_UPTIME_MS = 30_000; + +function Root() { + const skewMode = useSyncExternalStore(subscribeSkew, getSkewMode, getSkewMode); + + useEffect(() => { + if (skewMode !== "none") return; + const timer = setTimeout(clearAutoReloadGuard, HEALTHY_UPTIME_MS); + return () => clearTimeout(timer); + }, [skewMode]); + + if (skewMode === "hard") return ; + + return ( + <> + + {skewMode === "soft" && } + + ); +} + +declare global { + interface Window { + __bifrostBooted?: boolean; + } +} +window.__bifrostBooted = true; + const rootEl = document.getElementById("root"); if (!rootEl) throw new Error("Root element #root not found"); createRoot(rootEl).render( - + , -); \ No newline at end of file +); diff --git a/ui/index.html b/ui/index.html index d0f49a1f471..886e0eeda95 100644 --- a/ui/index.html +++ b/ui/index.html @@ -7,6 +7,7 @@ Bifrost + diff --git a/ui/lib/utils/versionSkew.test.ts b/ui/lib/utils/versionSkew.test.ts new file mode 100644 index 00000000000..183d24500c2 --- /dev/null +++ b/ui/lib/utils/versionSkew.test.ts @@ -0,0 +1,252 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { + clearAutoReloadGuard, + consumeAutoReload, + getSkewMode, + isSkewError, + MAX_AUTO_RELOADS, + POLL_INTERVAL_MS, + POLL_TIMEOUT_MS, + reportSkew, + RELOAD_WINDOW_MS, + STABLE_POLLS_REQUIRED, + startVersionPoll, + subscribeSkew, + __resetSkewForTests, +} from "./versionSkew"; + +function installSessionStorage() { + const store = new Map(); + vi.stubGlobal("sessionStorage", { + getItem: (k: string) => store.get(k) ?? null, + setItem: (k: string, v: string) => void store.set(k, v), + removeItem: (k: string) => void store.delete(k), + }); + return store; +} + +describe("isSkewError", () => { + it("detects failed dynamic imports across browsers", () => { + expect(isSkewError(new TypeError("Failed to fetch dynamically imported module: https://x/assets/logs-A1.js"))).toBe(true); + expect(isSkewError(new TypeError("error loading dynamically imported module"))).toBe(true); + expect(isSkewError(new TypeError("Importing a module script failed."))).toBe(true); + }); + + it("detects bundler chunk failures by message and by name", () => { + expect(isSkewError(new Error("Loading chunk 42 failed."))).toBe(true); + expect(isSkewError(new Error("Loading CSS chunk 7 failed."))).toBe(true); + expect(isSkewError(Object.assign(new Error("boom"), { name: "ChunkLoadError" }))).toBe(true); + }); + + it("accepts bare strings", () => { + expect(isSkewError("Failed to fetch dynamically imported module")).toBe(true); + }); + + it("does not misclassify ordinary application errors", () => { + expect(isSkewError(new TypeError("x is not a function"))).toBe(false); + expect(isSkewError(new Error("Request failed with status 500"))).toBe(false); + expect(isSkewError(undefined)).toBe(false); + expect(isSkewError(null)).toBe(false); + expect(isSkewError("")).toBe(false); + }); +}); + +describe("skew store", () => { + beforeEach(() => __resetSkewForTests()); + + it("notifies subscribers once per level change", () => { + const onChange = vi.fn(); + subscribeSkew(onChange); + + expect(getSkewMode()).toBe("none"); + reportSkew("soft"); + expect(getSkewMode()).toBe("soft"); + reportSkew("soft"); + + expect(onChange).toHaveBeenCalledTimes(1); + }); + + it("escalates soft to hard when a route boundary trips", () => { + reportSkew("soft"); + reportSkew("hard"); + expect(getSkewMode()).toBe("hard"); + }); + + it("never downgrades hard back to soft", () => { + reportSkew("hard"); + reportSkew("soft"); + expect(getSkewMode()).toBe("hard"); + }); + + it("defaults to the non-destructive level", () => { + reportSkew(); + expect(getSkewMode()).toBe("soft"); + }); + + it("stops notifying after unsubscribe", () => { + const onChange = vi.fn(); + subscribeSkew(onChange)(); + reportSkew("soft"); + expect(onChange).not.toHaveBeenCalled(); + }); +}); + +describe("auto-reload guard", () => { + beforeEach(() => { + installSessionStorage(); + }); + + it("allows exactly MAX_AUTO_RELOADS inside the window", () => { + const t0 = 1_000_000; + expect(consumeAutoReload(t0)).toBe(true); + expect(consumeAutoReload(t0 + 1_000)).toBe(true); + expect(MAX_AUTO_RELOADS).toBe(2); + expect(consumeAutoReload(t0 + 2_000)).toBe(false); + }); + + it("starts a fresh budget once the window ages out", () => { + const t0 = 1_000_000; + consumeAutoReload(t0); + consumeAutoReload(t0); + expect(consumeAutoReload(t0)).toBe(false); + + expect(consumeAutoReload(t0 + RELOAD_WINDOW_MS + 1)).toBe(true); + }); + + it("resets when the guard is cleared after a healthy boot", () => { + const t0 = 1_000_000; + consumeAutoReload(t0); + consumeAutoReload(t0); + expect(consumeAutoReload(t0)).toBe(false); + + clearAutoReloadGuard(); + expect(consumeAutoReload(t0)).toBe(true); + }); + + it("degrades safely when sessionStorage is unavailable", () => { + vi.stubGlobal("sessionStorage", { + getItem: () => { + throw new Error("denied"); + }, + setItem: () => { + throw new Error("denied"); + }, + removeItem: () => { + throw new Error("denied"); + }, + }); + + // An unpersistable count would reset on every boot, so reloading is denied instead of looping forever. + expect(() => consumeAutoReload()).not.toThrow(); + expect(consumeAutoReload()).toBe(false); + expect(() => clearAutoReloadGuard()).not.toThrow(); + }); + + it("denies reloads when the count cannot be persisted", () => { + const store = installSessionStorage(); + const t0 = 1_000_000; + expect(consumeAutoReload(t0)).toBe(true); + + vi.stubGlobal("sessionStorage", { + getItem: (k: string) => store.get(k) ?? null, + setItem: () => { + throw new Error("quota exceeded"); + }, + removeItem: (k: string) => void store.delete(k), + }); + + // The budget cannot advance, so no further reload is authorized regardless of how often this is called. + expect(consumeAutoReload(t0 + 1_000)).toBe(false); + expect(consumeAutoReload(t0 + 2_000)).toBe(false); + }); +}); + +describe("startVersionPoll", () => { + beforeEach(() => vi.useFakeTimers()); + afterEach(() => vi.useRealTimers()); + + const okResponse = (version: string) => ({ ok: true, text: async () => version }) as unknown as Response; + + it("reloads once the reported version stays stable", async () => { + const onUpdateReady = vi.fn(); + const fetchVersion = vi.fn(async () => okResponse("v2")); + + const stop = startVersionPoll({ fetchVersion, onUpdateReady, onTimeout: vi.fn() }); + await vi.advanceTimersByTimeAsync(POLL_INTERVAL_MS * STABLE_POLLS_REQUIRED); + + expect(fetchVersion).toHaveBeenCalledTimes(STABLE_POLLS_REQUIRED); + expect(onUpdateReady).toHaveBeenCalledTimes(1); + + // The loop stopped, so no further requests go out. + await vi.advanceTimersByTimeAsync(POLL_INTERVAL_MS * 3); + expect(fetchVersion).toHaveBeenCalledTimes(STABLE_POLLS_REQUIRED); + stop(); + }); + + it("times out a request that never settles instead of stalling forever", async () => { + const onTimeout = vi.fn(); + let signal: AbortSignal | undefined; + const fetchVersion = vi.fn( + (s: AbortSignal) => + new Promise((_, reject) => { + signal = s; + s.addEventListener("abort", () => reject(new Error("aborted"))); + }), + ); + + const stop = startVersionPoll({ fetchVersion, onUpdateReady: vi.fn(), onTimeout }); + await vi.advanceTimersByTimeAsync(POLL_INTERVAL_MS); + expect(onTimeout).not.toHaveBeenCalled(); + + await vi.advanceTimersByTimeAsync(POLL_TIMEOUT_MS); + expect(signal?.aborted).toBe(true); + expect(onTimeout).toHaveBeenCalledTimes(1); + + // The poll is done: no retry is scheduled behind the timeout. + await vi.advanceTimersByTimeAsync(POLL_INTERVAL_MS * 3); + expect(fetchVersion).toHaveBeenCalledTimes(1); + expect(onTimeout).toHaveBeenCalledTimes(1); + stop(); + }); + + it("retries after a failed request until the budget runs out", async () => { + const onTimeout = vi.fn(); + const fetchVersion = vi.fn(async () => { + throw new Error("network down"); + }); + + const stop = startVersionPoll({ fetchVersion, onUpdateReady: vi.fn(), onTimeout }); + await vi.advanceTimersByTimeAsync(POLL_INTERVAL_MS * 3); + expect(fetchVersion).toHaveBeenCalledTimes(3); + expect(onTimeout).not.toHaveBeenCalled(); + + await vi.advanceTimersByTimeAsync(POLL_TIMEOUT_MS); + expect(onTimeout).toHaveBeenCalledTimes(1); + stop(); + }); + + it("aborts the in-flight request and stops polling when cancelled", async () => { + const onTimeout = vi.fn(); + const onUpdateReady = vi.fn(); + let signal: AbortSignal | undefined; + const fetchVersion = vi.fn( + (s: AbortSignal) => + new Promise((_, reject) => { + signal = s; + s.addEventListener("abort", () => reject(new Error("aborted"))); + }), + ); + + const stop = startVersionPoll({ fetchVersion, onUpdateReady, onTimeout }); + await vi.advanceTimersByTimeAsync(POLL_INTERVAL_MS); + stop(); + + expect(signal?.aborted).toBe(true); + expect(vi.getTimerCount()).toBe(0); + + await vi.advanceTimersByTimeAsync(POLL_TIMEOUT_MS * 2); + expect(fetchVersion).toHaveBeenCalledTimes(1); + expect(onTimeout).not.toHaveBeenCalled(); + expect(onUpdateReady).not.toHaveBeenCalled(); + }); +}); \ No newline at end of file diff --git a/ui/lib/utils/versionSkew.ts b/ui/lib/utils/versionSkew.ts new file mode 100644 index 00000000000..fd4ed9a0eea --- /dev/null +++ b/ui/lib/utils/versionSkew.ts @@ -0,0 +1,231 @@ +export const SKEW_ERROR_PATTERNS = [ + "failed to fetch dynamically imported module", + "error loading dynamically imported module", + "importing a module script failed", + "chunkloaderror", + "loading chunk", + "loading css chunk", + "failed to load module script", +] as const; + +export function isSkewError(err: unknown): boolean { + if (!err) return false; + + if (typeof err === "object" && "name" in err && (err as { name?: unknown }).name === "ChunkLoadError") { + return true; + } + + const message = typeof err === "object" && "message" in err ? String((err as { message?: unknown }).message ?? "") : String(err); + if (!message) return false; + + const haystack = message.toLowerCase(); + return SKEW_ERROR_PATTERNS.some((pattern) => haystack.includes(pattern)); +} + +// Soft failures preserve the app; hard failures replace an already-unmounted route. +export type SkewMode = "none" | "soft" | "hard"; + +let skewMode: SkewMode = "none"; +const subscribers = new Set<() => void>(); + +export function getSkewMode(): SkewMode { + return skewMode; +} + +export function subscribeSkew(onChange: () => void): () => void { + subscribers.add(onChange); + return () => { + subscribers.delete(onChange); + }; +} + +export function reportSkew(mode: "soft" | "hard" = "soft"): void { + if (skewMode === "hard") return; + if (skewMode === mode) return; + skewMode = mode; + for (const onChange of subscribers) onChange(); +} + +export function __resetSkewForTests(): void { + skewMode = "none"; + subscribers.clear(); +} + +const RELOAD_GUARD_KEY = "bifrost:skew-reloads"; +export const MAX_AUTO_RELOADS = 2; +export const RELOAD_WINDOW_MS = 60_000; + +interface ReloadGuardState { + count: number; + firstAt: number; +} + +function readGuard(): ReloadGuardState | null { + try { + const raw = sessionStorage.getItem(RELOAD_GUARD_KEY); + if (!raw) return null; + const parsed = JSON.parse(raw) as Partial; + if (typeof parsed?.count !== "number" || typeof parsed?.firstAt !== "number") return null; + return { count: parsed.count, firstAt: parsed.firstAt }; + } catch { + return null; + } +} + +function writeGuard(state: ReloadGuardState): boolean { + try { + sessionStorage.setItem(RELOAD_GUARD_KEY, JSON.stringify(state)); + return true; + } catch { + return false; + } +} + +// A reload is only authorized when the new count survives a reload; otherwise the budget could never be spent. +export function consumeAutoReload(now: number = Date.now()): boolean { + const existing = readGuard(); + + if (!existing || now - existing.firstAt > RELOAD_WINDOW_MS) { + return writeGuard({ count: 1, firstAt: now }); + } + + if (existing.count >= MAX_AUTO_RELOADS) return false; + + return writeGuard({ count: existing.count + 1, firstAt: existing.firstAt }); +} + +export function clearAutoReloadGuard(): void { + try { + sessionStorage.removeItem(RELOAD_GUARD_KEY); + } catch {} +} + +export const POLL_INTERVAL_MS = 3_000; +export const POLL_TIMEOUT_MS = 90_000; + +// Require repeated matches because polls may hit different pods during a rollout. +export const STABLE_POLLS_REQUIRED = 3; + +export interface VersionPollHandlers { + fetchVersion: (signal: AbortSignal) => Promise; + // The reported version stayed stable long enough to consider the rollout done. + onUpdateReady: () => void; + // The overall polling budget ran out, including while a request was still in flight. + onTimeout: () => void; +} + +/** Polls the version endpoint until it stabilises, times out, or is stopped. Returns the stop function. */ +export function startVersionPoll({ fetchVersion, onUpdateReady, onTimeout }: VersionPollHandlers): () => void { + const startedAt = Date.now(); + let cancelled = false; + let timer: ReturnType | undefined; + let abortTimer: ReturnType | undefined; + let controller: AbortController | undefined; + let lastVersion: string | null = null; + let stableCount = 0; + + const clearRequest = () => { + if (abortTimer) clearTimeout(abortTimer); + abortTimer = undefined; + controller = undefined; + }; + + const poll = async () => { + timer = undefined; + if (cancelled) return; + + const remaining = POLL_TIMEOUT_MS - (Date.now() - startedAt); + if (remaining <= 0) { + onTimeout(); + return; + } + + // Cap each request at the remaining budget so a request that never settles cannot stall the poll forever. + let timedOut = false; + controller = new AbortController(); + abortTimer = setTimeout(() => { + timedOut = true; + controller?.abort(); + }, remaining); + + try { + const response = await fetchVersion(controller.signal); + if (cancelled) return; + + if (response.ok) { + const version = (await response.text()).trim(); + if (cancelled) return; + + if (version && version === lastVersion) { + stableCount += 1; + } else { + lastVersion = version; + stableCount = 1; + } + + if (stableCount >= STABLE_POLLS_REQUIRED) { + onUpdateReady(); + return; + } + } + } catch { + if (timedOut && !cancelled) onTimeout(); + if (timedOut) return; + } finally { + clearRequest(); + } + + if (!cancelled) timer = setTimeout(poll, POLL_INTERVAL_MS); + }; + + timer = setTimeout(poll, POLL_INTERVAL_MS); + + return () => { + cancelled = true; + if (timer) clearTimeout(timer); + timer = undefined; + controller?.abort(); + clearRequest(); + }; +} + +function isAssetElementError(target: EventTarget | null): boolean { + if (!(target instanceof HTMLScriptElement) && !(target instanceof HTMLLinkElement)) return false; + const url = target instanceof HTMLScriptElement ? target.src : target.href; + if (!url) return false; + try { + const parsed = new URL(url, window.location.href); + return parsed.origin === window.location.origin && parsed.pathname.includes("/assets/"); + } catch { + return false; + } +} + +export function installVersionSkewListeners(): void { + if (typeof window === "undefined") return; + + // Do not preventDefault: Vite must rethrow so the route boundary can escalate to hard mode. + window.addEventListener("vite:preloadError", () => { + reportSkew("soft"); + }); + + window.addEventListener("unhandledrejection", (event) => { + if (!isSkewError(event.reason)) return; + event.preventDefault(); + reportSkew("soft"); + }); + + window.addEventListener( + "error", + (event) => { + if (isAssetElementError(event.target)) { + reportSkew("soft"); + return; + } + if (isSkewError((event as ErrorEvent).error ?? (event as ErrorEvent).message)) { + reportSkew("soft"); + } + }, + true, + ); +} \ No newline at end of file From e279ffddbb58788a0d0763973702e976d7e8f6fe Mon Sep 17 00:00:00 2001 From: Suresh Chaudhary <83772622+impoiler@users.noreply.github.com> Date: Fri, 14 Aug 2026 19:36:48 +0530 Subject: [PATCH 009/129] chore: format UI codebase (#6158) Briefly explain the purpose of this PR and the problem it solves. - What was changed and why - Any notable design decisions or trade-offs - [ ] Bug fix - [ ] Feature - [ ] Refactor - [ ] Documentation - [ ] Chore/CI - [ ] Core (Go) - [ ] Transports (HTTP) - [ ] Providers/Integrations - [ ] Plugins - [ ] UI (React) - [ ] Docs Describe the steps to validate this change. Include commands and expected outcomes. ```sh go version go test ./... cd ui pnpm i || npm i pnpm test || npm test pnpm build || npm run build ``` If adding new configs or environment variables, document them here. If UI changes, add before/after screenshots or short clips. - [ ] Yes - [ ] No If yes, describe impact and migration instructions. Link related issues and discussions. Example: Closes #123 Note any security implications (auth, secrets, PII, sandboxing, etc.). - [ ] I read `docs/contributing/README.md` and followed the guidelines - [ ] I added/updated tests where appropriate - [ ] I updated documentation where needed - [ ] I verified builds succeed (Go and UI) - [ ] I verified the CI pipeline passes locally if applicable --- .../managedVirtualKeyActions.tsx | 2 +- .../components/alerting/alertChannelsView.tsx | 2 +- .../components/branding/brandingView.tsx | 2 +- .../components/edge-control/configView.tsx | 2 +- .../components/edge-control/devicesView.tsx | 2 +- .../edge-control/fallbackWrapper.tsx | 2 +- .../components/edge-control/inventoryView.tsx | 2 +- .../components/license/licenseInfoView.tsx | 2 +- .../sheets/customerDetailSheet.tsx | 2 +- .../components/user-groups/teamsView.tsx | 2 +- .../user-groups/viewUserDetailsButton.tsx | 2 +- .../enterprise/lib/contexts/rbacContext.tsx | 2 +- .../lib/registrations/userPicker.ts | 2 +- .../enterprise/lib/types/accessProfile.ts | 2 +- ui/app/clientLayout.tsx | 2 +- ui/app/oauth/consent/layout.tsx | 2 +- ui/app/oauth/consent/page.tsx | 114 +--- ui/app/workspace/adaptive-routing/page.tsx | 4 +- .../adaptive-routing/settings/page.tsx | 2 +- ui/app/workspace/alerting/channels/layout.tsx | 4 +- ui/app/workspace/alerting/channels/page.tsx | 2 +- ui/app/workspace/alerting/history/layout.tsx | 4 +- ui/app/workspace/alerting/history/page.tsx | 2 +- ui/app/workspace/alerting/rules/layout.tsx | 4 +- ui/app/workspace/alerting/rules/page.tsx | 2 +- ui/app/workspace/circuit-breaker/page.tsx | 2 +- ui/app/workspace/cluster/page.tsx | 2 +- ui/app/workspace/complexity-router/page.tsx | 343 +++++----- ui/app/workspace/config/api-keys/page.tsx | 2 +- ui/app/workspace/config/branding/layout.tsx | 2 +- ui/app/workspace/config/branding/page.tsx | 2 +- ui/app/workspace/config/caching/page.tsx | 4 +- .../workspace/config/client-settings/page.tsx | 4 +- .../workspace/config/compatibility/page.tsx | 2 +- .../workspace/config/feature-flags/page.tsx | 2 +- ui/app/workspace/config/license/page.tsx | 2 +- ui/app/workspace/config/logging/page.tsx | 4 +- .../config/performance-tuning/page.tsx | 2 +- ui/app/workspace/config/proxy/page.tsx | 4 +- ui/app/workspace/config/security/page.tsx | 4 +- .../config/views/clientSettingsView.tsx | 2 +- ui/app/workspace/config/views/loggingView.tsx | 6 +- ui/app/workspace/config/views/mcpView.tsx | 598 ++++++++---------- .../workspace/config/views/securityView.tsx | 27 +- .../config/views/userAgentMappingsView.tsx | 28 +- .../overrides/pricingOverrideSheet.tsx | 13 +- ui/app/workspace/custom-pricing/page.tsx | 2 +- .../components/charts/logVolumeChart.tsx | 2 +- .../components/charts/throughputChart.tsx | 2 +- .../workspace/edge-control/config/layout.tsx | 2 +- ui/app/workspace/edge-control/config/page.tsx | 2 +- .../workspace/edge-control/devices/layout.tsx | 2 +- .../workspace/edge-control/devices/page.tsx | 2 +- .../edge-control/inventory/layout.tsx | 2 +- .../governance/views/customerSheet.tsx | 2 +- .../workspace/governance/views/teamSheet.tsx | 2 +- .../guardrails/configuration/page.tsx | 2 +- .../workspace/guardrails/providers/page.tsx | 4 +- ui/app/workspace/logs/page.tsx | 5 +- .../workspace/logs/sheets/logDetailView.tsx | 16 +- .../workspace/logs/sheets/logDetailsSheet.tsx | 2 +- .../logs/sheets/observabilityConfigSheet.tsx | 4 +- .../sheets/observabilitySettingsSheet.tsx | 4 +- .../logs/sheets/sessionDetailsSheet.tsx | 4 +- ui/app/workspace/logs/views/columns.tsx | 114 ++-- ui/app/workspace/logs/views/logsTable.tsx | 5 +- .../workspace/logs/views/logsVolumeChart.tsx | 2 +- .../logs/views/recalculateCostDialog.tsx | 22 +- ui/app/workspace/mcp-logs/page.tsx | 7 +- ui/app/workspace/mcp-logs/views/columns.tsx | 2 +- .../mcp-logs/views/mcpLogDetailsSheet.tsx | 6 +- .../workspace/mcp-registry/library/page.tsx | 2 +- .../views/mcpLibraryAddServerSheet.tsx | 2 +- .../library/views/mcpLibraryFilterSidebar.tsx | 8 +- .../library/views/mcpLibraryInstallSheet.tsx | 2 +- .../library/views/mcpLibraryServersTable.tsx | 2 +- .../library/views/mcpLibrarySettingsSheet.tsx | 2 +- ui/app/workspace/mcp-registry/page.tsx | 2 +- .../mcp-registry/views/authorizerUi.tsx | 12 +- .../mcp-registry/views/mcpClientForm.tsx | 81 +-- .../mcp-registry/views/mcpClientSheet.tsx | 129 ++-- .../views/mcpClientsFilterSidebar.tsx | 8 +- .../mcp-registry/views/mcpClientsTable.tsx | 16 +- .../views/mcpClientsTable.utils.test.ts | 2 +- .../views/mcpClientsTable.utils.ts | 9 +- .../views/mcpHeadersAuthorizer.tsx | 15 +- .../mcpUsageGuide/mcpUsageGuideSheet.tsx | 2 +- .../views/tokenExchangeFields.tsx | 20 +- ui/app/workspace/mcp-sessions/page.tsx | 2 +- .../views/mcpSessionsFilterSidebar.tsx | 8 +- ui/app/workspace/mcp-settings/page.tsx | 8 +- ui/app/workspace/mcp-tool-groups/page.tsx | 2 +- .../model-catalog/views/attributeSheet.tsx | 2 +- .../model-limits/views/modelLimitSheet.tsx | 2 +- ui/app/workspace/oauth-grants/layout.tsx | 2 +- ui/app/workspace/oauth-grants/page.tsx | 2 +- .../oauth-grants/views/grantActions.tsx | 23 +- .../oauth-grants/views/grantsTable.tsx | 45 +- .../views/oauthGrantsFilterSidebar.tsx | 8 +- .../oauth-grants/views/revokeGrantDialog.tsx | 20 +- ui/app/workspace/observability/page.tsx | 4 +- .../sheets/pluginTracingSheet.tsx | 2 +- .../plugins/sheets/addNewPluginSheet.tsx | 2 +- .../plugins/sheets/pluginSequenceSheet.tsx | 2 +- .../dialogs/addNewCustomProviderSheet.tsx | 2 +- .../providers/dialogs/addNewKeySheet.tsx | 2 +- .../providers/dialogs/providerConfigSheet.tsx | 2 +- .../fragments/allowedRequestsFields.tsx | 62 +- .../fragments/apiStructureFormFragment.tsx | 2 +- .../fragments/betaHeadersFormFragment.tsx | 2 +- .../fragments/governanceFormFragment.tsx | 2 +- .../fragments/networkFormFragment.tsx | 2 +- .../fragments/openaiConfigFormFragment.tsx | 2 +- .../fragments/performanceFormFragment.tsx | 2 +- .../providers/fragments/proxyFormFragment.tsx | 2 +- ui/app/workspace/providers/page.tsx | 2 +- .../providers/views/modelProviderConfig.tsx | 2 +- .../views/modelProviderKeysTableView.tsx | 22 +- .../providers/views/providerKeyForm.tsx | 2 +- .../views/routingRuleInfoSheet.tsx | 4 +- .../routing-rules/views/routingRuleSheet.tsx | 2 +- ui/app/workspace/scim/page.tsx | 2 +- .../skills-repo/components/shared.tsx | 2 +- .../components/skillDetailsView.tsx | 2 +- .../skills-repo/components/skillListView.tsx | 7 +- .../skills-repo/forms/skillEditForm.tsx | 26 +- ui/app/workspace/skills-repo/page.tsx | 2 +- .../views/virtualKeyDetailsSheet.tsx | 2 +- .../virtual-keys/views/virtualKeySheet.tsx | 52 +- ui/app/workspace/webhooks/page.tsx | 2 +- .../webhooks/views/webhookDetailsSheet.tsx | 4 +- .../workspace/webhooks/views/webhookSheet.tsx | 2 +- ui/components/budgetOverrideManagerDialog.tsx | 44 +- ui/components/filters/logsFilterSidebar.tsx | 8 +- ui/components/filters/mcpFilterSidebar.tsx | 8 +- .../prompts/components/apiKeySelectorView.tsx | 2 +- .../prompts/sheets/commitVersionSheet.tsx | 2 +- ui/components/prompts/sheets/folderSheet.tsx | 2 +- ui/components/prompts/sheets/promptSheet.tsx | 2 +- ui/components/sidebar.tsx | 128 ++-- .../ui/custom/celBuilder/celRuleBuilder.tsx | 15 +- ui/components/ui/modelMultiselect.tsx | 10 +- ui/components/ui/tagInput.tsx | 4 +- ui/hooks/useOnboardingChecklist.ts | 5 +- ui/lib/config/celFieldsRouting.ts | 28 +- ui/lib/constants/logs.test.ts | 2 +- ui/lib/constants/logs.ts | 2 +- ui/lib/registries/userPicker.tsx | 7 +- ui/lib/store/apis/configApi.ts | 2 +- ui/lib/store/apis/mcpLogsApi.ts | 2 +- ui/lib/store/apis/oauth2ConsentApi.ts | 10 +- ui/lib/store/apis/oauth2SessionsApi.ts | 2 +- ui/lib/utils/loginGoto.ts | 10 +- ui/lib/utils/validation.test.ts | 2 +- ui/lib/utils/validation.ts | 2 +- ui/package.json | 2 +- 156 files changed, 1158 insertions(+), 1254 deletions(-) diff --git a/ui/app/_fallbacks/enterprise/components/access-profiles/managedVirtualKeyActions.tsx b/ui/app/_fallbacks/enterprise/components/access-profiles/managedVirtualKeyActions.tsx index 48fca9adcf4..d344ee84669 100644 --- a/ui/app/_fallbacks/enterprise/components/access-profiles/managedVirtualKeyActions.tsx +++ b/ui/app/_fallbacks/enterprise/components/access-profiles/managedVirtualKeyActions.tsx @@ -6,4 +6,4 @@ interface ManagedVirtualKeyActionsProps { export default function ManagedVirtualKeyActions(_props: ManagedVirtualKeyActionsProps) { return null; -} +} \ No newline at end of file diff --git a/ui/app/_fallbacks/enterprise/components/alerting/alertChannelsView.tsx b/ui/app/_fallbacks/enterprise/components/alerting/alertChannelsView.tsx index 9e00fe27017..7639931dbef 100644 --- a/ui/app/_fallbacks/enterprise/components/alerting/alertChannelsView.tsx +++ b/ui/app/_fallbacks/enterprise/components/alerting/alertChannelsView.tsx @@ -9,4 +9,4 @@ export default function AlertChannelsView() { testIdPrefix="alert-channels" /> ); -} +} \ No newline at end of file diff --git a/ui/app/_fallbacks/enterprise/components/branding/brandingView.tsx b/ui/app/_fallbacks/enterprise/components/branding/brandingView.tsx index 65b8530cfe0..eb9fdf6a0d3 100644 --- a/ui/app/_fallbacks/enterprise/components/branding/brandingView.tsx +++ b/ui/app/_fallbacks/enterprise/components/branding/brandingView.tsx @@ -17,4 +17,4 @@ export default function BrandingView() { />
); -} +} \ No newline at end of file diff --git a/ui/app/_fallbacks/enterprise/components/edge-control/configView.tsx b/ui/app/_fallbacks/enterprise/components/edge-control/configView.tsx index 02bed0e187c..44578a3b34f 100644 --- a/ui/app/_fallbacks/enterprise/components/edge-control/configView.tsx +++ b/ui/app/_fallbacks/enterprise/components/edge-control/configView.tsx @@ -11,4 +11,4 @@ export default function ConfigView() { testIdPrefix="edge-config" /> ); -} +} \ No newline at end of file diff --git a/ui/app/_fallbacks/enterprise/components/edge-control/devicesView.tsx b/ui/app/_fallbacks/enterprise/components/edge-control/devicesView.tsx index c52972487a9..e43db2d6dcf 100644 --- a/ui/app/_fallbacks/enterprise/components/edge-control/devicesView.tsx +++ b/ui/app/_fallbacks/enterprise/components/edge-control/devicesView.tsx @@ -11,4 +11,4 @@ export default function DevicesView() { testIdPrefix="edge-devices" /> ); -} +} \ No newline at end of file diff --git a/ui/app/_fallbacks/enterprise/components/edge-control/fallbackWrapper.tsx b/ui/app/_fallbacks/enterprise/components/edge-control/fallbackWrapper.tsx index 8a84ba4dd11..484bdb283d5 100644 --- a/ui/app/_fallbacks/enterprise/components/edge-control/fallbackWrapper.tsx +++ b/ui/app/_fallbacks/enterprise/components/edge-control/fallbackWrapper.tsx @@ -21,4 +21,4 @@ export default function EdgeControlFallbackView({ icon, title, description, read />
); -} +} \ No newline at end of file diff --git a/ui/app/_fallbacks/enterprise/components/edge-control/inventoryView.tsx b/ui/app/_fallbacks/enterprise/components/edge-control/inventoryView.tsx index f1771d87258..d6ae619ee29 100644 --- a/ui/app/_fallbacks/enterprise/components/edge-control/inventoryView.tsx +++ b/ui/app/_fallbacks/enterprise/components/edge-control/inventoryView.tsx @@ -11,4 +11,4 @@ export default function InventoryView() { testIdPrefix="edge-inventory" /> ); -} +} \ No newline at end of file diff --git a/ui/app/_fallbacks/enterprise/components/license/licenseInfoView.tsx b/ui/app/_fallbacks/enterprise/components/license/licenseInfoView.tsx index 6d8f19604e7..f7684039882 100644 --- a/ui/app/_fallbacks/enterprise/components/license/licenseInfoView.tsx +++ b/ui/app/_fallbacks/enterprise/components/license/licenseInfoView.tsx @@ -14,4 +14,4 @@ export default function LicenseSettingsView() { />
); -} +} \ No newline at end of file diff --git a/ui/app/_fallbacks/enterprise/components/user-groups/sheets/customerDetailSheet.tsx b/ui/app/_fallbacks/enterprise/components/user-groups/sheets/customerDetailSheet.tsx index 9f407408040..888aa9c9d43 100644 --- a/ui/app/_fallbacks/enterprise/components/user-groups/sheets/customerDetailSheet.tsx +++ b/ui/app/_fallbacks/enterprise/components/user-groups/sheets/customerDetailSheet.tsx @@ -183,4 +183,4 @@ export function CustomerDetailSheet({ customer, open, onOpenChange }: Props) { ); } -export default CustomerDetailSheet; +export default CustomerDetailSheet; \ No newline at end of file diff --git a/ui/app/_fallbacks/enterprise/components/user-groups/teamsView.tsx b/ui/app/_fallbacks/enterprise/components/user-groups/teamsView.tsx index 60a2aa4bdcf..39993615b91 100644 --- a/ui/app/_fallbacks/enterprise/components/user-groups/teamsView.tsx +++ b/ui/app/_fallbacks/enterprise/components/user-groups/teamsView.tsx @@ -89,4 +89,4 @@ export function TeamsView() { isLoading={isFetching} /> ); -} +} \ No newline at end of file diff --git a/ui/app/_fallbacks/enterprise/components/user-groups/viewUserDetailsButton.tsx b/ui/app/_fallbacks/enterprise/components/user-groups/viewUserDetailsButton.tsx index def2d3aa449..47f7d042635 100644 --- a/ui/app/_fallbacks/enterprise/components/user-groups/viewUserDetailsButton.tsx +++ b/ui/app/_fallbacks/enterprise/components/user-groups/viewUserDetailsButton.tsx @@ -5,4 +5,4 @@ interface ViewUserDetailsButtonProps { export default function ViewUserDetailsButton(_props: ViewUserDetailsButtonProps) { return null; -} +} \ No newline at end of file diff --git a/ui/app/_fallbacks/enterprise/lib/contexts/rbacContext.tsx b/ui/app/_fallbacks/enterprise/lib/contexts/rbacContext.tsx index 9d0df9dfa1a..bc29659b3fb 100644 --- a/ui/app/_fallbacks/enterprise/lib/contexts/rbacContext.tsx +++ b/ui/app/_fallbacks/enterprise/lib/contexts/rbacContext.tsx @@ -93,4 +93,4 @@ export function useRbacContext() { }; } return context; -} +} \ No newline at end of file diff --git a/ui/app/_fallbacks/enterprise/lib/registrations/userPicker.ts b/ui/app/_fallbacks/enterprise/lib/registrations/userPicker.ts index 2fcebc58bdf..f1486ab5118 100644 --- a/ui/app/_fallbacks/enterprise/lib/registrations/userPicker.ts +++ b/ui/app/_fallbacks/enterprise/lib/registrations/userPicker.ts @@ -5,4 +5,4 @@ // resolves to _fallbacks/. The enterprise build replaces this module with // one that registers the async user picker, which is what reveals the // "User" scope options. -export {}; +export {}; \ No newline at end of file diff --git a/ui/app/_fallbacks/enterprise/lib/types/accessProfile.ts b/ui/app/_fallbacks/enterprise/lib/types/accessProfile.ts index 000ac55e6ad..e26b43f5b11 100644 --- a/ui/app/_fallbacks/enterprise/lib/types/accessProfile.ts +++ b/ui/app/_fallbacks/enterprise/lib/types/accessProfile.ts @@ -41,4 +41,4 @@ export interface UserAccessProfile { export interface GetUserAccessProfilesResponse { access_profiles: UserAccessProfile[]; -} +} \ No newline at end of file diff --git a/ui/app/clientLayout.tsx b/ui/app/clientLayout.tsx index c5ca570ba6a..03c2d1c55fa 100644 --- a/ui/app/clientLayout.tsx +++ b/ui/app/clientLayout.tsx @@ -226,4 +226,4 @@ export function ClientLayout({ children }: { children: React.ReactNode }) { ); -} +} \ No newline at end of file diff --git a/ui/app/oauth/consent/layout.tsx b/ui/app/oauth/consent/layout.tsx index 9f665a7e1b4..6ce4f112a3b 100644 --- a/ui/app/oauth/consent/layout.tsx +++ b/ui/app/oauth/consent/layout.tsx @@ -37,4 +37,4 @@ function RouteComponent() { export const Route = createFileRoute("/oauth/consent")({ staticData: { tempTokenScoped: true }, component: RouteComponent, -}); +}); \ No newline at end of file diff --git a/ui/app/oauth/consent/page.tsx b/ui/app/oauth/consent/page.tsx index 7f1324c8f3c..6a98ebda6e4 100644 --- a/ui/app/oauth/consent/page.tsx +++ b/ui/app/oauth/consent/page.tsx @@ -3,25 +3,9 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Separator } from "@/components/ui/separator"; import { toast } from "sonner"; -import { - getErrorMessage, - useGetOAuth2ConsentFlowQuery, - useIsAuthEnabledQuery, - useSubmitOAuth2ConsentFlowMutation, -} from "@/lib/store"; -import { - getActiveTempToken, - setActiveTempToken, - setSuppressGlobal401, -} from "@/lib/store/apis/tempToken"; -import { - Fingerprint, - KeyRound, - Loader2, - LogIn, - ShieldCheck, - UserRound, -} from "lucide-react"; +import { getErrorMessage, useGetOAuth2ConsentFlowQuery, useIsAuthEnabledQuery, useSubmitOAuth2ConsentFlowMutation } from "@/lib/store"; +import { getActiveTempToken, setActiveTempToken, setSuppressGlobal401 } from "@/lib/store/apis/tempToken"; +import { Fingerprint, KeyRound, Loader2, LogIn, ShieldCheck, UserRound } from "lucide-react"; import { useQueryState } from "nuqs"; import React, { useEffect, useMemo, useState } from "react"; @@ -34,9 +18,8 @@ export default function OAuth2ConsentPage() {

Missing flow identifier

- This URL is missing the{" "} - flow{" "} - query parameter. Restart the connection from your MCP client. + This URL is missing the flow query parameter. Restart the + connection from your MCP client.

@@ -96,10 +79,7 @@ function ConsentView({ flowId }: { flowId: string }) { } }, [flowId]); - const showLoginOption = - usingTempToken && - authState?.is_auth_enabled === true && - authState.has_valid_token === false; + const showLoginOption = usingTempToken && authState?.is_auth_enabled === true && authState.has_valid_token === false; const handleSubmit = async (mode: "vk" | "session" | "user") => { setSelectedMode(mode); @@ -135,8 +115,7 @@ function ConsentView({ flowId }: { flowId: string }) {

Link unavailable

- This authorization link may have expired or already been used. - Restart the connection from your MCP client to get a fresh link. + This authorization link may have expired or already been used. Restart the connection from your MCP client to get a fresh link.

@@ -156,27 +135,16 @@ function ConsentView({ flowId }: { flowId: string }) {
-

- {clientName} wants to connect -

-

- Choose how you'd like to identify yourself to Bifrost -

+

{clientName} wants to connect

+

Choose how you'd like to identify yourself to Bifrost

{/* No mode available — nothing the user can act on here */} {!hasAnyMode && ( -
-

- No authentication options available -

-

- Restart the connection from your MCP client. -

+
+

No authentication options available

+

Restart the connection from your MCP client.

)} @@ -188,9 +156,7 @@ function ConsentView({ flowId }: { flowId: string }) {
-

- {flow.logged_in_user.name || flow.logged_in_user.id} -

+

{flow.logged_in_user.name || flow.logged_in_user.id}

Signed-in account

@@ -201,7 +167,10 @@ function ConsentView({ flowId }: { flowId: string }) { disabled={submitting} > {submitting && selectedMode === "user" ? ( - <>Connecting… + <> + + Connecting… + ) : ( <>Continue as {flow.logged_in_user.name || flow.logged_in_user.id} )} @@ -218,9 +187,7 @@ function ConsentView({ flowId }: { flowId: string }) {

Sign in with your account

-

- Requires a Bifrost dashboard account -

+

Requires a Bifrost dashboard account

Virtual Key

-

- Use a Virtual Key from your Bifrost workspace -

+

Use a Virtual Key from your Bifrost workspace

{submitting && selectedMode === "vk" ? ( - <>Connecting… + <> + + Connecting… + ) : ( "Connect with key" )} {hasUser && (

- If this key is linked to a user account, you'll be asked to sign - in to confirm your identity. + If this key is linked to a user account, you'll be asked to sign in to confirm your identity.

)}
@@ -295,7 +262,7 @@ function ConsentView({ flowId }: { flowId: string }) { {hasVK && hasSession && (
- + or
@@ -306,29 +273,23 @@ function ConsentView({ flowId }: { flowId: string }) { )}
{/* Expiry */} -

- This link expires {formatExpiry(flow.expires_at)} -

+

This link expires {formatExpiry(flow.expires_at)}

); } @@ -351,9 +312,7 @@ function formatExpiry(iso: string): string { function Shell({ children }: { children: React.ReactNode }) { return (
-
- {children} -
+
{children}
); } @@ -362,15 +321,12 @@ function InvalidLinkView() { return (
-

- This link is no longer valid -

+

This link is no longer valid

- The authorization link has expired, been used already, or had its - token stripped. Restart the connection from your MCP client to get a - fresh link. + The authorization link has expired, been used already, or had its token stripped. Restart the connection from your MCP client to + get a fresh link.

); -} +} \ No newline at end of file diff --git a/ui/app/workspace/adaptive-routing/page.tsx b/ui/app/workspace/adaptive-routing/page.tsx index e53a89b9518..91151dbe7c2 100644 --- a/ui/app/workspace/adaptive-routing/page.tsx +++ b/ui/app/workspace/adaptive-routing/page.tsx @@ -2,8 +2,8 @@ import AdaptiveRoutingView from "@enterprise/components/adaptive-routing/adaptiv export default function AdaptiveRoutingPage() { return ( -
+
); -} +} \ No newline at end of file diff --git a/ui/app/workspace/adaptive-routing/settings/page.tsx b/ui/app/workspace/adaptive-routing/settings/page.tsx index f6206edfe61..d0f90b79153 100644 --- a/ui/app/workspace/adaptive-routing/settings/page.tsx +++ b/ui/app/workspace/adaptive-routing/settings/page.tsx @@ -6,4 +6,4 @@ export default function AdaptiveRoutingSettingsPage() {
); -} +} \ No newline at end of file diff --git a/ui/app/workspace/alerting/channels/layout.tsx b/ui/app/workspace/alerting/channels/layout.tsx index eea9878c646..e9d22aaa036 100644 --- a/ui/app/workspace/alerting/channels/layout.tsx +++ b/ui/app/workspace/alerting/channels/layout.tsx @@ -2,5 +2,5 @@ import { createFileRoute } from "@tanstack/react-router"; import AlertChannelsPage from "./page"; export const Route = createFileRoute("/workspace/alerting/channels")({ - component: AlertChannelsPage, -}); + component: AlertChannelsPage, +}); \ No newline at end of file diff --git a/ui/app/workspace/alerting/channels/page.tsx b/ui/app/workspace/alerting/channels/page.tsx index 4f6526bd923..09ddd10a853 100644 --- a/ui/app/workspace/alerting/channels/page.tsx +++ b/ui/app/workspace/alerting/channels/page.tsx @@ -2,4 +2,4 @@ import AlertChannelsView from "@enterprise/components/alerting/alertChannelsView export default function AlertChannelsPage() { return ; -} +} \ No newline at end of file diff --git a/ui/app/workspace/alerting/history/layout.tsx b/ui/app/workspace/alerting/history/layout.tsx index 3f8be68839b..0e06a7748d7 100644 --- a/ui/app/workspace/alerting/history/layout.tsx +++ b/ui/app/workspace/alerting/history/layout.tsx @@ -2,5 +2,5 @@ import { createFileRoute } from "@tanstack/react-router"; import AlertHistoryPage from "./page"; export const Route = createFileRoute("/workspace/alerting/history")({ - component: AlertHistoryPage, -}); + component: AlertHistoryPage, +}); \ No newline at end of file diff --git a/ui/app/workspace/alerting/history/page.tsx b/ui/app/workspace/alerting/history/page.tsx index a83b3eea5d4..e0a244f1513 100644 --- a/ui/app/workspace/alerting/history/page.tsx +++ b/ui/app/workspace/alerting/history/page.tsx @@ -2,4 +2,4 @@ import AlertHistoryView from "@enterprise/components/alerting/alertHistoryView"; export default function AlertHistoryPage() { return ; -} +} \ No newline at end of file diff --git a/ui/app/workspace/alerting/rules/layout.tsx b/ui/app/workspace/alerting/rules/layout.tsx index 87ff5eaac72..b87451af3a7 100644 --- a/ui/app/workspace/alerting/rules/layout.tsx +++ b/ui/app/workspace/alerting/rules/layout.tsx @@ -2,5 +2,5 @@ import { createFileRoute } from "@tanstack/react-router"; import AlertRulesPage from "./page"; export const Route = createFileRoute("/workspace/alerting/rules")({ - component: AlertRulesPage, -}); + component: AlertRulesPage, +}); \ No newline at end of file diff --git a/ui/app/workspace/alerting/rules/page.tsx b/ui/app/workspace/alerting/rules/page.tsx index 1aa65f49d42..47e98515cda 100644 --- a/ui/app/workspace/alerting/rules/page.tsx +++ b/ui/app/workspace/alerting/rules/page.tsx @@ -2,4 +2,4 @@ import AlertRulesView from "@enterprise/components/alerting/alertRulesView"; export default function AlertRulesPage() { return ; -} +} \ No newline at end of file diff --git a/ui/app/workspace/circuit-breaker/page.tsx b/ui/app/workspace/circuit-breaker/page.tsx index 471c7b5a430..833eca2c202 100644 --- a/ui/app/workspace/circuit-breaker/page.tsx +++ b/ui/app/workspace/circuit-breaker/page.tsx @@ -6,4 +6,4 @@ export default function CircuitBreakerPage() {
); -} +} \ No newline at end of file diff --git a/ui/app/workspace/cluster/page.tsx b/ui/app/workspace/cluster/page.tsx index 3f97f877693..328266aaa5c 100644 --- a/ui/app/workspace/cluster/page.tsx +++ b/ui/app/workspace/cluster/page.tsx @@ -6,4 +6,4 @@ export default function ClusterPage() {
); -} +} \ No newline at end of file diff --git a/ui/app/workspace/complexity-router/page.tsx b/ui/app/workspace/complexity-router/page.tsx index 9fb0edfed1a..c38998572c0 100644 --- a/ui/app/workspace/complexity-router/page.tsx +++ b/ui/app/workspace/complexity-router/page.tsx @@ -343,189 +343,188 @@ export default function ComplexityRouterPage() { onSubmit={handleSubmit(onValid)} noValidate > - {/* ── Page header ── */} -
-
-

Complexity Router

-

- Tune how incoming requests are classified into four tiers. Thresholds and keyword lists feed the{" "} - complexity_tier field that routing rules can - target. -

-
- -
- - {/* ── Complexity Spectrum ── */} -
-
-

Complexity Spectrum

-
- {Object.values(TIER_PALETTE).map(({ color, name }) => ( -
-
- {name} -
- ))} + {/* ── Page header ── */} +
+
+

Complexity Router

+

+ Tune how incoming requests are classified into four tiers. Thresholds and keyword lists feed the{" "} + complexity_tier field that routing rules can + target. +

+
- -
- {/* ── Tier Boundaries ── */} -
-

Tier Boundaries

- -
- {BOUNDARY_FIELDS.map(({ key, label, description, fromTier, toTier, fromColor, toColor }) => { - const fieldError = boundaryErrors?.[key]; - const inputId = `boundary-${key}`; - const errorId = `${inputId}-error`; - const { onChange, ...boundaryInputProps } = register(`tier_boundaries.${key}`, { - required: "Enter a number between 0 and 1", - setValueAs: boundaryValueAsNumber, - validate: (value) => { - if (!Number.isFinite(value)) return "Enter a number between 0 and 1"; - if (value <= 0) return "Must be greater than 0"; - if (value >= 1) return "Must be less than 1"; - const { simple_medium, medium_complex } = liveBoundaries; - if (key === "medium_complex" && Number.isFinite(simple_medium) && value <= simple_medium) { - return "Must be greater than Simple → Medium"; - } - if (key === "complex_reasoning" && Number.isFinite(medium_complex) && value <= medium_complex) { - return "Must be greater than Medium → Complex"; - } - return true; - }, - deps: - key === "simple_medium" - ? ["tier_boundaries.medium_complex"] - : key === "medium_complex" - ? ["tier_boundaries.complex_reasoning"] - : undefined, - }); - - return ( -
- {/* Tier transition label */} -
- - {fromTier} - - → - - {toTier} - + {/* ── Complexity Spectrum ── */} +
+
+

Complexity Spectrum

+
+ {Object.values(TIER_PALETTE).map(({ color, name }) => ( +
+
+ {name}
+ ))} +
+
+ +
- - { - normalizeBoundaryInput(event); - onChange(event); - }} - aria-invalid={fieldError ? true : undefined} - aria-describedby={fieldError ? errorId : undefined} - className={cn( - "h-11 text-center text-lg font-mono font-medium", - fieldError && "border-destructive focus-visible:ring-destructive", + {/* ── Tier Boundaries ── */} +
+

Tier Boundaries

+ +
+ {BOUNDARY_FIELDS.map(({ key, label, description, fromTier, toTier, fromColor, toColor }) => { + const fieldError = boundaryErrors?.[key]; + const inputId = `boundary-${key}`; + const errorId = `${inputId}-error`; + const { onChange, ...boundaryInputProps } = register(`tier_boundaries.${key}`, { + required: "Enter a number between 0 and 1", + setValueAs: boundaryValueAsNumber, + validate: (value) => { + if (!Number.isFinite(value)) return "Enter a number between 0 and 1"; + if (value <= 0) return "Must be greater than 0"; + if (value >= 1) return "Must be less than 1"; + const { simple_medium, medium_complex } = liveBoundaries; + if (key === "medium_complex" && Number.isFinite(simple_medium) && value <= simple_medium) { + return "Must be greater than Simple → Medium"; + } + if (key === "complex_reasoning" && Number.isFinite(medium_complex) && value <= medium_complex) { + return "Must be greater than Medium → Complex"; + } + return true; + }, + deps: + key === "simple_medium" + ? ["tier_boundaries.medium_complex"] + : key === "medium_complex" + ? ["tier_boundaries.complex_reasoning"] + : undefined, + }); + + return ( +
+ {/* Tier transition label */} +
+ + {fromTier} + + → + + {toTier} + +
+ + + { + normalizeBoundaryInput(event); + onChange(event); + }} + aria-invalid={fieldError ? true : undefined} + aria-describedby={fieldError ? errorId : undefined} + className={cn( + "h-11 text-center text-lg font-mono font-medium", + fieldError && "border-destructive focus-visible:ring-destructive", + )} + {...boundaryInputProps} + /> + + {fieldError ? ( +

+ {fieldError.message} +

+ ) : ( +

{description}

)} - {...boundaryInputProps} - /> - - {fieldError ? ( -

- {fieldError.message} -

- ) : ( -

{description}

- )} -
- ); - })} +
+ ); + })} +
-
- {/* ── Keyword Lists ── */} -
-
-

Keyword Lists

- - Lowercased and deduplicated on save. Each list requires at least one entry. - -
+ {/* ── Keyword Lists ── */} +
+
+

Keyword Lists

+ + Lowercased and deduplicated on save. Each list requires at least one entry. + +
-
- {KEYWORD_LIST_DEFINITIONS.map(({ key, label, description }) => { - const fieldError = keywordErrors?.[key as KeywordListKey]; - const errorId = `keywords-${key}-error`; - return ( -
- (value.length > 0 ? true : `${label} cannot be empty`) }} - render={({ field }) => ( -
-
- {label} - - {field.value.length} {field.value.length === 1 ? "entry" : "entries"} - +
+ {KEYWORD_LIST_DEFINITIONS.map(({ key, label, description }) => { + const fieldError = keywordErrors?.[key as KeywordListKey]; + const errorId = `keywords-${key}-error`; + return ( +
+ (value.length > 0 ? true : `${label} cannot be empty`) }} + render={({ field }) => ( +
+
+ {label} + + {field.value.length} {field.value.length === 1 ? "entry" : "entries"} + +
+

{description}

+ + {fieldError && ( +

+ {fieldError.message} +

+ )}
-

{description}

- - {fieldError && ( -

- {fieldError.message} -

- )} -
- )} - /> -
- ); - })} -
-
- - {/* ── Submit error ── */} - {submitError && ( -
- {submitError} + )} + /> +
+ ); + })} +
- )} + {/* ── Submit error ── */} + {submitError && ( +
+ {submitError} +
+ )} @@ -601,4 +600,4 @@ export default function ComplexityRouterPage() {
); -} +} \ No newline at end of file diff --git a/ui/app/workspace/config/api-keys/page.tsx b/ui/app/workspace/config/api-keys/page.tsx index 7e1c3e857ec..feb17bc844b 100644 --- a/ui/app/workspace/config/api-keys/page.tsx +++ b/ui/app/workspace/config/api-keys/page.tsx @@ -6,4 +6,4 @@ export default function APIKeysPage() {
); -} +} \ No newline at end of file diff --git a/ui/app/workspace/config/branding/layout.tsx b/ui/app/workspace/config/branding/layout.tsx index 0366b57a7a5..68a6fecceae 100644 --- a/ui/app/workspace/config/branding/layout.tsx +++ b/ui/app/workspace/config/branding/layout.tsx @@ -3,4 +3,4 @@ import BrandingPage from "./page"; export const Route = createFileRoute("/workspace/config/branding")({ component: BrandingPage, -}); +}); \ No newline at end of file diff --git a/ui/app/workspace/config/branding/page.tsx b/ui/app/workspace/config/branding/page.tsx index ae90d67b81b..80321d9a8d5 100644 --- a/ui/app/workspace/config/branding/page.tsx +++ b/ui/app/workspace/config/branding/page.tsx @@ -23,4 +23,4 @@ export default function BrandingPage() {
); -} +} \ No newline at end of file diff --git a/ui/app/workspace/config/caching/page.tsx b/ui/app/workspace/config/caching/page.tsx index 41c038701ea..76e8599f421 100644 --- a/ui/app/workspace/config/caching/page.tsx +++ b/ui/app/workspace/config/caching/page.tsx @@ -2,8 +2,8 @@ import CachingView from "../views/cachingView"; export default function CachingPage() { return ( -
+
); -} +} \ No newline at end of file diff --git a/ui/app/workspace/config/client-settings/page.tsx b/ui/app/workspace/config/client-settings/page.tsx index f70fc33dbf8..179c6fa66fc 100644 --- a/ui/app/workspace/config/client-settings/page.tsx +++ b/ui/app/workspace/config/client-settings/page.tsx @@ -2,8 +2,8 @@ import ClientSettingsView from "../views/clientSettingsView"; export default function ClientSettingsPage() { return ( -
+
); -} +} \ No newline at end of file diff --git a/ui/app/workspace/config/compatibility/page.tsx b/ui/app/workspace/config/compatibility/page.tsx index 6ee8b5a45b6..c6fb23698f7 100644 --- a/ui/app/workspace/config/compatibility/page.tsx +++ b/ui/app/workspace/config/compatibility/page.tsx @@ -6,4 +6,4 @@ export default function CompatibilityPage() {
); -} +} \ No newline at end of file diff --git a/ui/app/workspace/config/feature-flags/page.tsx b/ui/app/workspace/config/feature-flags/page.tsx index 519a5e23ce6..4aea3f07ab5 100644 --- a/ui/app/workspace/config/feature-flags/page.tsx +++ b/ui/app/workspace/config/feature-flags/page.tsx @@ -6,4 +6,4 @@ export default function FeatureFlagsPage() {
); -} +} \ No newline at end of file diff --git a/ui/app/workspace/config/license/page.tsx b/ui/app/workspace/config/license/page.tsx index facd2a9fd7b..1996c7087b0 100644 --- a/ui/app/workspace/config/license/page.tsx +++ b/ui/app/workspace/config/license/page.tsx @@ -21,4 +21,4 @@ export default function LicensePage() {
); -} +} \ No newline at end of file diff --git a/ui/app/workspace/config/logging/page.tsx b/ui/app/workspace/config/logging/page.tsx index 83c991ea5bd..b78edb2d0f5 100644 --- a/ui/app/workspace/config/logging/page.tsx +++ b/ui/app/workspace/config/logging/page.tsx @@ -2,8 +2,8 @@ import LoggingView from "../views/loggingView"; export default function LoggingPage() { return ( -
+
); -} +} \ No newline at end of file diff --git a/ui/app/workspace/config/performance-tuning/page.tsx b/ui/app/workspace/config/performance-tuning/page.tsx index 35802cf35db..736ff1a507a 100644 --- a/ui/app/workspace/config/performance-tuning/page.tsx +++ b/ui/app/workspace/config/performance-tuning/page.tsx @@ -6,4 +6,4 @@ export default function PerformanceTuningPage() {
); -} +} \ No newline at end of file diff --git a/ui/app/workspace/config/proxy/page.tsx b/ui/app/workspace/config/proxy/page.tsx index c0fa78786e9..ce63e5817b6 100644 --- a/ui/app/workspace/config/proxy/page.tsx +++ b/ui/app/workspace/config/proxy/page.tsx @@ -17,8 +17,8 @@ export default function ProxyPage() { } return ( -
+
); -} +} \ No newline at end of file diff --git a/ui/app/workspace/config/security/page.tsx b/ui/app/workspace/config/security/page.tsx index 53cb30e5868..93bf512802c 100644 --- a/ui/app/workspace/config/security/page.tsx +++ b/ui/app/workspace/config/security/page.tsx @@ -2,8 +2,8 @@ import SecurityView from "../views/securityView"; export default function SecurityPage() { return ( -
+
); -} +} \ No newline at end of file diff --git a/ui/app/workspace/config/views/clientSettingsView.tsx b/ui/app/workspace/config/views/clientSettingsView.tsx index 01bff3bec9e..68d815119a6 100644 --- a/ui/app/workspace/config/views/clientSettingsView.tsx +++ b/ui/app/workspace/config/views/clientSettingsView.tsx @@ -591,4 +591,4 @@ export default function ClientSettingsView() {
); -} +} \ No newline at end of file diff --git a/ui/app/workspace/config/views/loggingView.tsx b/ui/app/workspace/config/views/loggingView.tsx index 89d3395617f..50a8bf4dcab 100644 --- a/ui/app/workspace/config/views/loggingView.tsx +++ b/ui/app/workspace/config/views/loggingView.tsx @@ -273,8 +273,8 @@ export default function LoggingView() {

Comma-separated list of request headers to capture in log metadata. Supports exact names and wildcard patterns (e.g.{" "} x-custom-* captures all headers with that prefix, * logs all - headers; note that * will capture sensitive headers like Authorization). Values are - extracted from incoming requests and stored in the metadata field of log entries. Headers with the{" "} + headers; note that * will capture sensitive headers like Authorization). Values are extracted + from incoming requests and stored in the metadata field of log entries. Headers with the{" "} x-bf-lh- prefix are always captured automatically.