diff --git a/.changeset/warm-falcons-joke.md b/.changeset/warm-falcons-joke.md
deleted file mode 100644
index 845473c1a..000000000
--- a/.changeset/warm-falcons-joke.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"@trigger.dev/build": patch
----
-
-Add syncSupabaseEnvVars to pull database connection strings and save them as trigger.dev environment variables
diff --git a/.server-changes/add-errors-page.md b/.server-changes/add-errors-page.md
deleted file mode 100644
index cdb2a1b6c..000000000
--- a/.server-changes/add-errors-page.md
+++ /dev/null
@@ -1,11 +0,0 @@
----
-area: webapp
-type: feature
----
-
-A new Errors page for viewing and tracking errors that cause runs to fail
-
-- Errors are grouped using error fingerprinting
-- View top errors for a time period, filter by task, or search the text
-- View occurrences over time
-- View all the runs for an error and bulk replay them
diff --git a/.server-changes/dev-cli-disconnect-md b/.server-changes/dev-cli-disconnect-md
new file mode 100644
index 000000000..a0790d707
--- /dev/null
+++ b/.server-changes/dev-cli-disconnect-md
@@ -0,0 +1,6 @@
+---
+area: webapp
+type: feature
+---
+
+Added `/engine/v1/dev/disconnect` endpoint to auto-cancel runs when the CLI disconnects. Maximum of 500 runs can be cancelled. Uses the bulk action system when there are more than 25 runs to cancel.
\ No newline at end of file
diff --git a/.server-changes/test-page-sidebar-tabs.md b/.server-changes/test-page-sidebar-tabs.md
deleted file mode 100644
index 1803f2c2a..000000000
--- a/.server-changes/test-page-sidebar-tabs.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-area: webapp
-type: feature
----
-
-Add sidebar tabs (Options, AI, Schema) to the Test page for schemaTask payload generation and schema viewing.
diff --git a/apps/webapp/app/components/onboarding/TechnologyPicker.tsx b/apps/webapp/app/components/onboarding/TechnologyPicker.tsx
index 3d822e4b6..9c9f9a6b2 100644
--- a/apps/webapp/app/components/onboarding/TechnologyPicker.tsx
+++ b/apps/webapp/app/components/onboarding/TechnologyPicker.tsx
@@ -210,11 +210,22 @@ export function TechnologyPicker({
const addCustomValue = useCallback(() => {
const trimmed = otherInputValue.trim();
- if (trimmed && !customValues.includes(trimmed) && !value.includes(trimmed)) {
+ if (!trimmed) return;
+
+ const matchedOption = TECHNOLOGY_OPTIONS.find(
+ (opt) => opt.toLowerCase() === trimmed.toLowerCase()
+ );
+
+ if (matchedOption) {
+ if (!value.includes(matchedOption)) {
+ onChange([...value, matchedOption]);
+ }
+ } else if (!customValues.includes(trimmed) && !value.includes(trimmed)) {
onCustomValuesChange([...customValues, trimmed]);
- setOtherInputValue("");
}
- }, [otherInputValue, customValues, onCustomValuesChange, value]);
+
+ setOtherInputValue("");
+ }, [otherInputValue, customValues, onCustomValuesChange, value, onChange]);
const handleOtherKeyDown = useCallback(
(e: React.KeyboardEvent) => {
diff --git a/apps/webapp/app/components/primitives/Buttons.tsx b/apps/webapp/app/components/primitives/Buttons.tsx
index 5c52a5d95..8ba196b5d 100644
--- a/apps/webapp/app/components/primitives/Buttons.tsx
+++ b/apps/webapp/app/components/primitives/Buttons.tsx
@@ -1,10 +1,18 @@
import { Link, type LinkProps, NavLink, type NavLinkProps } from "@remix-run/react";
-import React, { forwardRef, type ReactNode, useImperativeHandle, useRef } from "react";
+import React, {
+ forwardRef,
+ type ReactNode,
+ useEffect,
+ useImperativeHandle,
+ useRef,
+ useState,
+} from "react";
import { type ShortcutDefinition, useShortcutKeys } from "~/hooks/useShortcutKeys";
import { cn } from "~/utils/cn";
import { ShortcutKey } from "./ShortcutKey";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./Tooltip";
import { Icon, type RenderIcon } from "./Icon";
+import { Spinner } from "./Spinner";
const sizes = {
small: {
@@ -180,6 +188,7 @@ export type ButtonContentPropsType = {
tooltip?: ReactNode;
iconSpacing?: string;
hideShortcutKey?: boolean;
+ isLoading?: boolean;
};
export function ButtonContent(props: ButtonContentPropsType) {
@@ -196,7 +205,19 @@ export function ButtonContent(props: ButtonContentPropsType) {
tooltip,
iconSpacing,
hideShortcutKey,
+ isLoading,
} = props;
+
+ const [showSpinner, setShowSpinner] = useState(false);
+ useEffect(() => {
+ if (!isLoading) {
+ setShowSpinner(false);
+ return;
+ }
+ const timer = setTimeout(() => setShowSpinner(true), 200);
+ return () => clearTimeout(timer);
+ }, [isLoading]);
+
const variation = allVariants.variant[props.variant];
const btnClassName = cn(allVariants.$all, variation.button);
@@ -217,56 +238,64 @@ export function ButtonContent(props: ButtonContentPropsType) {
const buttonContent = (
-
+
+ {LeadingIcon && (
+
+ )}
+
+ {text &&
+ (typeof text === "string" ? (
+
+ {text}
+
+ ) : (
+ <>{text}>
+ ))}
+
+ {shortcut &&
+ !tooltip &&
+ props.shortcutPosition === "before-trailing-icon" &&
+ renderShortcutKey()}
+
+ {TrailingIcon && (
+
+ )}
+
+ {shortcut &&
+ !tooltip &&
+ (!props.shortcutPosition || props.shortcutPosition === "after-trailing-icon") &&
+ renderShortcutKey()}
+
+ {showSpinner && (
+
+
+
)}
- >
- {LeadingIcon && (
-
- )}
-
- {text &&
- (typeof text === "string" ? (
-
- {text}
-
- ) : (
- <>{text}>
- ))}
-
- {shortcut &&
- !tooltip &&
- props.shortcutPosition === "before-trailing-icon" &&
- renderShortcutKey()}
-
- {TrailingIcon && (
-
- )}
-
- {shortcut &&
- !tooltip &&
- (!props.shortcutPosition || props.shortcutPosition === "after-trailing-icon") &&
- renderShortcutKey()}
);
@@ -298,6 +327,8 @@ export const Button = forwardRef(
const innerRef = useRef(null);
useImperativeHandle(ref, () => innerRef.current as HTMLButtonElement);
+ const isDisabled = disabled || props.isLoading;
+
useShortcutKeys({
shortcut: props.shortcut,
action: (e) => {
@@ -307,14 +338,14 @@ export const Button = forwardRef(
e.stopPropagation();
}
},
- disabled: disabled || !props.shortcut,
+ disabled: isDisabled || !props.shortcut,
});
return (