diff --git a/.agents/skills/add-enrichment/SKILL.md b/.agents/skills/add-enrichment/SKILL.md index 7b34e4c7c38..44c3e9f95da 100644 --- a/.agents/skills/add-enrichment/SKILL.md +++ b/.agents/skills/add-enrichment/SKILL.md @@ -63,7 +63,7 @@ Why it matters: the cascade runner only bills (and only reads `output.cost.total Create `apps/sim/enrichments/{name}/{name}.ts` and a barrel `index.ts`. Mirror the existing entries (`work-email`, `phone-number`, `company-domain`, `company-info`). ```typescript -import { SomeIcon } from 'lucide-react' +import { SomeIcon } from '@sim/emcn/icons' import { filterUndefined } from '@sim/utils/object' import { normalizeDomain, splitName, str, toolProvider } from '@/enrichments/providers' import type { EnrichmentConfig } from '@/enrichments/types' @@ -109,7 +109,7 @@ export { myEnrichment } from './my-enrichment' ``` Rules: -- Keep the file **client-safe**: import only `lucide-react`, `@sim/utils/*`, `@/enrichments/providers`, and the types. **Never import `@/tools`** here — the runner does the tool call. +- Keep the file **client-safe**: import only `@sim/emcn/icons`, `@sim/utils/*`, `@/enrichments/providers`, and the types. **Never import `@/tools`** here — the runner does the tool call. - `buildParams` returns `null` when inputs are insufficient (provider skipped). `mapOutput` returns `null`/empty for a miss (falls through). Use `filterUndefined` when assembling optional tool params; coerce numbers explicitly (don't pass `''` to number outputs). - Output `id`s are the keys `mapOutput` returns; output `name`s are the default column names (the user can rename them in the config). diff --git a/.agents/skills/emcn-design-review/SKILL.md b/.agents/skills/emcn-design-review/SKILL.md index 89ae8d47843..78253e5e772 100644 --- a/.agents/skills/emcn-design-review/SKILL.md +++ b/.agents/skills/emcn-design-review/SKILL.md @@ -18,7 +18,7 @@ This codebase uses **emcn**, a custom component library built on Radix UI primit ## Steps -1. Read the emcn public barrel at `apps/sim/components/emcn/index.ts` (re-exports components, Calendar, Table*, and icons) to know what's available; for the full icon set read `apps/sim/components/emcn/icons/index.ts` +1. Read the emcn public barrel at `packages/emcn/src/index.ts` (re-exports components, Calendar, Table*, and icons) to know what's available; for the full icon set read `packages/emcn/src/icons/index.ts` 2. Read `apps/sim/app/_styles/globals.css` for CSS variable tokens 3. Analyze the specified scope against every rule below 4. If fix=true, apply the fixes. If fix=false, propose the fixes without applying. @@ -28,7 +28,7 @@ This codebase uses **emcn**, a custom component library built on Radix UI primit ## Imports - Import from `@/components/emcn` barrel, never subpaths -- Icons from `@/components/emcn/icons` or `lucide-react` +- Icons from `@sim/emcn/icons` - Use `cn` from `@/lib/core/utils/cn` for conditional classes ## Design Tokens @@ -45,7 +45,7 @@ Use CSS variable pattern (`text-[var(--text-primary)]`), never Tailwind semantic ## Buttons -Intent-to-variant mapping (read the actual `buttonVariants` in `apps/sim/components/emcn/components/button/button.tsx` for the full variant set — it exposes more than listed here): +Intent-to-variant mapping (read the actual `buttonVariants` in `packages/emcn/src/components/button/button.tsx` for the full variant set — it exposes more than listed here): | Action | Variant | |--------|---------| diff --git a/.claude/commands/add-enrichment.md b/.claude/commands/add-enrichment.md index b0beef265cf..c23d001f70a 100644 --- a/.claude/commands/add-enrichment.md +++ b/.claude/commands/add-enrichment.md @@ -62,7 +62,7 @@ Why it matters: the cascade runner only bills (and only reads `output.cost.total Create `apps/sim/enrichments/{name}/{name}.ts` and a barrel `index.ts`. Mirror the existing entries (`work-email`, `phone-number`, `company-domain`, `company-info`). ```typescript -import { SomeIcon } from 'lucide-react' +import { SomeIcon } from '@sim/emcn/icons' import { filterUndefined } from '@sim/utils/object' import { normalizeDomain, splitName, str, toolProvider } from '@/enrichments/providers' import type { EnrichmentConfig } from '@/enrichments/types' @@ -108,7 +108,7 @@ export { myEnrichment } from './my-enrichment' ``` Rules: -- Keep the file **client-safe**: import only `lucide-react`, `@sim/utils/*`, `@/enrichments/providers`, and the types. **Never import `@/tools`** here — the runner does the tool call. +- Keep the file **client-safe**: import only `@sim/emcn/icons`, `@sim/utils/*`, `@/enrichments/providers`, and the types. **Never import `@/tools`** here — the runner does the tool call. - `buildParams` returns `null` when inputs are insufficient (provider skipped). `mapOutput` returns `null`/empty for a miss (falls through). Use `filterUndefined` when assembling optional tool params; coerce numbers explicitly (don't pass `''` to number outputs). - Output `id`s are the keys `mapOutput` returns; output `name`s are the default column names (the user can rename them in the config). diff --git a/.claude/commands/emcn-design-review.md b/.claude/commands/emcn-design-review.md index 741c02c64b9..1a5c562facd 100644 --- a/.claude/commands/emcn-design-review.md +++ b/.claude/commands/emcn-design-review.md @@ -17,7 +17,7 @@ This codebase uses **emcn**, a custom component library built on Radix UI primit ## Steps -1. Read the emcn public barrel at `apps/sim/components/emcn/index.ts` (re-exports components, Calendar, Table*, and icons) to know what's available; for the full icon set read `apps/sim/components/emcn/icons/index.ts` +1. Read the emcn public barrel at `packages/emcn/src/index.ts` (re-exports components, Calendar, Table*, and icons) to know what's available; for the full icon set read `packages/emcn/src/icons/index.ts` 2. Read `apps/sim/app/_styles/globals.css` for CSS variable tokens 3. Analyze the specified scope against every rule below 4. If fix=true, apply the fixes. If fix=false, propose the fixes without applying. @@ -27,7 +27,7 @@ This codebase uses **emcn**, a custom component library built on Radix UI primit ## Imports - Import from `@/components/emcn` barrel, never subpaths -- Icons from `@/components/emcn/icons` or `lucide-react` +- Icons from `@sim/emcn/icons` - Use `cn` from `@/lib/core/utils/cn` for conditional classes ## Design Tokens @@ -44,7 +44,7 @@ Use CSS variable pattern (`text-[var(--text-primary)]`), never Tailwind semantic ## Buttons -Intent-to-variant mapping (read the actual `buttonVariants` in `apps/sim/components/emcn/components/button/button.tsx` for the full variant set — it exposes more than listed here): +Intent-to-variant mapping (read the actual `buttonVariants` in `packages/emcn/src/components/button/button.tsx` for the full variant set — it exposes more than listed here): | Action | Variant | |--------|---------| diff --git a/.claude/rules/emcn-components.md b/.claude/rules/emcn-components.md index 23491d6aaaf..5ca3d2a8dcb 100644 --- a/.claude/rules/emcn-components.md +++ b/.claude/rules/emcn-components.md @@ -1,6 +1,6 @@ --- paths: - - "apps/sim/components/emcn/**" + - "packages/emcn/**" --- # EMCN Components @@ -20,7 +20,7 @@ The menu surface intentionally diverges from the pill: `dropdown-menu.tsx` items ## Component catalogue -- **`Chip` / `ChipLink`** — the pill button (` )} diff --git a/apps/docs/components/docs-layout/page-footer.tsx b/apps/docs/components/docs-layout/page-footer.tsx index c0796c1f359..7031211fe84 100644 --- a/apps/docs/components/docs-layout/page-footer.tsx +++ b/apps/docs/components/docs-layout/page-footer.tsx @@ -1,5 +1,5 @@ import type { ReactNode } from 'react' -import { ChevronLeft, ChevronRight } from 'lucide-react' +import { ChevronLeft, ChevronRight } from '@sim/emcn/icons' import Link from 'next/link' interface PageNeighbour { diff --git a/apps/docs/components/docs-layout/page-navigation-arrows.tsx b/apps/docs/components/docs-layout/page-navigation-arrows.tsx index 4701e2876a4..2aa973aa21e 100644 --- a/apps/docs/components/docs-layout/page-navigation-arrows.tsx +++ b/apps/docs/components/docs-layout/page-navigation-arrows.tsx @@ -1,6 +1,6 @@ 'use client' -import { ChevronLeft, ChevronRight } from 'lucide-react' +import { ChevronLeft, ChevronRight } from '@sim/emcn/icons' import Link from 'next/link' interface PageNavigationArrowsProps { diff --git a/apps/docs/components/page-actions.tsx b/apps/docs/components/page-actions.tsx index fe65e08e386..53c92c8d087 100644 --- a/apps/docs/components/page-actions.tsx +++ b/apps/docs/components/page-actions.tsx @@ -1,8 +1,8 @@ 'use client' import { Chip } from '@sim/emcn' +import { Check, Duplicate } from '@sim/emcn/icons' import { useCopyButton } from 'fumadocs-ui/utils/use-copy-button' -import { Check, Copy } from 'lucide-react' export function LLMCopyButton({ content }: { content: string }) { const [checked, onClick] = useCopyButton(() => navigator.clipboard.writeText(content)) @@ -10,7 +10,7 @@ export function LLMCopyButton({ content }: { content: string }) { return ( {checked ? 'Copied' : 'Copy page'} diff --git a/apps/docs/components/ui/code-block.tsx b/apps/docs/components/ui/code-block.tsx index 839a87ebff4..2d7b3c450f5 100644 --- a/apps/docs/components/ui/code-block.tsx +++ b/apps/docs/components/ui/code-block.tsx @@ -1,8 +1,8 @@ 'use client' import { useState } from 'react' +import { Check, Duplicate } from '@sim/emcn/icons' import { CodeBlock as FumadocsCodeBlock } from 'fumadocs-ui/components/codeblock' -import { Check, Copy } from 'lucide-react' import { cn } from '@/lib/utils' export function CodeBlock(props: React.ComponentProps) { @@ -31,9 +31,9 @@ export function CodeBlock(props: React.ComponentProps) > {copied ? ( - + ) : ( - + )} diff --git a/apps/docs/components/ui/faq.tsx b/apps/docs/components/ui/faq.tsx index da6cd6cd2b5..6b2d241b59b 100644 --- a/apps/docs/components/ui/faq.tsx +++ b/apps/docs/components/ui/faq.tsx @@ -1,7 +1,7 @@ 'use client' import { useState } from 'react' -import { ChevronRight } from 'lucide-react' +import { ChevronRight } from '@sim/emcn/icons' import { serializeJsonLd } from '@/lib/json-ld' import { cn } from '@/lib/utils' diff --git a/apps/docs/components/ui/heading.tsx b/apps/docs/components/ui/heading.tsx index b012e9f00ce..cd259daa121 100644 --- a/apps/docs/components/ui/heading.tsx +++ b/apps/docs/components/ui/heading.tsx @@ -1,7 +1,7 @@ 'use client' import { type ComponentPropsWithoutRef, useState } from 'react' -import { Check, Link } from 'lucide-react' +import { Check, Link } from '@sim/emcn/icons' import { cn } from '@/lib/utils' type HeadingTag = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' diff --git a/apps/docs/components/ui/response-section.tsx b/apps/docs/components/ui/response-section.tsx index c19353a1065..44a7c0ebf08 100644 --- a/apps/docs/components/ui/response-section.tsx +++ b/apps/docs/components/ui/response-section.tsx @@ -1,7 +1,7 @@ 'use client' import { useEffect, useRef, useState } from 'react' -import { ChevronDown } from 'lucide-react' +import { ChevronDown } from '@sim/emcn/icons' import { cn } from '@/lib/utils' interface ResponseSectionProps { diff --git a/apps/docs/components/ui/search-trigger.tsx b/apps/docs/components/ui/search-trigger.tsx index 7e222fac26b..2577f6a2585 100644 --- a/apps/docs/components/ui/search-trigger.tsx +++ b/apps/docs/components/ui/search-trigger.tsx @@ -6,7 +6,7 @@ import { chipGeometryClass, TRIGGER_BORDER_CLASS, } from '@sim/emcn' -import { Search } from 'lucide-react' +import { Search } from '@sim/emcn/icons' import { cn } from '@/lib/utils' export function SearchTrigger() { diff --git a/apps/docs/components/workflow-preview/block-icons.tsx b/apps/docs/components/workflow-preview/block-icons.tsx index b759375a56b..aa2f297d619 100644 --- a/apps/docs/components/workflow-preview/block-icons.tsx +++ b/apps/docs/components/workflow-preview/block-icons.tsx @@ -1,5 +1,5 @@ import type { ComponentType, SVGProps } from 'react' -import { Clock, Database, Layers, Repeat, Table } from 'lucide-react' +import { Clock, Database, Repeat, Split, Table } from '@sim/emcn/icons' import { ApiIcon, ChartBarIcon, @@ -21,7 +21,7 @@ import { blockTypeToIconMap } from '@/components/ui/icon-mapping' /** * The two Sim-specific block glyphs we need, ported verbatim from * `apps/sim/components/icons.tsx` so the preview matches the real builder. - * Other block types fall back to lucide-react stand-ins for now. + * Other block types fall back to `@sim/emcn/icons` stand-ins for now. */ export function StartIcon(props: SVGProps) { return ( @@ -76,7 +76,7 @@ export function AgentIcon(props: SVGProps) { ) } -/** Block type → glyph. Brand glyphs from the app for core blocks; lucide stand-ins for the rest. */ +/** Block type → glyph. Brand glyphs from the app for core blocks; `@sim/emcn/icons` stand-ins for the rest. */ export const BLOCK_ICONS: Record> = { starter: StartIcon, start_trigger: StartIcon, @@ -97,7 +97,7 @@ export const BLOCK_ICONS: Record openWith(null)} className='absolute top-2 right-2 z-10 flex size-[28px] items-center justify-center rounded-[6px] border border-[var(--border-1)] bg-[var(--surface-4)] text-[var(--text-muted)] opacity-0 transition-opacity duration-150 hover:text-[var(--text-primary)] group-hover:opacity-100' > - + diff --git a/apps/docs/content/docs/de/copilot/index.mdx b/apps/docs/content/docs/de/copilot/index.mdx index e9a92f7e209..4ccdbc8b4c7 100644 --- a/apps/docs/content/docs/de/copilot/index.mdx +++ b/apps/docs/content/docs/de/copilot/index.mdx @@ -5,7 +5,7 @@ title: Copilot import { Callout } from 'fumadocs-ui/components/callout' import { Card, Cards } from 'fumadocs-ui/components/card' import { Image } from '@/components/ui/image' -import { MessageCircle, Package, Zap, Infinity as InfinityIcon, Brain, BrainCircuit } from 'lucide-react' +import { Box, Brain, BrainCircuit, InfinityIcon, Task, Zap } from '@sim/emcn/icons' Copilot ist dein Assistent im Editor, der dir hilft, Workflows mit Sim Copilot zu erstellen und zu bearbeiten sowie diese zu verstehen und zu verbessern. Er kann: @@ -48,7 +48,7 @@ Diese kontextbezogenen Informationen helfen Copilot, genauere und relevantere Un - + Fragen } @@ -60,7 +60,7 @@ Diese kontextbezogenen Informationen helfen Copilot, genauere und relevantere Un - + Agent } diff --git a/apps/docs/content/docs/es/copilot/index.mdx b/apps/docs/content/docs/es/copilot/index.mdx index 4752570179b..49d5cc2b717 100644 --- a/apps/docs/content/docs/es/copilot/index.mdx +++ b/apps/docs/content/docs/es/copilot/index.mdx @@ -5,7 +5,7 @@ title: Copilot import { Callout } from 'fumadocs-ui/components/callout' import { Card, Cards } from 'fumadocs-ui/components/card' import { Image } from '@/components/ui/image' -import { MessageCircle, Package, Zap, Infinity as InfinityIcon, Brain, BrainCircuit } from 'lucide-react' +import { Box, Brain, BrainCircuit, InfinityIcon, Task, Zap } from '@sim/emcn/icons' Copilot es tu asistente integrado en el editor que te ayuda a crear y editar flujos de trabajo con Sim Copilot, así como a entenderlos y mejorarlos. Puede: @@ -48,7 +48,7 @@ Esta información contextual ayuda a Copilot a proporcionar asistencia más prec - + Preguntar } @@ -60,7 +60,7 @@ Esta información contextual ayuda a Copilot a proporcionar asistencia más prec - + Agente } diff --git a/apps/docs/content/docs/fr/copilot/index.mdx b/apps/docs/content/docs/fr/copilot/index.mdx index 53dc6c352c5..4a04abb26ed 100644 --- a/apps/docs/content/docs/fr/copilot/index.mdx +++ b/apps/docs/content/docs/fr/copilot/index.mdx @@ -5,7 +5,7 @@ title: Copilot import { Callout } from 'fumadocs-ui/components/callout' import { Card, Cards } from 'fumadocs-ui/components/card' import { Image } from '@/components/ui/image' -import { MessageCircle, Package, Zap, Infinity as InfinityIcon, Brain, BrainCircuit } from 'lucide-react' +import { Box, Brain, BrainCircuit, InfinityIcon, Task, Zap } from '@sim/emcn/icons' Copilot est votre assistant intégré à l'éditeur qui vous aide à créer et à modifier des flux de travail avec Sim Copilot, ainsi qu'à les comprendre et à les améliorer. Il peut : @@ -48,7 +48,7 @@ Ces informations contextuelles aident Copilot à fournir une assistance plus pr - + Demander } @@ -60,7 +60,7 @@ Ces informations contextuelles aident Copilot à fournir une assistance plus pr - + Agent } diff --git a/apps/docs/content/docs/ja/copilot/index.mdx b/apps/docs/content/docs/ja/copilot/index.mdx index d38f9a42cb2..e586d36c25f 100644 --- a/apps/docs/content/docs/ja/copilot/index.mdx +++ b/apps/docs/content/docs/ja/copilot/index.mdx @@ -5,7 +5,7 @@ title: Copilot import { Callout } from 'fumadocs-ui/components/callout' import { Card, Cards } from 'fumadocs-ui/components/card' import { Image } from '@/components/ui/image' -import { MessageCircle, Package, Zap, Infinity as InfinityIcon, Brain, BrainCircuit } from 'lucide-react' +import { Box, Brain, BrainCircuit, InfinityIcon, Task, Zap } from '@sim/emcn/icons' Copilotはエディター内のアシスタントで、Sim Copilotを使用してワークフローの構築や編集を支援し、それらを理解して改善するのに役立ちます。以下のことができます: @@ -48,7 +48,7 @@ Copilotはエディター内のアシスタントで、Sim Copilotを使用し - + 質問 } @@ -60,7 +60,7 @@ Copilotはエディター内のアシスタントで、Sim Copilotを使用し - + エージェント } diff --git a/apps/docs/content/docs/zh/copilot/index.mdx b/apps/docs/content/docs/zh/copilot/index.mdx index b37b1cdc6dc..38fdf56c9f7 100644 --- a/apps/docs/content/docs/zh/copilot/index.mdx +++ b/apps/docs/content/docs/zh/copilot/index.mdx @@ -5,7 +5,7 @@ title: Copilot import { Callout } from 'fumadocs-ui/components/callout' import { Card, Cards } from 'fumadocs-ui/components/card' import { Image } from '@/components/ui/image' -import { MessageCircle, Package, Zap, Infinity as InfinityIcon, Brain, BrainCircuit } from 'lucide-react' +import { Box, Brain, BrainCircuit, InfinityIcon, Task, Zap } from '@sim/emcn/icons' Copilot 是您编辑器中的助手,可帮助您使用 Sim Copilot 构建和编辑工作流,同时理解和改进它们。它可以: @@ -48,7 +48,7 @@ Copilot 是您编辑器中的助手,可帮助您使用 Sim Copilot 构建和 - + 提问 } @@ -60,7 +60,7 @@ Copilot 是您编辑器中的助手,可帮助您使用 Sim Copilot 构建和 - + 代理 } diff --git a/apps/docs/package.json b/apps/docs/package.json index 864b61681e4..2bcc1ec4ebe 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -30,7 +30,6 @@ "fumadocs-mdx": "14.3.2", "fumadocs-openapi": "10.8.1", "fumadocs-ui": "16.8.5", - "lucide-react": "^0.511.0", "next": "16.2.12", "next-themes": "^0.4.6", "react": "19.2.4", diff --git a/apps/sim/app/(auth)/components/auth-submit-button.tsx b/apps/sim/app/(auth)/components/auth-submit-button.tsx index ae9004b8188..5d37ecb12ea 100644 --- a/apps/sim/app/(auth)/components/auth-submit-button.tsx +++ b/apps/sim/app/(auth)/components/auth-submit-button.tsx @@ -32,7 +32,6 @@ export function AuthSubmitButton({ onClick={onClick} disabled={disabled || loading} fullWidth - flush className={AUTH_BUTTON_CLASS} > {loading ? ( diff --git a/apps/sim/app/(auth)/components/password-input.tsx b/apps/sim/app/(auth)/components/password-input.tsx index d602b24e25e..f62e9c23ad3 100644 --- a/apps/sim/app/(auth)/components/password-input.tsx +++ b/apps/sim/app/(auth)/components/password-input.tsx @@ -2,7 +2,7 @@ import { useState } from 'react' import { ChipInput, type ChipInputProps, cn } from '@sim/emcn' -import { Eye, EyeOff } from 'lucide-react' +import { Eye, EyeOff } from '@sim/emcn/icons' import { AUTH_CONTROL_HEIGHT } from '@/app/(auth)/components/constants' type PasswordInputProps = Omit diff --git a/apps/sim/app/(auth)/components/social-login-buttons.tsx b/apps/sim/app/(auth)/components/social-login-buttons.tsx index f315e52da97..c2156e7dd3f 100644 --- a/apps/sim/app/(auth)/components/social-login-buttons.tsx +++ b/apps/sim/app/(auth)/components/social-login-buttons.tsx @@ -73,7 +73,6 @@ export function SocialLoginButtons({ const githubButton = ( - {showPassword ? : } + {showPassword ? ( + + ) : ( + + )}
= 15} className='size-[28px] rounded-full p-0' > - + @@ -273,7 +273,7 @@ export const ChatInput: React.FC<{ aria-label='Send message' className='size-[28px] rounded-full p-0' > - + )}
diff --git a/apps/sim/app/(interfaces)/chat/components/message-container/message-container.tsx b/apps/sim/app/(interfaces)/chat/components/message-container/message-container.tsx index 12a55db00b1..cf70ace756a 100644 --- a/apps/sim/app/(interfaces)/chat/components/message-container/message-container.tsx +++ b/apps/sim/app/(interfaces)/chat/components/message-container/message-container.tsx @@ -2,7 +2,7 @@ import type { Ref, RefObject } from 'react' import { Button } from '@sim/emcn' -import { ArrowDown } from 'lucide-react' +import { ArrowDown } from '@sim/emcn/icons' import { type ChatMessage, ClientChatMessage, diff --git a/apps/sim/app/(interfaces)/chat/components/message/components/file-download.tsx b/apps/sim/app/(interfaces)/chat/components/message/components/file-download.tsx index 8dfebdc3fda..cd4714b9248 100644 --- a/apps/sim/app/(interfaces)/chat/components/message/components/file-download.tsx +++ b/apps/sim/app/(interfaces)/chat/components/message/components/file-download.tsx @@ -2,9 +2,9 @@ import { useState } from 'react' import { Button, Download, Loader } from '@sim/emcn' +import { Music } from '@sim/emcn/icons' import { createLogger } from '@sim/logger' import { sleep } from '@sim/utils/helpers' -import { Music } from 'lucide-react' import { DefaultFileIcon, getDocumentIcon } from '@/components/icons/document-icons' import { isSafeHttpUrl } from '@/lib/core/utils/urls' import type { ChatFile } from '@/app/(interfaces)/chat/components/message/message' diff --git a/apps/sim/app/(interfaces)/chat/components/message/components/markdown-renderer.tsx b/apps/sim/app/(interfaces)/chat/components/message/components/markdown-renderer.tsx index 3229631d24c..6bb25c54649 100644 --- a/apps/sim/app/(interfaces)/chat/components/message/components/markdown-renderer.tsx +++ b/apps/sim/app/(interfaces)/chat/components/message/components/markdown-renderer.tsx @@ -83,8 +83,8 @@ const COMPONENTS = { } return ( -
-
+
+
{codeProps.className?.replace('language-', '') || 'code'} @@ -107,12 +107,12 @@ const COMPONENTS = { ), blockquote: ({ children }: React.HTMLAttributes) => ( -
+
{children}
), - hr: () =>
, + hr: () =>
, a: ({ href, children, ...props }: React.AnchorHTMLAttributes) => ( @@ -131,10 +131,10 @@ const COMPONENTS = { {children} ), tbody: ({ children }: React.HTMLAttributes) => ( - {children} + {children} ), tr: ({ children }: React.HTMLAttributes) => ( - + {children} ), diff --git a/apps/sim/app/(interfaces)/chat/components/message/message.tsx b/apps/sim/app/(interfaces)/chat/components/message/message.tsx index f22753168f5..330130de659 100644 --- a/apps/sim/app/(interfaces)/chat/components/message/message.tsx +++ b/apps/sim/app/(interfaces)/chat/components/message/message.tsx @@ -2,7 +2,7 @@ import { memo, useState } from 'react' import { Button, Check, cn, Duplicate, handleKeyboardActivation, Tooltip } from '@sim/emcn' -import { File as FileIcon, FileText, Image as ImageIcon } from 'lucide-react' +import { File as FileIcon, FileText, ImageUp as ImageIcon } from '@sim/emcn/icons' import { AgentStreamThinkingChrome, AgentStreamToolCallsChrome, @@ -273,11 +273,7 @@ export const ClientChatMessage = memo(function ClientChatMessage({ setTimeout(() => setIsCopied(false), 2000) }} > - {isCopied ? ( - - ) : ( - - )} + {isCopied ? : } diff --git a/apps/sim/app/(interfaces)/resume/[workflowId]/[executionId]/resume-page-client.tsx b/apps/sim/app/(interfaces)/resume/[workflowId]/[executionId]/resume-page-client.tsx index 51de190af68..c7cc61cff7d 100644 --- a/apps/sim/app/(interfaces)/resume/[workflowId]/[executionId]/resume-page-client.tsx +++ b/apps/sim/app/(interfaces)/resume/[workflowId]/[executionId]/resume-page-client.tsx @@ -18,9 +18,9 @@ import { TableRow, Tooltip, } from '@sim/emcn' +import { RefreshCw } from '@sim/emcn/icons' import { formatDateTime } from '@sim/utils/formatting' import { useQueryClient } from '@tanstack/react-query' -import { RefreshCw } from 'lucide-react' import { useRouter } from 'next/navigation' import { type PauseContextDetail, diff --git a/apps/sim/app/(landing)/changelog/components/changelog-actions/changelog-actions.tsx b/apps/sim/app/(landing)/changelog/components/changelog-actions/changelog-actions.tsx index 8bfb499b774..d5713c641f9 100644 --- a/apps/sim/app/(landing)/changelog/components/changelog-actions/changelog-actions.tsx +++ b/apps/sim/app/(landing)/changelog/components/changelog-actions/changelog-actions.tsx @@ -1,7 +1,7 @@ 'use client' import { ChipLink } from '@sim/emcn' -import { BookOpen, Rss } from 'lucide-react' +import { BookOpen, Rss } from '@sim/emcn/icons' import { GithubOutlineIcon } from '@/components/icons' /** diff --git a/apps/sim/app/(landing)/changelog/components/changelog-timeline/changelog-timeline.tsx b/apps/sim/app/(landing)/changelog/components/changelog-timeline/changelog-timeline.tsx index a806a28bb8c..030613e1a52 100644 --- a/apps/sim/app/(landing)/changelog/components/changelog-timeline/changelog-timeline.tsx +++ b/apps/sim/app/(landing)/changelog/components/changelog-timeline/changelog-timeline.tsx @@ -210,7 +210,7 @@ export function ChangelogTimeline({ initialEntries }: ChangelogTimelineProps) { {!done ? (
- + {loading ? 'Loading…' : 'Show more'}
diff --git a/apps/sim/app/(landing)/components/auth-modal/auth-modal.tsx b/apps/sim/app/(landing)/components/auth-modal/auth-modal.tsx index b0b7aa127d3..f7a7872c093 100644 --- a/apps/sim/app/(landing)/components/auth-modal/auth-modal.tsx +++ b/apps/sim/app/(landing)/components/auth-modal/auth-modal.tsx @@ -10,8 +10,8 @@ import { ModalTitle, ModalTrigger, } from '@sim/emcn' +import { X } from '@sim/emcn/icons' import { createLogger } from '@sim/logger' -import { X } from 'lucide-react' import Image from 'next/image' import { useRouter } from 'next/navigation' import { GithubIcon, GoogleIcon, MicrosoftIcon } from '@/components/icons' diff --git a/apps/sim/app/(landing)/components/features/components/build-callout/components/build-chat-animation/build-chat-animation.tsx b/apps/sim/app/(landing)/components/features/components/build-callout/components/build-chat-animation/build-chat-animation.tsx index d8334668478..d17b94f807f 100644 --- a/apps/sim/app/(landing)/components/features/components/build-callout/components/build-chat-animation/build-chat-animation.tsx +++ b/apps/sim/app/(landing)/components/features/components/build-callout/components/build-chat-animation/build-chat-animation.tsx @@ -2,8 +2,7 @@ import { useEffect, useRef, useState } from 'react' import { cn } from '@sim/emcn' -import { Blimp } from '@sim/emcn/icons' -import { ArrowUp, Mic, Paperclip, Plus, Slash } from 'lucide-react' +import { ArrowUp, Blimp, Mic, Paperclip, Plus, Slash } from '@sim/emcn/icons' import { ThinkingLoader } from '@/components/ui' const PROMPT = 'Build a workflow to schedule and publish posts to my X account.' diff --git a/apps/sim/app/(landing)/components/features/components/feature-card/feature-card.tsx b/apps/sim/app/(landing)/components/features/components/feature-card/feature-card.tsx index cea43fd45f3..5b7de3ac577 100644 --- a/apps/sim/app/(landing)/components/features/components/feature-card/feature-card.tsx +++ b/apps/sim/app/(landing)/components/features/components/feature-card/feature-card.tsx @@ -1,6 +1,6 @@ import type { ReactNode } from 'react' import { ChipTag, cn } from '@sim/emcn' -import { ArrowRight } from 'lucide-react' +import { ArrowRight } from '@sim/emcn/icons' import Image from 'next/image' import Link from 'next/link' diff --git a/apps/sim/app/(landing)/components/footer/footer.tsx b/apps/sim/app/(landing)/components/footer/footer.tsx index 697e006d7df..1fedf3b0226 100644 --- a/apps/sim/app/(landing)/components/footer/footer.tsx +++ b/apps/sim/app/(landing)/components/footer/footer.tsx @@ -40,7 +40,6 @@ const PRODUCT_LINKS: FooterItem[] = [ { label: 'Tables', href: '/tables' }, { label: 'Files', href: '/files' }, { label: 'Logs', href: '/logs' }, - { label: 'Scheduled Tasks', href: '/scheduled-tasks' }, { label: 'MCP', href: 'https://docs.sim.ai/agents/mcp', external: true }, { label: 'API', href: 'https://docs.sim.ai/api-reference/getting-started', external: true }, { label: 'Self Hosting', href: 'https://docs.sim.ai/platform/self-hosting', external: true }, diff --git a/apps/sim/app/(landing)/components/hero/components/hero-chat-loop/hero-chat-loop.tsx b/apps/sim/app/(landing)/components/hero/components/hero-chat-loop/hero-chat-loop.tsx index 8c37423abef..df3fcb2c954 100644 --- a/apps/sim/app/(landing)/components/hero/components/hero-chat-loop/hero-chat-loop.tsx +++ b/apps/sim/app/(landing)/components/hero/components/hero-chat-loop/hero-chat-loop.tsx @@ -5,14 +5,14 @@ import { cn, Tooltip } from '@sim/emcn' import { ArrowRight, ArrowUp, - Copy, + Duplicate, Mic, Paperclip, Plus, Slash, ThumbsDown, ThumbsUp, -} from 'lucide-react' +} from '@sim/emcn/icons' import { ThinkingLoader } from '@/components/ui' import { HERO_TOOLTIP_OFFSET } from '@/app/(landing)/components/hero/components/hero-platform-loop/sidebar-hotspots' @@ -53,7 +53,7 @@ interface HeroChatLoopProps { * screenshot is invisible. The reply STREAMS in word by word (the way the * real Mothership streams its responses); once the text completes, the * "Suggested follow-ups" block (the real special-tags markup: numbered - * `--divider`-ruled rows with a trailing arrow) and the action row land + * `--border`-ruled rows with a trailing arrow) and the action row land * together; under `prefers-reduced-motion` it appears whole. * The content column is centered and capped (like the real full-width * MothershipChat) so it reads right both full-width (stage collapsed) and at @@ -153,7 +153,7 @@ export function HeroChatLoop({ phase, fading }: HeroChatLoopProps) { 0 && 'border-t' )} > @@ -167,7 +167,7 @@ export function HeroChatLoop({ phase, fading }: HeroChatLoopProps) {
- +
diff --git a/apps/sim/app/(landing)/components/hero/components/hero-visual/stage-home.tsx b/apps/sim/app/(landing)/components/hero/components/hero-visual/stage-home.tsx index 816c5ff547c..1b576a5df87 100644 --- a/apps/sim/app/(landing)/components/hero/components/hero-visual/stage-home.tsx +++ b/apps/sim/app/(landing)/components/hero/components/hero-visual/stage-home.tsx @@ -304,9 +304,7 @@ export function StageHome({ )} > {isEmpty ? ( - - Ask Sim to build an agent… - + Ask Sim to build an agent… ) : ( <> diff --git a/apps/sim/app/(landing)/components/hero/components/hero-visual/stage-kb.tsx b/apps/sim/app/(landing)/components/hero/components/hero-visual/stage-kb.tsx index 2526c402e9e..82c183cfb3c 100644 --- a/apps/sim/app/(landing)/components/hero/components/hero-visual/stage-kb.tsx +++ b/apps/sim/app/(landing)/components/hero/components/hero-visual/stage-kb.tsx @@ -1,6 +1,6 @@ import type { CSSProperties, RefObject } from 'react' import { cn } from '@sim/emcn' -import { Upload, X } from 'lucide-react' +import { Upload, X } from '@sim/emcn/icons' import { GRAPH_EDGES, GRAPH_NODES, diff --git a/apps/sim/app/(landing)/components/landing-preview/components/landing-preview-chat/chat-input.tsx b/apps/sim/app/(landing)/components/landing-preview/components/landing-preview-chat/chat-input.tsx index 9d1fb6a09d0..7ce8c3c545f 100644 --- a/apps/sim/app/(landing)/components/landing-preview/components/landing-preview-chat/chat-input.tsx +++ b/apps/sim/app/(landing)/components/landing-preview/components/landing-preview-chat/chat-input.tsx @@ -63,7 +63,7 @@ export function LandingPreviewChatInput({ aria-label={placeholder} rows={1} readOnly={readOnly} - className='m-0 block max-h-[200px] min-h-[24px] w-full resize-none overflow-y-auto border-0 bg-transparent px-1 py-1 font-body text-[15px] text-[var(--text-primary)] leading-[24px] tracking-[-0.015em] outline-none placeholder:font-[380] placeholder:text-[var(--text-muted)] focus-visible:ring-0' + className='m-0 block max-h-[200px] min-h-[24px] w-full resize-none overflow-y-auto border-0 bg-transparent px-1 py-1 font-body text-[15px] text-[var(--text-primary)] leading-[24px] tracking-[-0.015em] outline-none placeholder:text-[var(--text-muted)] focus-visible:ring-0' style={{ caretColor: caretHidden ? 'transparent' : 'var(--text-primary)' }} /> diff --git a/apps/sim/app/(landing)/components/landing-preview/components/landing-preview-home/landing-preview-home.tsx b/apps/sim/app/(landing)/components/landing-preview/components/landing-preview-home/landing-preview-home.tsx index 12d9bb4693f..d2eabfdd3b7 100644 --- a/apps/sim/app/(landing)/components/landing-preview/components/landing-preview-home/landing-preview-home.tsx +++ b/apps/sim/app/(landing)/components/landing-preview/components/landing-preview-home/landing-preview-home.tsx @@ -2,9 +2,8 @@ import { memo, useCallback, useEffect, useRef, useState } from 'react' import { ArrowRight, Blimp, Checkbox, ChevronDown, cn, Shuffle } from '@sim/emcn' -import { TypeBoolean, TypeNumber, TypeText } from '@sim/emcn/icons' +import { Mail, Table, Task, TypeBoolean, TypeNumber, TypeText } from '@sim/emcn/icons' import { AnimatePresence, domAnimation, LazyMotion, m } from 'framer-motion' -import { Mail, MessageSquare, Table } from 'lucide-react' import { captureClientEvent } from '@/lib/posthog/client' import { LandingPreviewChatInput } from '@/app/(landing)/components/landing-preview/components/landing-preview-chat/chat-input' import { LandingPreviewChatTitleBar } from '@/app/(landing)/components/landing-preview/components/landing-preview-chat/chat-title-bar' @@ -15,7 +14,7 @@ import { useAnimatedPlaceholder } from '@/hooks/use-animated-placeholder' /** Static, greyscale suggestion rows mirroring the workspace SuggestedActions. */ const SUGGESTED_ACTIONS = [ { id: 'crm', label: 'Create a CRM with sample data', icon: Table }, - { id: 'slack', label: 'Summarize my unread Slack messages', icon: MessageSquare }, + { id: 'slack', label: 'Summarize my unread Slack messages', icon: Task }, { id: 'gmail', label: 'Draft replies to my support emails', icon: Mail }, { id: 'tracker', label: 'Build a project tracker table', icon: Table }, ] as const diff --git a/apps/sim/app/(landing)/components/landing-preview/components/landing-preview-logs/landing-preview-logs.tsx b/apps/sim/app/(landing)/components/landing-preview/components/landing-preview-logs/landing-preview-logs.tsx index 3cb17549ee2..06d0d287411 100644 --- a/apps/sim/app/(landing)/components/landing-preview/components/landing-preview-logs/landing-preview-logs.tsx +++ b/apps/sim/app/(landing)/components/landing-preview/components/landing-preview-logs/landing-preview-logs.tsx @@ -233,7 +233,7 @@ export function LandingPreviewLogs() { - + {COL_HEADERS.map(({ key, label }) => ( ))} - + {columns.map((col) => ( - -const COLUMNS: PreviewColumn[] = [ - { id: 'task', header: 'Task' }, - { id: 'schedule', header: 'Schedule', width: 240 }, - { id: 'nextRun', header: 'Next Run' }, - { id: 'lastRun', header: 'Last Run' }, -] - -const ROWS: PreviewRow[] = [ - { - id: '1', - cells: { - task: { icon: CAL_ICON, label: 'Sync CRM contacts' }, - schedule: { label: 'Recurring, every day at 9:00 AM' }, - nextRun: { label: 'Tomorrow' }, - lastRun: { label: '2 hours ago' }, - }, - }, - { - id: '2', - cells: { - task: { icon: CAL_ICON, label: 'Generate weekly report' }, - schedule: { label: 'Recurring, every Monday at 8:00 AM' }, - nextRun: { label: 'In 5 days' }, - lastRun: { label: '6 days ago' }, - }, - }, - { - id: '3', - cells: { - task: { icon: CAL_ICON, label: 'Clean up stale files' }, - schedule: { label: 'Recurring, every Sunday at midnight' }, - nextRun: { label: 'In 2 days' }, - lastRun: { label: '6 days ago' }, - }, - }, - { - id: '4', - cells: { - task: { icon: CAL_ICON, label: 'Send performance digest' }, - schedule: { label: 'Recurring, every Friday at 5:00 PM' }, - nextRun: { label: 'In 3 days' }, - lastRun: { label: '3 days ago' }, - }, - }, - { - id: '5', - cells: { - task: { icon: CAL_ICON, label: 'Backup production data' }, - schedule: { label: 'Recurring, every 4 hours' }, - nextRun: { label: 'In 2 hours' }, - lastRun: { label: '2 hours ago' }, - }, - }, - { - id: '6', - cells: { - task: { icon: CAL_ICON, label: 'Scrape competitor pricing' }, - schedule: { label: 'Recurring, every Tuesday at 6:00 AM' }, - nextRun: { label: 'In 6 days' }, - lastRun: { label: '1 week ago' }, - }, - }, -] - -/** - * Static landing preview of the Scheduled Tasks workspace page. - */ -export function LandingPreviewScheduledTasks() { - return ( - - ) -} diff --git a/apps/sim/app/(landing)/components/landing-preview/components/landing-preview-sidebar/landing-preview-sidebar.tsx b/apps/sim/app/(landing)/components/landing-preview/components/landing-preview-sidebar/landing-preview-sidebar.tsx index 400d4c47efa..e46c51a749e 100644 --- a/apps/sim/app/(landing)/components/landing-preview/components/landing-preview-sidebar/landing-preview-sidebar.tsx +++ b/apps/sim/app/(landing)/components/landing-preview/components/landing-preview-sidebar/landing-preview-sidebar.tsx @@ -1,25 +1,9 @@ 'use client' import { ChevronDown, cn, Home, Library } from '@sim/emcn' -import { - Calendar, - Database, - File, - HelpCircle, - Search, - Settings, - Table, - Workflow, -} from '@sim/emcn/icons' +import { Database, File, HelpCircle, Search, Settings, Table, Workflow } from '@sim/emcn/icons' import type { PreviewWorkflow } from '@/app/(landing)/components/landing-preview/components/landing-preview-workflow/workflow-data' -export type SidebarView = - | 'home' - | 'workflow' - | 'tables' - | 'files' - | 'knowledge' - | 'logs' - | 'scheduled-tasks' +export type SidebarView = 'home' | 'workflow' | 'tables' | 'files' | 'knowledge' | 'logs' interface LandingPreviewSidebarProps { workflows: PreviewWorkflow[] @@ -34,7 +18,6 @@ const WORKSPACE_NAV = [ { id: 'tables', label: 'Tables', icon: Table }, { id: 'files', label: 'Files', icon: File }, { id: 'knowledge', label: 'Knowledge Base', icon: Database }, - { id: 'scheduled-tasks', label: 'Scheduled Tasks', icon: Calendar }, { id: 'logs', label: 'Logs', icon: Library }, ] as const @@ -58,9 +41,7 @@ function NavItem({ return (
- - {label} - + {label}
) } @@ -75,9 +56,7 @@ function NavItem({ )} > - - {label} - + {label} ) } @@ -133,12 +112,7 @@ export function LandingPreviewSidebar({ )} > - - Home - + Home
@@ -182,10 +156,7 @@ export function LandingPreviewSidebar({ )} > -
+
{workflow.name}
diff --git a/apps/sim/app/(landing)/components/landing-preview/components/landing-preview-stage/landing-preview-stage-header.tsx b/apps/sim/app/(landing)/components/landing-preview/components/landing-preview-stage/landing-preview-stage-header.tsx index ff5d55dbd40..d6ab860810b 100644 --- a/apps/sim/app/(landing)/components/landing-preview/components/landing-preview-stage/landing-preview-stage-header.tsx +++ b/apps/sim/app/(landing)/components/landing-preview/components/landing-preview-stage/landing-preview-stage-header.tsx @@ -1,5 +1,4 @@ -import { X } from '@sim/emcn' -import { PanelRight, Workflow } from 'lucide-react' +import { PanelRight, Workflow, X } from '@sim/emcn/icons' interface LandingPreviewStageHeaderProps { /** The staged resource's display name. */ @@ -11,7 +10,7 @@ interface LandingPreviewStageHeaderProps { * (44px, `px-4`, `gap-1.5`) that sits above the workflow canvas in the "chat * everywhere" layout. There is no tab strip and no Deploy/Run: a workflow's * panel actions are `null` in the real header, so it carries only the staged - * resource's identity - the lucide `Workflow` mark (`size-[14px]`, `--text-icon`) + * resource's identity - the emcn `Workflow` mark (`size-[14px]`, `--text-icon`) * and its name in chip geometry - plus the panel's close + collapse controls on * the right. Aligns to the chat pane's title bar so the two read as one header * row across the split. diff --git a/apps/sim/app/(landing)/components/landing-preview/components/landing-preview-workflow/preview-block-node.tsx b/apps/sim/app/(landing)/components/landing-preview/components/landing-preview-workflow/preview-block-node.tsx index 7f1d33beba9..027fa0c6157 100644 --- a/apps/sim/app/(landing)/components/landing-preview/components/landing-preview-workflow/preview-block-node.tsx +++ b/apps/sim/app/(landing)/components/landing-preview/components/landing-preview-workflow/preview-block-node.tsx @@ -2,8 +2,8 @@ import { memo } from 'react' import { Blimp } from '@sim/emcn' +import { Database } from '@sim/emcn/icons' import { domAnimation, LazyMotion, m } from 'framer-motion' -import { Database } from 'lucide-react' import { Handle, type NodeProps, Position } from 'reactflow' import { AgentIcon, diff --git a/apps/sim/app/(landing)/components/landing-preview/components/landing-preview-workflow/workflow-data.ts b/apps/sim/app/(landing)/components/landing-preview/components/landing-preview-workflow/workflow-data.ts index 0309c6dadb1..e105e5e7cba 100644 --- a/apps/sim/app/(landing)/components/landing-preview/components/landing-preview-workflow/workflow-data.ts +++ b/apps/sim/app/(landing)/components/landing-preview/components/landing-preview-workflow/workflow-data.ts @@ -401,10 +401,6 @@ export const RESOURCE_CHATS: Record = { user: 'What’s in our knowledge base?', assistant: 'Here are your knowledge bases. Your agents read from these to ground every answer.', }, - 'scheduled-tasks': { - user: 'What’s scheduled to run?', - assistant: 'These are your scheduled tasks. I can pause, edit, or add a new one for you.', - }, } /** The chat shown for a staged resource: the workflow's own exchange, else the view's. */ diff --git a/apps/sim/app/(landing)/components/landing-preview/landing-preview.tsx b/apps/sim/app/(landing)/components/landing-preview/landing-preview.tsx index 1fe20a74aca..ef0b25d078c 100644 --- a/apps/sim/app/(landing)/components/landing-preview/landing-preview.tsx +++ b/apps/sim/app/(landing)/components/landing-preview/landing-preview.tsx @@ -7,7 +7,6 @@ import { LandingPreviewFiles } from '@/app/(landing)/components/landing-preview/ import { LandingPreviewHome } from '@/app/(landing)/components/landing-preview/components/landing-preview-home/landing-preview-home' import { LandingPreviewKnowledge } from '@/app/(landing)/components/landing-preview/components/landing-preview-knowledge/landing-preview-knowledge' import { LandingPreviewLogs } from '@/app/(landing)/components/landing-preview/components/landing-preview-logs/landing-preview-logs' -import { LandingPreviewScheduledTasks } from '@/app/(landing)/components/landing-preview/components/landing-preview-scheduled-tasks/landing-preview-scheduled-tasks' import type { SidebarView } from '@/app/(landing)/components/landing-preview/components/landing-preview-sidebar/landing-preview-sidebar' import { LandingPreviewSidebar } from '@/app/(landing)/components/landing-preview/components/landing-preview-sidebar/landing-preview-sidebar' import { LandingPreviewStageHeader } from '@/app/(landing)/components/landing-preview/components/landing-preview-stage/landing-preview-stage-header' @@ -26,7 +25,6 @@ const CHAT_TITLES: Partial> = { tables: 'Workspace data', files: 'Files', knowledge: 'Knowledge base', - 'scheduled-tasks': 'Scheduled tasks', } const containerVariants: Variants = { @@ -108,7 +106,7 @@ interface LandingPreviewProps { /** * Initial staged view for the static snapshot (`autoplay={false}`). Defaults * to `'workflow'`. Lets each feature stage show the platform surface that - * matches its callout (e.g. `'logs'`, `'scheduled-tasks'`). + * matches its callout (e.g. `'logs'`, `'tables'`). */ initialView?: SidebarView /** Initial workflow for the static snapshot. Defaults to the first preview workflow. */ @@ -326,15 +324,6 @@ export function LandingPreview({ )} - {activeView === 'scheduled-tasks' && ( - - - - )} ) : activeView === 'tables' ? ( @@ -344,8 +333,6 @@ export function LandingPreview({ ) : activeView === 'logs' ? ( - ) : activeView === 'scheduled-tasks' ? ( - ) : ( )} diff --git a/apps/sim/app/(landing)/components/navbar/components/mobile-nav/mobile-nav.tsx b/apps/sim/app/(landing)/components/navbar/components/mobile-nav/mobile-nav.tsx index 4790edb28d1..c91b65f72fe 100644 --- a/apps/sim/app/(landing)/components/navbar/components/mobile-nav/mobile-nav.tsx +++ b/apps/sim/app/(landing)/components/navbar/components/mobile-nav/mobile-nav.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react' import { ChipLink, cn } from '@sim/emcn' -import { Menu, X } from 'lucide-react' +import { Menu, X } from '@sim/emcn/icons' import Link from 'next/link' import { GithubOutlineIcon } from '@/components/icons' import { NAV_MENUS } from '@/app/(landing)/components/navbar/components/nav-menu-chip' @@ -161,7 +161,6 @@ export function MobileNav({ stars }: MobileNavProps) { variant='border' href='/login' fullWidth - flush prefetch={false} className='h-[40px] justify-center [&>span]:flex-none' onClick={() => setOpen(false)} @@ -172,7 +171,6 @@ export function MobileNav({ stars }: MobileNavProps) { variant='primary' href={DEMO_HREF} fullWidth - flush className='h-[40px] justify-center [&>span]:flex-none' onClick={() => setOpen(false)} > diff --git a/apps/sim/app/(landing)/components/navbar/components/nav-menu-chip/constants.ts b/apps/sim/app/(landing)/components/navbar/components/nav-menu-chip/constants.ts index b598d046e72..31675b86ec0 100644 --- a/apps/sim/app/(landing)/components/navbar/components/nav-menu-chip/constants.ts +++ b/apps/sim/app/(landing)/components/navbar/components/nav-menu-chip/constants.ts @@ -27,11 +27,6 @@ export const PLATFORM_MENU: NavMenu = { description: 'One file store for team and agents', href: '/files', }, - { - title: 'Scheduled Tasks', - description: 'Run agents on a cadence', - href: '/scheduled-tasks', - }, { title: 'Logs', description: 'Trace every agent decision', diff --git a/apps/sim/app/(landing)/components/navbar/components/nav-menu-chip/nav-menu-chip.tsx b/apps/sim/app/(landing)/components/navbar/components/nav-menu-chip/nav-menu-chip.tsx index 3036158a3a4..f0d71450684 100644 --- a/apps/sim/app/(landing)/components/navbar/components/nav-menu-chip/nav-menu-chip.tsx +++ b/apps/sim/app/(landing)/components/navbar/components/nav-menu-chip/nav-menu-chip.tsx @@ -1,7 +1,7 @@ 'use client' import { useState } from 'react' -import { ChipChevronDown, chipContentLabelClass, chipGeometryClass, cn } from '@sim/emcn' +import { ChipChevronDown, chipContentLabelClass, chipVariants, cn } from '@sim/emcn' import { NavMenuItem } from '@/app/(landing)/components/navbar/components/nav-menu-chip/components/nav-menu-item' import type { NavMenu } from '@/app/(landing)/components/navbar/components/nav-menu-chip/types' @@ -68,8 +68,9 @@ export function NavMenuChip({ menu }: NavMenuChipProps) { aria-label={`${label} menu`} onFocus={reArm} className={cn( - chipGeometryClass, - 'mx-0.5 inline-flex cursor-pointer transition-colors hover-hover:bg-[var(--surface-active)]', + chipVariants(), + /* Held open by the wrapper's state, not the button's own hover, so the + panel and its trigger light together. */ 'group-focus-within/navmenu:bg-[var(--surface-active)] group-hover/navmenu:bg-[var(--surface-active)]' )} > diff --git a/apps/sim/app/(landing)/components/navbar/navbar.tsx b/apps/sim/app/(landing)/components/navbar/navbar.tsx index 2cf7458cac6..19a67772f3a 100644 --- a/apps/sim/app/(landing)/components/navbar/navbar.tsx +++ b/apps/sim/app/(landing)/components/navbar/navbar.tsx @@ -19,10 +19,10 @@ import { DEMO_HREF, SIGNUP_HREF } from '@/app/(landing)/constants' * {@link NavbarShell} (which frosts the bar to glass on scroll) are isolated * client leaves, so the wordmark and links stay zero-hydration, crawlable HTML. * - * Every item is a bare emcn chip. Both clusters use `gap-1`, which with - * the chips' own `mx-0.5` margins yields 8px between pills; the nav's - * `gap-3.5` (14px) plus the first chip's 2px margin puts exactly 16px - - * twice the inter-chip gap - between the wordmark and the first menu chip. + * Every item is a bare emcn chip. Chips carry no margin of their own, so both + * clusters' `gap-1` is the full 4px between pills, and the nav's own `gap-4` + * is the full 16px between the wordmark and the first menu chip - twice the + * inter-chip gap. Only that first gap is live: the trailing cluster is `ml-auto`. * Horizontal padding (`px-20`, 48px) matches every section's edge gutter, * and the bar content is capped and centered at the shared * `max-w-[1460px]` (1300px content + the two 80px gutters) so the wordmark @@ -59,7 +59,7 @@ export function Navbar({ stars, logoOnly = false }: NavbarProps) { aria-label='Primary navigation' itemScope itemType='https://schema.org/SiteNavigationElement' - className='relative mx-auto flex w-full max-w-[1460px] items-center gap-3.5 px-20 py-4 max-sm:px-5 max-lg:px-8' + className='relative mx-auto flex w-full max-w-[1460px] items-center gap-4 px-20 py-4 max-sm:px-5 max-lg:px-8' > diff --git a/apps/sim/app/(landing)/components/share-button/share-button.tsx b/apps/sim/app/(landing)/components/share-button/share-button.tsx index 8d0bb6dc095..b95f1ee81ba 100644 --- a/apps/sim/app/(landing)/components/share-button/share-button.tsx +++ b/apps/sim/app/(landing)/components/share-button/share-button.tsx @@ -9,8 +9,7 @@ import { TRIGGER_BORDER_CLASS, useCopyToClipboard, } from '@sim/emcn' -import { Duplicate } from '@sim/emcn/icons' -import { Share2 } from 'lucide-react' +import { Duplicate, Share } from '@sim/emcn/icons' import { LinkedInIcon, xIcon as XIcon } from '@/components/icons' interface ShareButtonProps { @@ -35,7 +34,7 @@ export function ShareButton({ url, title }: ShareButtonProps) { return ( - + Share diff --git a/apps/sim/app/(landing)/components/shared/editor-loop/editor-loop.tsx b/apps/sim/app/(landing)/components/shared/editor-loop/editor-loop.tsx index ecbe334e528..47cbcd1e50c 100644 --- a/apps/sim/app/(landing)/components/shared/editor-loop/editor-loop.tsx +++ b/apps/sim/app/(landing)/components/shared/editor-loop/editor-loop.tsx @@ -43,7 +43,7 @@ interface EditorLoopProps { /** * The chat-free sibling of the enterprise platform loop, shared by the - * workflows and scheduled-tasks heroes. Same architecture (the + * workflows hero. Same architecture (the * {@link HeroLoopShell}'s fixed 1280x735 design-space layer scaled to the * window, a parent-owned clock driving a presentational stage, reduced-motion * showing the finished frame) and the same live sidebar, but the workspace diff --git a/apps/sim/app/(landing)/components/solutions-page/components/solutions-card-row/components/solutions-pill-cta/solutions-pill-cta.tsx b/apps/sim/app/(landing)/components/solutions-page/components/solutions-card-row/components/solutions-pill-cta/solutions-pill-cta.tsx index 05e80ee7122..48935402b55 100644 --- a/apps/sim/app/(landing)/components/solutions-page/components/solutions-card-row/components/solutions-pill-cta/solutions-pill-cta.tsx +++ b/apps/sim/app/(landing)/components/solutions-page/components/solutions-card-row/components/solutions-pill-cta/solutions-pill-cta.tsx @@ -1,7 +1,7 @@ 'use client' import { ChipLink } from '@sim/emcn' -import { ArrowRight } from 'lucide-react' +import { ArrowRight } from '@sim/emcn/icons' import type { SolutionsPillCta as SolutionsPillCtaConfig } from '@/app/(landing)/components/solutions-page/types' /** diff --git a/apps/sim/app/(landing)/contact/components/contact-form/contact-form.tsx b/apps/sim/app/(landing)/contact/components/contact-form/contact-form.tsx index 28f8d516ee5..b21f950b3bb 100644 --- a/apps/sim/app/(landing)/contact/components/contact-form/contact-form.tsx +++ b/apps/sim/app/(landing)/contact/components/contact-form/contact-form.tsx @@ -284,7 +284,6 @@ export function ContactForm() { updateField('topic', value as ContactRequestPayload['topic'])} @@ -342,7 +341,6 @@ export function ContactForm() { } ) : ( - {placeholder} + {placeholder} )} @@ -275,7 +275,7 @@ export function EnterpriseHomeStage({
- Send message to Sim + Send message to Sim
diff --git a/apps/sim/app/(landing)/enterprise/components/enterprise-platform-loop/enterprise-sidebar.tsx b/apps/sim/app/(landing)/enterprise/components/enterprise-platform-loop/enterprise-sidebar.tsx index c77deff231c..24eddcfc205 100644 --- a/apps/sim/app/(landing)/enterprise/components/enterprise-platform-loop/enterprise-sidebar.tsx +++ b/apps/sim/app/(landing)/enterprise/components/enterprise-platform-loop/enterprise-sidebar.tsx @@ -43,7 +43,7 @@ function IconRow({ icon: Icon, label, active = false }: IconRowProps) { )} > - {label} + {label} ) } @@ -52,7 +52,7 @@ function IconRow({ icon: Icon, label, active = false }: IconRowProps) { function TextRow({ label }: { label: string }) { return (
- {label} + {label}
) } diff --git a/apps/sim/app/(landing)/files/components/files-hero-loop.tsx b/apps/sim/app/(landing)/files/components/files-hero-loop.tsx index 9e73b86cad0..47f07524568 100644 --- a/apps/sim/app/(landing)/files/components/files-hero-loop.tsx +++ b/apps/sim/app/(landing)/files/components/files-hero-loop.tsx @@ -274,7 +274,7 @@ export function FilesHeroLoop() {
{COL_HEADERS.map((header) => ( diff --git a/apps/sim/app/(landing)/knowledge/components/knowledge-hero-loop.tsx b/apps/sim/app/(landing)/knowledge/components/knowledge-hero-loop.tsx index dc262b2d24b..c54861f5951 100644 --- a/apps/sim/app/(landing)/knowledge/components/knowledge-hero-loop.tsx +++ b/apps/sim/app/(landing)/knowledge/components/knowledge-hero-loop.tsx @@ -209,7 +209,7 @@ export function KnowledgeHeroLoop() { - + {COL_HEADERS.map((header) => ( ))} - + {COL_HEADERS.map((label) => ( {priceSubtext ?? ' '}

- + {cta.label} diff --git a/apps/sim/app/(landing)/scheduled-tasks/components/scheduled-tasks-calendar-loop/index.ts b/apps/sim/app/(landing)/scheduled-tasks/components/scheduled-tasks-calendar-loop/index.ts deleted file mode 100644 index 35e43aa2af3..00000000000 --- a/apps/sim/app/(landing)/scheduled-tasks/components/scheduled-tasks-calendar-loop/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { ScheduledTasksCalendarLoop } from './scheduled-tasks-calendar-loop' diff --git a/apps/sim/app/(landing)/scheduled-tasks/components/scheduled-tasks-calendar-loop/scheduled-tasks-calendar-loop.module.css b/apps/sim/app/(landing)/scheduled-tasks/components/scheduled-tasks-calendar-loop/scheduled-tasks-calendar-loop.module.css deleted file mode 100644 index ea6ba04bb0a..00000000000 --- a/apps/sim/app/(landing)/scheduled-tasks/components/scheduled-tasks-calendar-loop/scheduled-tasks-calendar-loop.module.css +++ /dev/null @@ -1,28 +0,0 @@ -/** - * The ScheduledTasksCalendarLoop's motion: a newly scheduled task pill stamps - * in once when the parent clock mounts it on its calendar day. A one-shot - * mount animation - the loop's restart unmounts and remounts the pills, so a - * new cycle replays them naturally. Under prefers-reduced-motion the parent - * renders the finished frame and this never plays. - */ - -.pillIn { - animation: scheduled-tasks-pill-in 0.35s cubic-bezier(0.22, 1, 0.36, 1) backwards; -} - -@keyframes scheduled-tasks-pill-in { - from { - opacity: 0; - transform: translateY(-4px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -@media (prefers-reduced-motion: reduce) { - .pillIn { - animation: none; - } -} diff --git a/apps/sim/app/(landing)/scheduled-tasks/components/scheduled-tasks-calendar-loop/scheduled-tasks-calendar-loop.tsx b/apps/sim/app/(landing)/scheduled-tasks/components/scheduled-tasks-calendar-loop/scheduled-tasks-calendar-loop.tsx deleted file mode 100644 index 73824540eae..00000000000 --- a/apps/sim/app/(landing)/scheduled-tasks/components/scheduled-tasks-calendar-loop/scheduled-tasks-calendar-loop.tsx +++ /dev/null @@ -1,300 +0,0 @@ -'use client' - -import { useState } from 'react' -import { Chip, chipContentGap, chipPrimaryFillTokens, cn } from '@sim/emcn' -import { Calendar, Plus } from '@sim/emcn/icons' -import { ChevronLeft, ChevronRight } from 'lucide-react' -import { HeroLoopShell } from '@/app/(landing)/components/shared/hero-loop-shell' -import { RESET_FADE_MS } from '@/app/(landing)/hooks/use-design-scale' -import { useMotionSafeCycle } from '@/app/(landing)/hooks/use-motion-safe-cycle' -import styles from '@/app/(landing)/scheduled-tasks/components/scheduled-tasks-calendar-loop/scheduled-tasks-calendar-loop.module.css' - -/** Sidebar content for the scheduled-tasks hero - a recurring-ops workspace. */ -const SIDEBAR_CHATS = [ - 'Morning digest setup', - 'Move sync to nightly', - 'Weekly KPI report', - 'Retry failed runs', -] as const - -/** Deployed workflows in the sidebar - five fill the design height. */ -const SIDEBAR_WORKFLOWS = [ - 'Morning digest', - 'Nightly data sync', - 'Weekly KPI report', - 'Invoice sweep', - 'Churn-risk alerts', -] as const - -/** Weekday header labels, Sunday-start, matching the real month grid. */ -const WEEKDAY_LABELS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] as const - -/** Month the fixed calendar shows - June 2026 opens on a Monday and fills five Sunday-start weeks. */ -const MONTH_LABEL = 'June 2026' -/** Day-of-month carrying the today ring, a mid-month Wednesday. */ -const TODAY = 10 -/** Days in June - the grid's in-month cells span indexes 1..30. */ -const DAYS_IN_MONTH = 30 -/** Total cells in the five-week Sunday-start grid. */ -const CELL_COUNT = 35 - -interface CalendarPill { - /** Occurrence start time, preformatted like the real event chip's `h:mm a`. */ - time: string - /** Scheduled task title. */ - title: string - /** Paused schedules render dimmed - the one status the real pill signals. */ - paused?: boolean - /** - * Position of this pill in the animated stamp-in series (the Weekly KPI - * report being scheduled across the month's Mondays); unset pills are the - * settled base calendar. - */ - stampIndex?: number -} - -interface CalendarCell { - /** Day-of-month number the cell shows. */ - day: number - /** In-month days get body-colored numbers; leading/trailing days go muted. */ - inMonth: boolean - /** Today's number gets the real grid's 26px primary-filled square. */ - isToday: boolean - /** Task occurrences on this day, in start-time order. */ - pills: CalendarPill[] -} - -/** Mondays in the grid (cell indexes), in stamp order for the animated series. */ -const MONDAY_CELLS: readonly number[] = [1, 8, 15, 22, 29] - -/** - * Derives one day's task pills from the workspace's recurring schedules: - * the Morning digest on weekdays at 9:00 AM, the paused Churn-risk alerts on - * Thursdays, the Nightly data sync every night, a monthly Invoice sweep on - * the 30th, and - as the animated series - the Weekly KPI report landing on - * each Monday as the schedule is created. - */ -function pillsForCell(index: number): CalendarPill[] { - const weekday = index % 7 - const pills: CalendarPill[] = [] - const mondayOrder = MONDAY_CELLS.indexOf(index) - if (mondayOrder !== -1) { - pills.push({ time: '8:00 AM', title: 'Weekly KPI report', stampIndex: mondayOrder }) - } - if (weekday >= 1 && weekday <= 5) { - pills.push({ time: '9:00 AM', title: 'Morning digest' }) - } - if (index === DAYS_IN_MONTH) { - pills.push({ time: '3:00 PM', title: 'Invoice sweep' }) - } - if (weekday === 4) { - pills.push({ time: '4:00 PM', title: 'Churn-risk alerts', paused: true }) - } - pills.push({ time: '11:00 PM', title: 'Nightly data sync' }) - return pills -} - -/** - * The fixed June 2026 grid: May 31 leads the first week, June fills the - * middle, and July 1-4 close the fifth week - every cell's pills derived - * from the recurring schedules above. - */ -const CALENDAR_CELLS: readonly CalendarCell[] = Array.from({ length: CELL_COUNT }, (_, index) => { - const inMonth = index >= 1 && index <= DAYS_IN_MONTH - const day = index === 0 ? 31 : inMonth ? index : index - DAYS_IN_MONTH - return { - day, - inMonth, - isToday: index === TODAY, - pills: pillsForCell(index), - } -}) - -/** Total pills the animated Weekly KPI series stamps onto the month's Mondays. */ -const TOTAL_STAMPED_PILLS = MONDAY_CELLS.length - -/** The settled calendar holds this long before the first KPI pill lands. */ -const IDLE_HOLD_MS = 900 -/** Stamped pill N lands at IDLE_HOLD_MS + N * PILL_STEP_MS. */ -const PILL_STEP_MS = 620 -/** The fully scheduled month holds this long before the fade. */ -const SCHEDULED_HOLD_MS = 5200 - -interface CalendarPanePillProps { - pill: CalendarPill - /** Stamped pills replay the mount animation each cycle. */ - animate: boolean -} - -/** - * One task pill in a day cell - the real calendar event chip's exact - * chrome (primary fill, start time + title, paused schedules dimmed) rendered - * as a plain `
` since the whole frame is `aria-hidden` decoration. - */ -function CalendarPanePill({ pill, animate }: CalendarPanePillProps) { - return ( -
- {pill.time} - {pill.title} -
- ) -} - -interface ScheduledTasksCalendarPaneProps { - /** How many Weekly KPI pills the stamp-in series has landed (0..5). */ - stampedCount: number -} - -/** - * The static Scheduled Tasks page in the real workspace's exact vocabulary - - * the resource header (Calendar icon + title + primary "New scheduled task" - * chip), the calendar toolbar (Today jump, period label, prev/next chevrons, - * scope chip), the sticky weekday header, and the five-week month grid of day - * cells with the today square and stacked task pills - rendered from the - * parent clock's `stampedCount` beat. - */ -function ScheduledTasksCalendarPane({ stampedCount }: ScheduledTasksCalendarPaneProps) { - return ( -
-
- - - Scheduled Tasks - - - New scheduled task - -
- -
-
- Today - {MONTH_LABEL} -
-
- - - Month -
-
- -
- {WEEKDAY_LABELS.map((label, index) => ( -
- {label} -
- ))} -
- -
- {CALENDAR_CELLS.map((cell, index) => ( -
- - {cell.day} - -
- {cell.pills.map((pill) => - pill.stampIndex !== undefined && pill.stampIndex >= stampedCount ? null : ( - - ) - )} -
-
- ))} -
-
- ) -} - -/** - * The scheduled-tasks hero's calendar loop - the workspace's real Scheduled - * Tasks surface as the workspace pane. Same architecture as the sibling hero - * loops (the {@link HeroLoopShell}'s fixed 1280x735 design-space layer scaled - * to the window, a parent-owned clock driving a presentational pane, - * reduced-motion showing the finished frame) and the same live sidebar with - * its Scheduled tasks nav row highlighted, but the pane is the month calendar - * itself: the settled recurring schedules hold, then a new Weekly KPI report - * schedule lands pill by pill across the month's Mondays, the fully - * scheduled month holds, and the scene fades before the cycle restarts. - * - * Everything is `pointer-events-none` decorative, matching the hero's - * `aria-hidden` frame. Under `prefers-reduced-motion` the loop never starts: - * the fully scheduled calendar renders statically. - */ -export function ScheduledTasksCalendarLoop() { - const [stampedCount, setStampedCount] = useState(0) - const [fading, setFading] = useState(false) - const [cycleId, setCycleId] = useState(0) - - useMotionSafeCycle({ - scheduleCycle: () => { - setFading(false) - setStampedCount(0) - setCycleId((c) => c + 1) - const totalMs = IDLE_HOLD_MS + (TOTAL_STAMPED_PILLS - 1) * PILL_STEP_MS + SCHEDULED_HOLD_MS - return { - timers: [ - ...Array.from({ length: TOTAL_STAMPED_PILLS }, (_, i) => - setTimeout(() => setStampedCount(i + 1), IDLE_HOLD_MS + i * PILL_STEP_MS) - ), - setTimeout(() => setFading(true), totalMs - RESET_FADE_MS), - ], - totalMs, - } - }, - showFinished: () => { - setFading(false) - setStampedCount(TOTAL_STAMPED_PILLS) - }, - }) - - return ( - -
-
- -
-
-
- ) -} diff --git a/apps/sim/app/(landing)/scheduled-tasks/page.tsx b/apps/sim/app/(landing)/scheduled-tasks/page.tsx deleted file mode 100644 index 5bbf0a9d905..00000000000 --- a/apps/sim/app/(landing)/scheduled-tasks/page.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { buildLandingMetadata } from '@/lib/landing/seo' -import ScheduledTasks, { - SCHEDULED_TASKS_PAGE_DESCRIPTION, -} from '@/app/(landing)/scheduled-tasks/scheduled-tasks' - -export const revalidate = 3600 - -const TITLE = 'Schedule AI Agents: Cron & Recurring Runs | Sim' - -export const metadata = buildLandingMetadata({ - title: TITLE, - description: SCHEDULED_TASKS_PAGE_DESCRIPTION, - path: '/scheduled-tasks', - keywords: - 'AI workspace, scheduled AI agents, cron AI workflows, recurring agent runs, AI task scheduler, schedule workflow automation, open-source AI agent platform, agentic workflows', -}) - -export default function Page() { - return -} diff --git a/apps/sim/app/(landing)/scheduled-tasks/scheduled-tasks.tsx b/apps/sim/app/(landing)/scheduled-tasks/scheduled-tasks.tsx deleted file mode 100644 index 464d2313b8d..00000000000 --- a/apps/sim/app/(landing)/scheduled-tasks/scheduled-tasks.tsx +++ /dev/null @@ -1,199 +0,0 @@ -import { - PlatformHeroVisual, - SolutionsPage, - type SolutionsPageConfig, -} from '@/app/(landing)/components' -import { - AuditTrailGraphic, - DeployGraphic, - ItPlatformTeamsGraphic, - OperationsTeamsGraphic, - RunMonitoringGraphic, -} from '@/app/(landing)/enterprise/components/feature-graphics' -import { ScheduledTasksCalendarLoop } from '@/app/(landing)/scheduled-tasks/components/scheduled-tasks-calendar-loop' -import { DocumentDraftGraphic } from '@/app/(landing)/solutions/components/feature-graphics' - -/** - * Scheduled Tasks platform page - a consumer of {@link SolutionsPage} - * rendered with the enterprise page's feature-tile treatment. - * - * The page tells the product's real scheduling story: workflows carry - * schedule triggers (from every-15-minutes intervals through daily, weekly, - * and monthly cadences to custom cron, timezone-aware), and the workspace's - * Scheduled Tasks calendar shows every upcoming and finished run with - * pause/resume and full run history. The hero replays that calendar itself - - * the month grid filling with recurring task pills as a new schedule lands - * on its cadence. Every card visual slot reuses a - * parametrized enterprise feature graphic retold for recurring runs - the - * policy checklist as a schedule card, the routing fan as the delivery - * fan-out, the drafted document as the digest that arrives on time, the - * monitoring panel, deploy window, and audit ledger for the operate story. - * - * The JSON-LD emitted by {@link SolutionsPage} is structurally identical to - * the workflows page's (`WebPage` + `BreadcrumbList` + `WebApplication`). - */ -/** Shared meta + JSON-LD description for the Scheduled Tasks page (one string, zero drift). */ -export const SCHEDULED_TASKS_PAGE_DESCRIPTION = - 'Schedule AI agents in Sim to run every 15 minutes, daily, monthly, or on a custom cron, timezone-aware, with every run traced on the calendar.' - -const SCHEDULED_TASKS_CONFIG: SolutionsPageConfig = { - module: 'Scheduled Tasks', - path: '/scheduled-tasks', - seoDescription: SCHEDULED_TASKS_PAGE_DESCRIPTION, - hero: { - eyebrow: 'Scheduled Tasks', - heading: 'Schedule AI agents to run on their own.', - description: - 'Scheduled Tasks is the scheduler in Sim, the open-source AI workspace. Run any agent every 15 minutes, daily, monthly, or on a custom cron, timezone-aware, with every run traced.', - summary: - 'Scheduled Tasks is the scheduler in Sim, the open-source AI workspace where teams build, deploy, and manage AI agents. Teams schedule any workflow to run every 15 minutes, daily, weekly, monthly, or on a custom cron in your timezone, and every run lands on the calendar with full history.', - visual: ( - - - - ), - }, - rows: [ - { - id: 'schedule', - title: 'Put any AI agent on a schedule.', - subtitle: - 'Sim turns a workflow into a scheduled task with one trigger, from 15-minute intervals to monthly closes, in your timezone.', - cta: { label: 'Schedule your first agent', href: '/signup' }, - cards: [ - { - title: 'Pick the cadence', - description: - 'Choose minutes, hourly, daily, weekly, or monthly, or write a cron expression for exact control.', - visual: ( - - ), - }, - { - title: 'Fan out on every run', - description: - 'Each run pulls from your systems and delivers wherever the work lives, without anyone pressing go.', - featureTileTone: 'dark', - featureTileDescriptionTone: 'soft', - visual: ( - - ), - }, - { - title: 'Wake up to the output', - description: - 'The digest, report, or sync is done before the day starts. Sim delivers it where your team looks first.', - visual: ( - - ), - }, - ], - }, - { - id: 'operate', - title: 'Stay on top of every run.', - subtitle: - 'Scheduled runs land on the workspace calendar, and Sim keeps the full history so nothing happens silently.', - cta: { label: 'See runs in action', href: '/signup' }, - cards: [ - { - title: 'Trace each scheduled run', - description: - 'Every run is logged block by block, with the trigger, duration, and output a click away.', - visual: ( - - ), - }, - { - title: 'Keep it running', - description: - 'Schedules stay live in production. Pause, resume, or retune the timing without redeploying.', - featureTileTone: 'dark', - featureTileDescriptionTone: 'soft', - visual: ( - - ), - }, - { - title: 'Keep the history', - description: - 'Every run and every schedule change lands in one record, so teams see what ran and when.', - visual: ( - - ), - }, - ], - }, - ], -} - -export default function ScheduledTasks() { - return -} diff --git a/apps/sim/app/_styles/globals.css b/apps/sim/app/_styles/globals.css index 1ba253d00b5..e6678aed470 100644 --- a/apps/sim/app/_styles/globals.css +++ b/apps/sim/app/_styles/globals.css @@ -12,7 +12,7 @@ :root { --sidebar-width: 0px; /* 0 outside workspace; blocking script always sets actual value on workspace pages */ --sidebar-collapsed-width: 51px; /* icon rail on web; desktop overrides to 0 before first paint */ - --sidebar-expanded-width: 248px; /* SIDEBAR_WIDTH.DEFAULT; the width to restore to, held even while collapsed */ + --sidebar-expanded-width: 238px; /* SIDEBAR_WIDTH.DEFAULT; the width to restore to, held even while collapsed */ --desktop-title-bar-height: 0px; /* macOS traffic-light lane; desktop overrides before first paint */ --workspace-content-title-bar-inset: 0px; /* lane the content pane must leave clear; only non-zero when the pane, not the sidebar, sits under it */ --desktop-title-bar-inset-x: 0px; /* clearance past the traffic lights; desktop overrides */ @@ -23,6 +23,12 @@ --editor-connections-height: 172px; /* EDITOR_CONNECTIONS_HEIGHT.DEFAULT */ --terminal-height: 206px; /* TERMINAL_HEIGHT.DEFAULT */ --output-panel-width: 560px; /* OUTPUT_PANEL_WIDTH.DEFAULT */ + /** + * Neutral border and divider thickness. Standard-density displays cannot draw + * a sub-pixel line crisply, so they keep a full pixel; the hidpi override below + * thins it. Consumed by Tailwind's `border*`, `divide-*`, and `h-px`/`w-px`. + */ + --border-width: 1px; --auth-primary-btn-bg: #ffffff; --auth-primary-btn-border: #ffffff; --auth-primary-btn-text: #000000; @@ -48,6 +54,26 @@ --shadow-kbd: 0 4px 0 0 rgba(48, 48, 48, 1); --shadow-kbd-sm: 0 2px 0 0 rgba(48, 48, 48, 1); --shadow-card: 0 1px 3px rgba(0, 0, 0, 0.04); + /** + * The scale's around-shape entry: one layer like every other, borrowing + * `--shadow-subtle`'s alpha and `--shadow-medium`'s blur, with the y-offset + * dropped so the spread lands evenly on all four sides instead of pooling + * underneath. For a raised surface that floats in open space (the chat input); + * the directional entries above still own menus, cards, and overlays. + */ + --shadow-ambient: 0 0 12px 0 rgba(0, 0, 0, 0.08); +} + +/** + * One device pixel on high-density displays. Browsers floor a border to whole + * device pixels, so every value in (0, 1px) collapses to this same hairline and + * the next drawable step is a full 1px — i.e. double. Line weight between those + * two is therefore tuned on `--border`, never here. + */ +@media (min-resolution: 2dppx) { + :root { + --border-width: 0.5px; + } } /** @@ -312,9 +338,7 @@ html.sidebar-booting .sidebar-shell-inner { --surface-2: #ffffff; /* blocks, cards, modals - pure white */ --surface-3: #f7f7f7; /* popovers, headers */ --surface-4: #f5f5f5; /* buttons base */ - --border: #dedede; /* primary border */ --surface-5: #f3f3f3; /* inputs, form elements */ - --border-1: #e0e0e0; /* stronger border */ --surface-6: #e5e5e5; /* popovers, elevated surfaces */ --surface-7: #d9d9d9; --surface-hover: #f2f2f2; /* hover state */ @@ -326,20 +350,24 @@ html.sidebar-booting .sidebar-shell-inner { --text-primary: #1a1a1a; --text-secondary: #525252; --text-tertiary: #5c5c5c; - --text-muted: #707070; + --text-muted: #7a7a7a; --text-subtle: #8c8c8c; - --text-body: #3b3b3b; - --text-icon: #525252; + --text-body: #434343; + --text-icon: #5a5a5a; + --text-icon-muted: #5c5c5c; --text-inverse: #ffffff; --text-muted-inverse: #a0a0a0; --text-error: #ef4444; /* Borders / dividers */ - --divider: #ededed; - --border-muted: #e4e4e4; + --border: #d8d8d8; --border-success: #e0e0e0; + /** Legacy neutral-border aliases. New work should use --border. */ + --border-1: var(--border); + --border-muted: var(--border); + /* Brand & state */ --brand-agent: #6f3dfa; --brand-secondary: #33b4ff; @@ -360,11 +388,6 @@ html.sidebar-booting .sidebar-shell-inner { /* Utility */ --white: #ffffff; - /* Font weights - lighter for light mode */ - --font-weight-base: 420; - --font-weight-medium: 440; - --font-weight-semibold: 500; - /* Landing / marketing - text (static dark-section colors) */ --landing-text: #ececec; --landing-text-muted: #999999; @@ -470,9 +493,7 @@ html.sidebar-booting .sidebar-shell-inner { --surface-2: #232323; --surface-3: #242424; --surface-4: #292929; - --border: #333333; --surface-5: #363636; - --border-1: #3d3d3d; --surface-6: #454545; --surface-7: #505050; --surface-hover: #262626; /* hover state */ @@ -484,20 +505,24 @@ html.sidebar-booting .sidebar-shell-inner { --text-primary: #e6e6e6; --text-secondary: #cccccc; --text-tertiary: #b3b3b3; - --text-muted: #787878; + --text-muted: #6e6e6e; --text-subtle: #7d7d7d; - --text-body: #cdcdcd; - --text-icon: #a0a0a0; + --text-body: #c1c1c1; + --text-icon: #969696; + --text-icon-muted: #949494; --text-inverse: #1b1b1b; --text-muted-inverse: #b3b3b3; --text-error: #ef4444; - /* --border-strong: #303030; */ - --divider: #393939; - --border-muted: #424242; + /* Borders / dividers */ + --border: #444444; --border-success: #575757; + /** Legacy neutral-border aliases. New work should use --border. */ + --border-1: var(--border); + --border-muted: var(--border); + /* Brand & state */ --brand-agent: #701ffc; --brand-secondary: #33b4ff; @@ -518,11 +543,6 @@ html.sidebar-booting .sidebar-shell-inner { /* Utility */ --white: #ffffff; - /* Font weights - standard weights for dark mode */ - --font-weight-base: 420; - --font-weight-medium: 480; - --font-weight-semibold: 550; - /* Code editor (dark mode overrides) */ --code-bg: #1f1f1f; --code-foreground: #eeeeee; @@ -598,6 +618,8 @@ html.sidebar-booting .sidebar-shell-inner { /* Shadow scale - dark */ --shadow-overlay: 0 10px 30px rgba(0, 0, 0, 0.3); + /* Same 2.7x light-to-dark alpha step --shadow-overlay takes (0.11 -> 0.3). */ + --shadow-ambient: 0 0 12px 0 rgba(0, 0, 0, 0.22); } } diff --git a/apps/sim/app/api/emails/preview/route.ts b/apps/sim/app/api/emails/preview/route.ts index c4791882bb4..77ec9673d1e 100644 --- a/apps/sim/app/api/emails/preview/route.ts +++ b/apps/sim/app/api/emails/preview/route.ts @@ -118,7 +118,6 @@ const emailTemplates = { 'schedule-disabled': () => renderScheduleDisabledEmail({ recipientName: 'John', - kind: 'workflow', resourceName: 'Daily digest', reason: 'consecutive_failures', failedCount: 100, @@ -127,10 +126,9 @@ const emailTemplates = { 'schedule-disabled-auth': () => renderScheduleDisabledEmail({ recipientName: 'John', - kind: 'job', resourceName: 'Weekly report', reason: 'authentication_error', - manageLink: 'https://sim.ai/workspace/ws_123/scheduled-tasks', + manageLink: 'https://sim.ai/workspace/ws_123/w/wf_456', }), } as const diff --git a/apps/sim/app/api/mothership/execute/route.test.ts b/apps/sim/app/api/mothership/execute/route.test.ts index b052d2cfa79..fbd95db75cf 100644 --- a/apps/sim/app/api/mothership/execute/route.test.ts +++ b/apps/sim/app/api/mothership/execute/route.test.ts @@ -89,11 +89,7 @@ vi.mock('@/lib/workspaces/permissions/utils', () => ({ })) import type { CopilotLifecycleOptions } from '@/lib/copilot/request/lifecycle/run' -import { - buildExecuteResponsePayload, - CALLER_VISIBLE_SERVER_TOOLS, - POST, -} from '@/app/api/mothership/execute/route' +import { buildExecuteResponsePayload, POST } from '@/app/api/mothership/execute/route' type Payload = Parameters[0] @@ -102,25 +98,6 @@ function resultWithToolCalls(names: string[]): Payload { } describe('buildExecuteResponsePayload', () => { - // The scheduled-task runner branches on whether the agent called - // complete_scheduled_task (background/schedule-execution.ts reads - // responseBody.toolCalls). This filter used to admit only integration tools - // and mcp-*, so that check was permanently false: a job completed itself, the - // signal was dropped here, and the runner's post-run bookkeeping wrote - // status='active' with a fresh nextRunAt straight back over the completion — - // the job then reran forever, each time telling the model it was done. - it('keeps complete_scheduled_task so the schedule runner can see it', () => { - const payload = buildExecuteResponsePayload( - resultWithToolCalls(['complete_scheduled_task']), - 'chat-1', - [] - ) - - expect(payload.toolCalls.map((tc: { name: string }) => tc.name)).toContain( - 'complete_scheduled_task' - ) - }) - it('still admits integration and mcp tool calls, and still drops other server tools', () => { const payload = buildExecuteResponsePayload( resultWithToolCalls(['gmail_send', 'mcp-notion-create', 'read', 'edit_workflow']), @@ -131,13 +108,6 @@ describe('buildExecuteResponsePayload', () => { const names = payload.toolCalls.map((tc: { name: string }) => tc.name) expect(names).toEqual(['gmail_send', 'mcp-notion-create']) }) - - // Guards the cross-file contract: the literal the runner greps for must be in - // the allowlist above. These live in different files and nothing else ties - // them together. - it('exposes the exact tool name the schedule runner looks for', () => { - expect(CALLER_VISIBLE_SERVER_TOOLS.has('complete_scheduled_task')).toBe(true) - }) }) describe('mothership private trace provenance transport', () => { diff --git a/apps/sim/app/api/mothership/execute/route.ts b/apps/sim/app/api/mothership/execute/route.ts index c3e9d23f77a..0133d6562f8 100644 --- a/apps/sim/app/api/mothership/execute/route.ts +++ b/apps/sim/app/api/mothership/execute/route.ts @@ -89,15 +89,6 @@ function encodeNdjson(value: unknown): Uint8Array { return ndjsonEncoder.encode(`${JSON.stringify(value)}\n`) } -/** - * Server-owned tools whose invocation the CALLER must see, even though they are - * not client/integration tools. The scheduled-task runner branches on whether - * the agent called complete_scheduled_task; filtering it out of the response - * made that check permanently false, so a completed job was rescheduled by the - * runner's own post-run bookkeeping. - */ -export const CALLER_VISIBLE_SERVER_TOOLS = new Set(['complete_scheduled_task']) - export function buildExecuteResponsePayload( result: Awaited>, effectiveChatId: string, @@ -105,10 +96,7 @@ export function buildExecuteResponsePayload( ) { const clientToolNames = new Set(integrationTools.map((t) => t.name)) const clientToolCalls = (result.toolCalls || []).filter( - (tc: { name: string }) => - clientToolNames.has(tc.name) || - tc.name.startsWith('mcp-') || - CALLER_VISIBLE_SERVER_TOOLS.has(tc.name) + (tc: { name: string }) => clientToolNames.has(tc.name) || tc.name.startsWith('mcp-') ) return { diff --git a/apps/sim/app/api/schedules/[id]/route.ts b/apps/sim/app/api/schedules/[id]/route.ts index 6c3f6248c4c..b810d183aa8 100644 --- a/apps/sim/app/api/schedules/[id]/route.ts +++ b/apps/sim/app/api/schedules/[id]/route.ts @@ -14,14 +14,7 @@ import { parseRequest } from '@/lib/api/server' import { getSession } from '@/lib/auth' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' -import { captureServerEvent } from '@/lib/posthog/server' -import { - performDeleteJob, - performExcludeOccurrence, - performUpdateJob, -} from '@/lib/workflows/schedules/orchestration' import { validateCronExpression } from '@/lib/workflows/schedules/utils' -import { verifyWorkspaceMembership } from '@/app/api/workflows/utils' const logger = createLogger('ScheduleAPI') @@ -46,18 +39,6 @@ async function fetchAndAuthorize( return NextResponse.json({ error: 'Schedule not found' }, { status: 404 }) } - if (schedule.sourceType === 'job') { - if (!schedule.sourceWorkspaceId) { - return NextResponse.json({ error: 'Job has no workspace' }, { status: 400 }) - } - const permission = await verifyWorkspaceMembership(userId, schedule.sourceWorkspaceId) - const canWrite = permission === 'admin' || permission === 'write' - if (!permission || (action === 'write' && !canWrite)) { - return NextResponse.json({ error: 'Not authorized' }, { status: 403 }) - } - return { schedule, workspaceId: schedule.sourceWorkspaceId } - } - if (!schedule.workflowId) { logger.warn(`[${requestId}] Schedule has no workflow: ${scheduleId}`) return NextResponse.json({ error: 'Schedule has no associated workflow' }, { status: 400 }) @@ -178,95 +159,6 @@ export const PUT = withRouteHandler( return NextResponse.json({ message: 'Schedule disabled successfully' }) } - if (action === 'update') { - if (schedule.sourceType !== 'job') { - return NextResponse.json( - { error: 'Only standalone job schedules can be edited' }, - { status: 400 } - ) - } - - if (!workspaceId) { - return NextResponse.json({ error: 'Job has no workspace' }, { status: 400 }) - } - - const updateResult = await performUpdateJob({ - jobId: scheduleId, - workspaceId, - userId: session.user.id, - actorName: session.user.name, - actorEmail: session.user.email, - title: validatedBody.title, - prompt: validatedBody.prompt, - timezone: validatedBody.timezone, - lifecycle: validatedBody.lifecycle, - maxRuns: validatedBody.maxRuns, - cronExpression: validatedBody.cronExpression, - time: validatedBody.time, - endsAt: validatedBody.endsAt, - contexts: validatedBody.contexts, - secretScope: validatedBody.secretScope, - mountedSecrets: validatedBody.mountedSecrets, - request, - }) - if (!updateResult.success) { - return NextResponse.json( - { error: updateResult.error || 'Failed to update schedule' }, - { - status: - updateResult.errorCode === 'forbidden' - ? 403 - : updateResult.errorCode === 'validation' - ? 400 - : 500, - } - ) - } - - logger.info(`[${requestId}] Updated job schedule: ${scheduleId}`) - - return NextResponse.json({ message: 'Schedule updated successfully' }) - } - - if (action === 'exclude_occurrence') { - if (schedule.sourceType !== 'job') { - return NextResponse.json( - { error: 'Only standalone job schedules have occurrences' }, - { status: 400 } - ) - } - if (!workspaceId) { - return NextResponse.json({ error: 'Job has no workspace' }, { status: 400 }) - } - - const excludeResult = await performExcludeOccurrence({ - jobId: scheduleId, - workspaceId, - userId: session.user.id, - actorName: session.user.name, - actorEmail: session.user.email, - occurrence: validatedBody.occurrence, - request, - }) - if (!excludeResult.success) { - return NextResponse.json( - { error: excludeResult.error || 'Failed to delete occurrence' }, - { - status: - excludeResult.errorCode === 'not_found' - ? 404 - : excludeResult.errorCode === 'validation' - ? 400 - : 500, - } - ) - } - - logger.info(`[${requestId}] Excluded occurrence on job schedule: ${scheduleId}`) - - return NextResponse.json({ message: 'Occurrence deleted successfully' }) - } - // reactivate if (schedule.status === 'active') { return NextResponse.json({ message: 'Schedule is already active' }) @@ -341,27 +233,6 @@ export const DELETE = withRouteHandler( if (result instanceof NextResponse) return result const { schedule, workspaceId } = result - if (schedule.sourceType === 'job') { - if (!workspaceId) { - return NextResponse.json({ error: 'Job has no workspace' }, { status: 400 }) - } - const deleteResult = await performDeleteJob({ - jobId: scheduleId, - workspaceId, - userId: session.user.id, - actorName: session.user.name, - actorEmail: session.user.email, - request, - }) - if (!deleteResult.success) { - return NextResponse.json( - { error: deleteResult.error || 'Failed to delete schedule' }, - { status: deleteResult.errorCode === 'not_found' ? 404 : 500 } - ) - } - return NextResponse.json({ message: 'Schedule deleted successfully' }) - } - await db.delete(workflowSchedule).where(eq(workflowSchedule.id, scheduleId)) logger.info(`[${requestId}] Deleted schedule: ${scheduleId}`) @@ -375,7 +246,7 @@ export const DELETE = withRouteHandler( resourceType: AuditResourceType.SCHEDULE, resourceId: scheduleId, resourceName: schedule.jobTitle ?? undefined, - description: `Deleted ${schedule.sourceType === 'job' ? 'job' : 'schedule'} "${schedule.jobTitle ?? scheduleId}"`, + description: `Deleted schedule "${schedule.jobTitle ?? scheduleId}"`, metadata: { sourceType: schedule.sourceType, cronExpression: schedule.cronExpression, @@ -384,13 +255,6 @@ export const DELETE = withRouteHandler( request, }) - captureServerEvent( - session.user.id, - 'scheduled_task_deleted', - { workspace_id: workspaceId ?? '' }, - workspaceId ? { groups: { workspace: workspaceId } } : undefined - ) - return NextResponse.json({ message: 'Schedule deleted successfully' }) } catch (error) { logger.error(`[${requestId}] Error deleting schedule`, error) diff --git a/apps/sim/app/api/schedules/execute/route.test.ts b/apps/sim/app/api/schedules/execute/route.test.ts index 45053f9d53d..2f67617c6ef 100644 --- a/apps/sim/app/api/schedules/execute/route.test.ts +++ b/apps/sim/app/api/schedules/execute/route.test.ts @@ -21,7 +21,6 @@ const orderByLimitMock = vi.fn() const { mockVerifyCronAuth, mockExecuteScheduleJob, - mockExecuteJobInline, mockReleaseScheduleLock, mockEnqueue, mockGetJob, @@ -37,7 +36,6 @@ const { } = vi.hoisted(() => ({ mockVerifyCronAuth: vi.fn().mockReturnValue(null), mockExecuteScheduleJob: vi.fn().mockResolvedValue(undefined), - mockExecuteJobInline: vi.fn().mockResolvedValue(undefined), mockReleaseScheduleLock: vi.fn().mockResolvedValue(undefined), mockEnqueue: vi.fn().mockResolvedValue('job-id-1'), mockGetJob: vi.fn().mockResolvedValue(null), @@ -63,7 +61,6 @@ vi.mock('@/lib/billing/core/billing-attribution', () => ({ vi.mock('@/background/schedule-execution', () => ({ executeScheduleJob: mockExecuteScheduleJob, - executeJobInline: mockExecuteJobInline, releaseScheduleLock: mockReleaseScheduleLock, applyScheduleFailureUpdate: mockApplyScheduleFailureUpdate, })) @@ -205,18 +202,6 @@ function createBillingAttribution(workspaceId: string, actorUserId = `owner-${wo } } -const SINGLE_JOB = [ - { - id: 'job-1', - cronExpression: '0 * * * *', - failedCount: 0, - infraRetryCount: 0, - timezone: 'UTC', - lastQueuedAt: undefined, - sourceType: 'job', - }, -] - function conditionContains( condition: unknown, predicate: (entry: Record) => boolean @@ -313,8 +298,6 @@ describe('Scheduled Workflow Execution API Route', () => { mockCancelJob.mockResolvedValue(undefined) mockExecuteScheduleJob.mockReset() mockExecuteScheduleJob.mockResolvedValue(undefined) - mockExecuteJobInline.mockReset() - mockExecuteJobInline.mockResolvedValue(undefined) mockReleaseScheduleLock.mockReset() mockReleaseScheduleLock.mockResolvedValue(undefined) mockAssertBillingAttributionSnapshot.mockReset() @@ -375,21 +358,6 @@ describe('Scheduled Workflow Execution API Route', () => { expect(result.processedCount).toBe(2) }) - it('should execute mothership jobs inline', async () => { - dbChainMockFns.limit.mockResolvedValueOnce([]).mockResolvedValueOnce([{ id: 'job-1' }]) - dbChainMockFns.returning.mockReturnValueOnce(SINGLE_JOB) - - await runScheduleTick('test-request-id') - expect(mockExecuteJobInline).toHaveBeenCalledWith( - expect.objectContaining({ - scheduleId: 'job-1', - cronExpression: '0 * * * *', - failedCount: 0, - now: expect.any(String), - }) - ) - }) - it('should enqueue schedule with one atomic system actor and payer snapshot', async () => { dbChainMockFns.limit .mockResolvedValueOnce(SINGLE_CLAIMED_SCHEDULE_ROWS) @@ -1024,7 +992,11 @@ describe('Scheduled Workflow Execution API Route', () => { const result = await runScheduleTick('test-request-id') expect(result.processedCount).toBe(100) - expect(dbChainMockFns.limit).toHaveBeenCalledWith(100) + // The workflow claim is capped by SCHEDULE_WORKFLOW_ENQUEUE_LIMIT + // (SCHEDULE_EXECUTION_CONCURRENCY_LIMIT 30 x SCHEDULE_ENQUEUE_BUDGET_MULTIPLIER 2), + // not by WORKFLOW_CHUNK_SIZE. This used to read 100, which only ever matched + // the separate job claim's chunk size rather than the budget under test. + expect(dbChainMockFns.limit).toHaveBeenCalledWith(60) expect(mockEnqueue).toHaveBeenCalledTimes(100) }) diff --git a/apps/sim/app/api/schedules/execute/route.ts b/apps/sim/app/api/schedules/execute/route.ts index de14972d700..168f2a13f1a 100644 --- a/apps/sim/app/api/schedules/execute/route.ts +++ b/apps/sim/app/api/schedules/execute/route.ts @@ -17,7 +17,6 @@ import { } from '@/lib/billing/core/billing-attribution' import { getJobQueue, shouldExecuteInline } from '@/lib/core/async-jobs' import { JOB_STATUS, type Job } from '@/lib/core/async-jobs/types' -import { env } from '@/lib/core/config/env' import { isRetryableInfrastructureError } from '@/lib/core/errors/retryable-infrastructure' import { getMaxExecutionTimeout } from '@/lib/core/execution-limits' import { runDetached } from '@/lib/core/utils/background' @@ -34,7 +33,6 @@ import { import { calculateScheduleInfraRetryDelayMs } from '@/lib/workflows/schedules/retry' import { applyScheduleFailureUpdate, - executeJobInline, executeScheduleJob, releaseScheduleLock, type ScheduleExecutionPayload, @@ -51,7 +49,6 @@ const WORKFLOW_CHUNK_SIZE = 100 * into hundreds of inline sends; the remainder is logged. */ const STALE_SCHEDULE_RECOVERY_NOTIFY_LIMIT = 25 -const JOB_CHUNK_SIZE = 100 const MAX_TICK_DURATION_MS = 3 * 60 * 1000 const STALE_SCHEDULE_CLAIM_MS = getMaxExecutionTimeout() const STALE_SCHEDULE_RECOVERY_BATCH_SIZE = 100 @@ -81,9 +78,6 @@ const workflowScheduleFilter = (queuedAt: Date) => activeWorkflowDeploymentFilter() ) -const jobScheduleFilter = (queuedAt: Date) => - and(dueFilter(queuedAt), sql`${workflowSchedule.sourceType} = 'job'`) - async function runWithDatabaseScheduleStartTurn( operation: () => Promise ): Promise { @@ -184,44 +178,7 @@ async function claimWorkflowSchedules(queuedAt: Date, limit: number) { }) } -async function claimJobSchedules(queuedAt: Date, limit: number) { - if (limit <= 0) return [] - - return db.transaction(async (tx) => { - const rows = await tx - .select({ id: workflowSchedule.id }) - .from(workflowSchedule) - .where(jobScheduleFilter(queuedAt)) - .for('update', { skipLocked: true }) - .limit(limit) - - if (rows.length === 0) return [] - - return tx - .update(workflowSchedule) - .set({ lastQueuedAt: queuedAt, updatedAt: queuedAt }) - .where( - and( - jobScheduleFilter(queuedAt), - inArray( - workflowSchedule.id, - rows.map((row) => row.id) - ) - ) - ) - .returning({ - id: workflowSchedule.id, - cronExpression: workflowSchedule.cronExpression, - timezone: workflowSchedule.timezone, - failedCount: workflowSchedule.failedCount, - lastQueuedAt: workflowSchedule.lastQueuedAt, - sourceType: workflowSchedule.sourceType, - }) - }) -} - type ClaimedSchedule = Awaited>[number] -type ClaimedJob = Awaited>[number] type JobQueue = Awaited> type DatabaseScheduleExecutionTarget = Pick< ClaimedSchedule, @@ -1198,40 +1155,13 @@ async function processScheduleItem( } } -async function processJobItem(job: ClaimedJob, queuedAt: Date, requestId: string) { - const queueTime = job.lastQueuedAt ?? queuedAt - const payload = { - scheduleId: job.id, - cronExpression: job.cronExpression || undefined, - failedCount: job.failedCount || 0, - now: queueTime.toISOString(), - } - - try { - await executeJobInline(payload) - } catch (error) { - logger.error(`[${requestId}] Job execution failed for ${job.id}`, { - error: toError(error).message, - }) - await releaseScheduleLock( - job.id, - requestId, - queuedAt, - `Failed to release lock for job ${job.id}`, - undefined, - { expectedLastQueuedAt: queueTime } - ) - } -} - interface ScheduleTickResult { processedCount: number totalSchedules: number - totalJobs: number } /** - * Drains due schedules and jobs, claiming and enqueuing work until the tick + * Drains due schedules, claiming and enqueuing work until the tick * budget is exhausted or no more items are due. Runs detached from the HTTP * response so the cron caller does not wait; cross-replica safety is provided by * the `FOR UPDATE SKIP LOCKED` claim layer, not this function. @@ -1242,25 +1172,11 @@ export async function runScheduleTick(requestId: string): Promise processJobItem(job, queuedAt, requestId)), - ]) + await Promise.allSettled(schedulePromises) } - const totalCount = totalSchedules + totalJobs + const totalCount = totalSchedules const durationMs = Date.now() - tickStart logger.info( - `[${requestId}] Processed ${totalCount} items across ${iterations} iteration(s) in ${durationMs}ms (${totalSchedules} schedules, ${totalJobs} jobs)`, + `[${requestId}] Processed ${totalCount} items across ${iterations} iteration(s) in ${durationMs}ms`, { scheduleConcurrencyLimit: SCHEDULE_EXECUTION_CONCURRENCY_LIMIT, scheduleEnqueueBudget: SCHEDULE_WORKFLOW_ENQUEUE_LIMIT, @@ -1336,7 +1246,7 @@ export async function runScheduleTick(requestId: string): Promise { diff --git a/apps/sim/app/api/schedules/route.ts b/apps/sim/app/api/schedules/route.ts index f2741fc4908..f3dc25aa919 100644 --- a/apps/sim/app/api/schedules/route.ts +++ b/apps/sim/app/api/schedules/route.ts @@ -4,12 +4,11 @@ import { createLogger } from '@sim/logger' import { authorizeWorkflowByWorkspacePermission } from '@sim/platform-authz/workflow' import { and, eq, isNull, or } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' -import { createScheduleContract, scheduleQuerySchema } from '@/lib/api/contracts/schedules' -import { parseRequest, validationErrorResponse } from '@/lib/api/server' +import { scheduleQuerySchema } from '@/lib/api/contracts/schedules' +import { validationErrorResponse } from '@/lib/api/server' import { getSession } from '@/lib/auth' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' -import { performCreateJob } from '@/lib/workflows/schedules/orchestration' import { verifyWorkspaceMembership } from '@/app/api/workflows/utils' const logger = createLogger('ScheduledAPI') @@ -128,159 +127,41 @@ async function handleWorkspaceSchedules(requestId: string, userId: string, works logger.info(`[${requestId}] Getting all schedules for workspace ${workspaceId}`) - const [workflowRows, jobRows] = await Promise.all([ - db - .select({ - schedule: workflowSchedule, - workflowName: workflow.name, - }) - .from(workflowSchedule) - .innerJoin(workflow, eq(workflow.id, workflowSchedule.workflowId)) - .leftJoin( - workflowDeploymentVersion, - and( - eq(workflowDeploymentVersion.workflowId, workflowSchedule.workflowId), - eq(workflowDeploymentVersion.isActive, true) - ) + const workflowRows = await db + .select({ + schedule: workflowSchedule, + workflowName: workflow.name, + }) + .from(workflowSchedule) + .innerJoin(workflow, eq(workflow.id, workflowSchedule.workflowId)) + .leftJoin( + workflowDeploymentVersion, + and( + eq(workflowDeploymentVersion.workflowId, workflowSchedule.workflowId), + eq(workflowDeploymentVersion.isActive, true) ) - .where( - and( - eq(workflow.workspaceId, workspaceId), - isNull(workflow.archivedAt), - eq(workflowSchedule.triggerType, 'schedule'), - isNull(workflowSchedule.archivedAt), - or(eq(workflowSchedule.sourceType, 'workflow'), isNull(workflowSchedule.sourceType)), - or( - eq(workflowSchedule.deploymentVersionId, workflowDeploymentVersion.id), - and(isNull(workflowDeploymentVersion.id), isNull(workflowSchedule.deploymentVersionId)) - ) - ) - ), - db - .select({ schedule: workflowSchedule }) - .from(workflowSchedule) - .where( - and( - eq(workflowSchedule.sourceWorkspaceId, workspaceId), - eq(workflowSchedule.sourceType, 'job'), - isNull(workflowSchedule.archivedAt) + ) + .where( + and( + eq(workflow.workspaceId, workspaceId), + isNull(workflow.archivedAt), + eq(workflowSchedule.triggerType, 'schedule'), + isNull(workflowSchedule.archivedAt), + or(eq(workflowSchedule.sourceType, 'workflow'), isNull(workflowSchedule.sourceType)), + or( + eq(workflowSchedule.deploymentVersionId, workflowDeploymentVersion.id), + and(isNull(workflowDeploymentVersion.id), isNull(workflowSchedule.deploymentVersionId)) ) - ), - ]) + ) + ) const headers = new Headers() headers.set('Cache-Control', 'no-store, max-age=0') - const schedules = [ - ...workflowRows.map((r) => ({ - ...r.schedule, - workflowName: r.workflowName, - })), - ...jobRows.map((r) => ({ - ...r.schedule, - workflowName: null, - })), - ] + const schedules = workflowRows.map((r) => ({ + ...r.schedule, + workflowName: r.workflowName, + })) return NextResponse.json({ schedules }, { headers }) } - -/** - * Create a standalone scheduled job. - * - * Body: { workspaceId, title, prompt, cronExpression, timezone, lifecycle?, maxRuns?, startDate? } - */ -export const POST = withRouteHandler(async (req: NextRequest) => { - const requestId = generateRequestId() - - try { - const session = await getSession() - if (!session?.user?.id) { - logger.warn(`[${requestId}] Unauthorized schedule creation attempt`) - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) - } - - const parsed = await parseRequest( - createScheduleContract, - req, - {}, - { - validationErrorResponse: (error) => - NextResponse.json( - { error: 'Invalid request body', details: error.issues }, - { status: 400 } - ), - } - ) - if (!parsed.success) return parsed.response - - const { - workspaceId, - title, - prompt, - cronExpression, - time, - timezone, - lifecycle, - maxRuns, - endsAt, - startDate, - contexts, - secretScope, - mountedSecrets, - } = parsed.data.body - - const permission = await verifyWorkspaceMembership(session.user.id, workspaceId) - if (permission !== 'admin' && permission !== 'write') { - return NextResponse.json({ error: 'Not authorized' }, { status: 403 }) - } - - const result = await performCreateJob({ - workspaceId, - userId: session.user.id, - actorName: session.user.name, - actorEmail: session.user.email, - title, - prompt, - cronExpression, - time, - timezone, - lifecycle, - maxRuns, - endsAt, - startDate, - contexts, - secretScope, - mountedSecrets, - request: req, - }) - if (!result.success || !result.schedule) { - return NextResponse.json( - { error: result.error || 'Failed to create schedule' }, - { status: result.errorCode === 'validation' ? 400 : 500 } - ) - } - - logger.info(`[${requestId}] Created job schedule ${result.schedule.id}`, { - title, - cronExpression, - timezone, - lifecycle, - }) - - return NextResponse.json( - { - schedule: { - id: result.schedule.id, - status: result.schedule.status, - cronExpression: result.schedule.cronExpression, - nextRunAt: result.schedule.nextRunAt, - }, - }, - { status: 201 } - ) - } catch (error) { - logger.error(`[${requestId}] Error creating schedule`, error) - return NextResponse.json({ error: 'Failed to create schedule' }, { status: 500 }) - } -}) diff --git a/apps/sim/app/api/workflows/[id]/references/route.test.ts b/apps/sim/app/api/workflows/[id]/references/route.test.ts deleted file mode 100644 index 785a1ec2893..00000000000 --- a/apps/sim/app/api/workflows/[id]/references/route.test.ts +++ /dev/null @@ -1,88 +0,0 @@ -/** - * @vitest-environment node - */ -import { authMockFns, createMockRequest, workflowAuthzMockFns } from '@sim/testing' -import { beforeEach, describe, expect, it, vi } from 'vitest' - -const { mockGetWorkflowReferences } = vi.hoisted(() => ({ - mockGetWorkflowReferences: vi.fn(), -})) - -const mockAuthorizeWorkflow = workflowAuthzMockFns.mockAuthorizeWorkflowByWorkspacePermission - -vi.mock('@/lib/workflows/references/operations', () => ({ - getWorkflowReferences: mockGetWorkflowReferences, -})) - -import { GET } from '@/app/api/workflows/[id]/references/route' - -const mockGetSession = authMockFns.mockGetSession - -const REFERENCES = { - callers: [{ id: 'b', name: 'B', cycle: false, children: [] }], - callees: [], -} - -function callRoute(id = 'wf-1') { - const url = `http://localhost:3000/api/workflows/${id}/references` - return GET(createMockRequest('GET', undefined, {}, url), { params: Promise.resolve({ id }) }) -} - -describe('GET /api/workflows/[id]/references', () => { - beforeEach(() => { - vi.clearAllMocks() - mockGetSession.mockResolvedValue({ user: { id: 'user-1' } }) - mockAuthorizeWorkflow.mockResolvedValue({ - allowed: true, - status: 200, - workflow: { id: 'wf-1', workspaceId: 'ws-1' }, - workspacePermission: 'read', - }) - mockGetWorkflowReferences.mockResolvedValue(REFERENCES) - }) - - it('returns 401 without a session', async () => { - mockGetSession.mockResolvedValue(null) - const response = await callRoute() - expect(response.status).toBe(401) - expect(mockGetWorkflowReferences).not.toHaveBeenCalled() - }) - - it('returns 404 when the workflow does not exist', async () => { - mockAuthorizeWorkflow.mockResolvedValue({ - allowed: false, - status: 404, - message: 'Workflow not found', - workflow: null, - workspacePermission: null, - }) - const response = await callRoute() - expect(response.status).toBe(404) - expect(mockGetWorkflowReferences).not.toHaveBeenCalled() - }) - - it('returns 403 when the user cannot read the workflow', async () => { - mockAuthorizeWorkflow.mockResolvedValue({ - allowed: false, - status: 403, - message: 'Unauthorized: Access denied to read this workflow', - workflow: { id: 'wf-1', workspaceId: 'ws-1' }, - workspacePermission: null, - }) - const response = await callRoute() - expect(response.status).toBe(403) - expect(mockGetWorkflowReferences).not.toHaveBeenCalled() - }) - - it('returns the reference trees scoped to the workflow workspace', async () => { - const response = await callRoute() - expect(response.status).toBe(200) - expect(await response.json()).toEqual(REFERENCES) - expect(mockAuthorizeWorkflow).toHaveBeenCalledWith({ - workflowId: 'wf-1', - userId: 'user-1', - action: 'read', - }) - expect(mockGetWorkflowReferences).toHaveBeenCalledWith('ws-1', 'wf-1') - }) -}) diff --git a/apps/sim/app/api/workflows/[id]/references/route.ts b/apps/sim/app/api/workflows/[id]/references/route.ts deleted file mode 100644 index c2e5505e391..00000000000 --- a/apps/sim/app/api/workflows/[id]/references/route.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { authorizeWorkflowByWorkspacePermission } from '@sim/platform-authz/workflow' -import type { NextRequest } from 'next/server' -import { NextResponse } from 'next/server' -import { getWorkflowReferencesContract } from '@/lib/api/contracts/workflow-references' -import { parseRequest } from '@/lib/api/server' -import { getSession } from '@/lib/auth' -import { withRouteHandler } from '@/lib/core/utils/with-route-handler' -import { getWorkflowReferences } from '@/lib/workflows/references/operations' - -type RouteContext = { params: Promise<{ id: string }> } - -export const GET = withRouteHandler(async (request: NextRequest, context: RouteContext) => { - const session = await getSession() - if (!session?.user?.id) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) - } - - const parsed = await parseRequest(getWorkflowReferencesContract, request, context) - if (!parsed.success) return parsed.response - - const { id } = parsed.data.params - - const auth = await authorizeWorkflowByWorkspacePermission({ - workflowId: id, - userId: session.user.id, - action: 'read', - }) - if (!auth.allowed || !auth.workflow?.workspaceId) { - return NextResponse.json( - { error: auth.message ?? 'Access denied' }, - { status: auth.allowed ? 403 : auth.status } - ) - } - - const references = await getWorkflowReferences(auth.workflow.workspaceId, id) - return NextResponse.json(references) -}) diff --git a/apps/sim/app/f/[token]/public-file-auth.tsx b/apps/sim/app/f/[token]/public-file-auth.tsx index 005f8606248..c0682b0d123 100644 --- a/apps/sim/app/f/[token]/public-file-auth.tsx +++ b/apps/sim/app/f/[token]/public-file-auth.tsx @@ -2,8 +2,8 @@ import { useState } from 'react' import { cn, Input, Label } from '@sim/emcn' +import { Eye, EyeOff } from '@sim/emcn/icons' import { getErrorMessage } from '@sim/utils/errors' -import { Eye, EyeOff } from 'lucide-react' import { useRouter } from 'next/navigation' import { AuthSubmitButton } from '@/app/(auth)/components' import { PublicFileAuthShell } from '@/app/f/[token]/public-file-auth-shell' @@ -75,7 +75,7 @@ export function PublicFileAuth({ token }: PublicFileAuthProps) { className='-translate-y-1/2 absolute top-1/2 right-3 text-[var(--text-muted)] hover:text-[var(--text-primary)]' aria-label={showPassword ? 'Hide password' : 'Show password'} > - {showPassword ? : } + {showPassword ? : }
{error ?

{error}

: null} diff --git a/apps/sim/app/invite/components/status-card.tsx b/apps/sim/app/invite/components/status-card.tsx index 67ec787d8ce..fb0b1e6d433 100644 --- a/apps/sim/app/invite/components/status-card.tsx +++ b/apps/sim/app/invite/components/status-card.tsx @@ -34,10 +34,10 @@ export function InviteStatusCard({ return ( <>
-

+

Loading

-

{description}

+

{description}

@@ -49,10 +49,10 @@ export function InviteStatusCard({ return ( <>
-

+

{title}

-

{description}

+

{description}

@@ -78,7 +78,6 @@ export function InviteStatusCard({ )} - {/* + {/* Workspace layout dimensions: set CSS vars before hydration to avoid layout jump. IMPORTANT: These hardcoded values must stay in sync with stores/constants.ts @@ -93,9 +93,9 @@ export default function RootLayout({ children }: { children: React.ReactNode }) } // Sidebar width. Mirror clampSidebarWidth() in stores/sidebar/store.ts: - // the upper bound can never fall below the 248px minimum, so a narrow + // the upper bound can never fall below the 238px minimum, so a narrow // window yields a width >= MIN instead of a sub-minimum sliver. - var defaultSidebarWidth = 248; + var defaultSidebarWidth = 238; try { // Collapse comes from the cookie (independent of localStorage // parsing); the persisted width is read defensively below. Match the @@ -121,10 +121,10 @@ export default function RootLayout({ children }: { children: React.ReactNode }) // collapsed, because the desktop hover-peek renders the sidebar at // its restore width while --sidebar-width still reads collapsed. var width = state && state.sidebarWidth; - var maxSidebarWidth = Math.max(248, window.innerWidth * 0.3); + var maxSidebarWidth = Math.max(238, window.innerWidth * 0.3); var expandedWidth = typeof width === 'number' && isFinite(width) - ? Math.min(Math.max(width, 248), maxSidebarWidth) + ? Math.min(Math.max(width, 238), maxSidebarWidth) : defaultSidebarWidth; document.documentElement.style.setProperty( '--sidebar-expanded-width', diff --git a/apps/sim/app/playground/page.tsx b/apps/sim/app/playground/page.tsx index 4923394efa3..10560fa90da 100644 --- a/apps/sim/app/playground/page.tsx +++ b/apps/sim/app/playground/page.tsx @@ -77,14 +77,13 @@ import { ToastProvider, Tooltip, Trash, - Trash2, toast, Undo, Wrap, ZoomIn, ZoomOut, } from '@sim/emcn' -import { ArrowLeft, Folder, Moon, Sun } from 'lucide-react' +import { ArrowLeft, Folder, Moon, Sun } from '@sim/emcn/icons' import { notFound, useRouter } from 'next/navigation' import { env, isTruthy } from '@/lib/core/config/env' @@ -1050,7 +1049,6 @@ export default function PlaygroundPage() { { Icon: Redo, name: 'Redo' }, { Icon: Rocket, name: 'Rocket' }, { Icon: Trash, name: 'Trash' }, - { Icon: Trash2, name: 'Trash2' }, { Icon: Undo, name: 'Undo' }, { Icon: Wrap, name: 'Wrap' }, { Icon: ZoomIn, name: 'ZoomIn' }, diff --git a/apps/sim/app/sitemap.ts b/apps/sim/app/sitemap.ts index b468d883742..e0b7a0d6a8d 100644 --- a/apps/sim/app/sitemap.ts +++ b/apps/sim/app/sitemap.ts @@ -71,9 +71,6 @@ export default async function sitemap(): Promise { { url: `${baseUrl}/logs`, }, - { - url: `${baseUrl}/scheduled-tasks`, - }, { url: `${baseUrl}/pricing`, }, diff --git a/apps/sim/app/unsubscribe/unsubscribe.tsx b/apps/sim/app/unsubscribe/unsubscribe.tsx index 605f45a1943..5f3483accb1 100644 --- a/apps/sim/app/unsubscribe/unsubscribe.tsx +++ b/apps/sim/app/unsubscribe/unsubscribe.tsx @@ -43,9 +43,7 @@ function UnsubscribeContent() {

Loading

-

- Validating your unsubscribe link… -

+

Validating your unsubscribe link…

@@ -61,7 +59,7 @@ function UnsubscribeContent() {

Invalid Unsubscribe Link

-

{error}

+

{error}

@@ -80,7 +78,7 @@ function UnsubscribeContent() {

Important Account Emails

-

+

Transactional emails like password resets, account confirmations, and security alerts cannot be unsubscribed from as they contain essential information for your account.

@@ -102,7 +100,7 @@ function UnsubscribeContent() {

Successfully Unsubscribed

-

+

You have been unsubscribed from our emails. You will stop receiving emails within 48 hours.

@@ -125,10 +123,10 @@ function UnsubscribeContent() {

Email Preferences

-

+

Choose which emails you'd like to stop receiving.

-

{data?.email}

+

{data?.email}

@@ -145,14 +143,11 @@ function UnsubscribeContent() {
- - or choose specific types - + or choose specific types
handleUnsubscribe('marketing')} disabled={ processing || @@ -168,7 +163,6 @@ function UnsubscribeContent() { handleUnsubscribe('updates')} disabled={ processing || @@ -184,7 +178,6 @@ function UnsubscribeContent() { handleUnsubscribe('notifications')} disabled={ processing || @@ -200,7 +193,7 @@ function UnsubscribeContent() {
-

+

You'll continue receiving important account emails like password resets and security alerts.

@@ -218,9 +211,7 @@ export default function Unsubscribe() {

Loading

-

- Validating your unsubscribe link… -

+

Validating your unsubscribe link…

diff --git a/apps/sim/app/workspace/[workspaceId]/components/custom-tool-editor/generate-prompt-control.tsx b/apps/sim/app/workspace/[workspaceId]/components/custom-tool-editor/generate-prompt-control.tsx index a0d07c1e511..a0f290f045d 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/custom-tool-editor/generate-prompt-control.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/custom-tool-editor/generate-prompt-control.tsx @@ -2,7 +2,7 @@ import { useRef, useState } from 'react' import { Chip, ChipInput } from '@sim/emcn' -import { ArrowUp } from 'lucide-react' +import { ArrowUp } from '@sim/emcn/icons' interface GeneratePromptControlProps { isLoading: boolean @@ -42,7 +42,7 @@ export function GeneratePromptControl({ if (!isActive) { return ( - + Generate ) @@ -72,7 +72,6 @@ export function GeneratePromptControl({ placeholder='Describe what to generate...' />
- {icon ?? } + {icon ?? }
-

+

{title}

diff --git a/apps/sim/app/workspace/[workspaceId]/components/index.ts b/apps/sim/app/workspace/[workspaceId]/components/index.ts index 02855214e23..16570ad0070 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/components/index.ts @@ -2,6 +2,7 @@ export { ConversationListItem } from './conversation-list-item' export type { ErrorBoundaryProps, ErrorStateProps } from './error' export { ErrorShell, ErrorState } from './error' export { InlineRenameInput } from './inline-rename-input' +export { IntegrationTabsHeader } from './integration-tabs-header' export { MessageActions } from './message-actions' export { FloatingOverflowText } from './resource/components/floating-overflow-text' export { ownerCell } from './resource/components/owner-cell' diff --git a/apps/sim/app/workspace/[workspaceId]/components/integration-tabs-header/index.ts b/apps/sim/app/workspace/[workspaceId]/components/integration-tabs-header/index.ts new file mode 100644 index 00000000000..1706152ecf3 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/components/integration-tabs-header/index.ts @@ -0,0 +1 @@ +export { IntegrationTabsHeader } from './integration-tabs-header' diff --git a/apps/sim/app/workspace/[workspaceId]/integrations/components/integration-tabs-header/integration-tabs-header.tsx b/apps/sim/app/workspace/[workspaceId]/components/integration-tabs-header/integration-tabs-header.tsx similarity index 56% rename from apps/sim/app/workspace/[workspaceId]/integrations/components/integration-tabs-header/integration-tabs-header.tsx rename to apps/sim/app/workspace/[workspaceId]/components/integration-tabs-header/integration-tabs-header.tsx index 3706f107890..53efc5a0175 100644 --- a/apps/sim/app/workspace/[workspaceId]/integrations/components/integration-tabs-header/integration-tabs-header.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/integration-tabs-header/integration-tabs-header.tsx @@ -5,13 +5,20 @@ import { HEADER_ACTION_CLUSTER, PAGE_HEADER_BAR } from '@/components/page-header interface IntegrationTabsHeaderProps { active: 'integrations' | 'skills' workspaceId: string + /** Trailing actions for the owning page (e.g. skills' "Add skill"). */ rightSlot?: ReactNode } /** - * Top-of-page chip header shared by the Integrations and Skills pages. - * Highlights the active tab and links to the sibling tab; `rightSlot` lets - * each page render its own trailing actions (e.g. an "Add skill" button). + * Top-of-page tab header shared by the Integrations and Skills pages — two halves + * of one surface, so each highlights itself and links to its sibling. + * + * Lives in the shared workspace components rather than under `integrations/` + * because both pages own it equally; its former home made Skills reach across into + * a sibling feature for its own chrome. + * + * The `gap-1` is explicit because chips carry no outer margin — the parent owns the + * space between them. */ export function IntegrationTabsHeader({ active, @@ -19,7 +26,7 @@ export function IntegrationTabsHeader({ rightSlot, }: IntegrationTabsHeaderProps) { return ( -

+
Integrations diff --git a/apps/sim/app/workspace/[workspaceId]/components/invite-modal/invite-modal.tsx b/apps/sim/app/workspace/[workspaceId]/components/invite-modal/invite-modal.tsx index c110e1b064e..9b21c25ed31 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/invite-modal/invite-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/invite-modal/invite-modal.tsx @@ -327,7 +327,6 @@ export function InviteModal({ searchable={isOrganizationInvite} searchPlaceholder='Search workspaces...' fullWidth - flush disabled={isSubmitting || !canInvite || workspaceOptions.length <= 1} /> diff --git a/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-header/resource-header.tsx b/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-header/resource-header.tsx index 9f295fe229a..d70a12049b8 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-header/resource-header.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-header/resource-header.tsx @@ -1,3 +1,5 @@ +'use client' + import { type ComponentType, Fragment, @@ -29,7 +31,7 @@ import { useFloatingTooltip, useIsOverflowing, } from '@sim/emcn' -import { ArrowUpLeft } from 'lucide-react' +import { ArrowUpLeft } from '@sim/emcn/icons' import { createPortal } from 'react-dom' import { HEADER_ACTION_CLUSTER, TITLE_BAR_LANE_PT } from '@/components/page-header-bar' import { orderHeaderActions } from '@/components/settings/settings-header' @@ -309,10 +311,7 @@ const BreadcrumbSegment = memo(function BreadcrumbSegment({ * rounded-[5px] / justify-center and break chip parity with the static/title * crumbs. */ - const triggerClassName = cn( - chipVariants({ flush: true }), - 'group min-w-0 max-w-full justify-start' - ) + const triggerClassName = cn(chipVariants(), 'group min-w-0 max-w-full justify-start') if (dropdownItems && dropdownItems.length > 0) { return ( @@ -459,7 +458,7 @@ function BreadcrumbLocationPopover({ onMouseEnter={openPopover} onMouseLeave={scheduleClose} className={cn( - chipVariants({ flush: true }), + chipVariants(), 'max-w-none gap-1.5 px-2 transition-colors', open && 'relative z-[var(--z-popover)]', className @@ -467,10 +466,7 @@ function BreadcrumbLocationPopover({ > - + {rootBreadcrumb?.label && ( diff --git a/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-options/resource-options.tsx b/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-options/resource-options.tsx index de67d5ef176..101acc67ee8 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-options/resource-options.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-options/resource-options.tsx @@ -1,3 +1,5 @@ +'use client' + import { memo, type ReactNode, useState } from 'react' import * as PopoverPrimitive from '@radix-ui/react-popover' import { @@ -143,7 +145,7 @@ export const ResourceOptions = memo(function ResourceOptions({ and only the trailing action is pushed to the far edge. */}
{aside} -
+
{filterTags?.map((tag) => ( {tag.label} @@ -163,7 +165,7 @@ export const ResourceOptions = memo(function ResourceOptions({ } > -
+
Filter diff --git a/apps/sim/app/workspace/[workspaceId]/components/resource/resource.tsx b/apps/sim/app/workspace/[workspaceId]/components/resource/resource.tsx index 707cfdaeb9f..721d3bc0c5a 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/resource/resource.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/resource/resource.tsx @@ -21,9 +21,8 @@ import { cn, Loader, } from '@sim/emcn' -import { Pin } from '@sim/emcn/icons' +import { ChevronLeft, ChevronRight, Pin } from '@sim/emcn/icons' import { useVirtualizer } from '@tanstack/react-virtual' -import { ChevronLeft, ChevronRight } from 'lucide-react' import { InlineRenameInput } from '@/app/workspace/[workspaceId]/components/inline-rename-input' import { FloatingOverflowText } from '@/app/workspace/[workspaceId]/components/resource/components/floating-overflow-text' import { ResourceHeader } from '@/app/workspace/[workspaceId]/components/resource/components/resource-header' @@ -339,7 +338,7 @@ const ResourceTable = memo(function ResourceTable({
{hasCheckbox && ( diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/document-table.css b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/document-table.css index 6a73506634b..d8832305035 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/document-table.css +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/document-table.css @@ -25,7 +25,7 @@ .document-table th, .document-table td { position: relative; - border: 1px solid var(--divider); + border: var(--border-width) solid var(--border); padding: 0.5rem 0.75rem; text-align: left; vertical-align: top; diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/document-table.test.ts b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/document-table.test.ts index 753d4f5c343..0fb8336c705 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/document-table.test.ts +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/document-table.test.ts @@ -6,7 +6,7 @@ * (`.document-table`) sit in the same file viewer, in the same session, one click apart. They used * to drift — the previews carried their own chrome (rounded outer frame, `--surface-2` header, * 13px body / 12px header, `--text-secondary` cells) while markdown tables used full cell borders - * on `--divider`, a `--surface-4` header, and 14px text. + * on `--border`, a `--surface-4` header, and 14px text. * * These load the real, shipped CSS (not a copy). Two complementary assertions, because jsdom's CSS * engine resolves only part of what matters here: it applies the cascade for longhand declarations @@ -124,7 +124,9 @@ describe('document-table chrome is shared with markdown tables', () => { }) it('one rule draws the cell border for both roots', () => { - expect(selectorsDeclaring(SHARED_CSS_PATH, 'border', 'var(--divider)')).toEqual( + expect( + selectorsDeclaring(SHARED_CSS_PATH, 'border', 'var(--border-width) solid var(--border)') + ).toEqual( expect.arrayContaining([ '.rich-markdown-prose th', '.rich-markdown-prose td', diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/editor-context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/editor-context-menu.tsx index 3cfd6c0e228..08205839da4 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/editor-context-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/editor-context-menu.tsx @@ -8,8 +8,7 @@ import { DropdownMenuShortcut, DropdownMenuTrigger, } from '@sim/emcn' -import { Blimp, Clipboard, Duplicate, Search, SelectAll } from '@sim/emcn/icons' -import { Scissors } from 'lucide-react' +import { Blimp, Clipboard, Duplicate, Scissors, Search, SelectAll } from '@sim/emcn/icons' interface EditorContextMenuProps { isOpen: boolean diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx index 006012191ac..d4b8bce907a 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx @@ -1,7 +1,7 @@ 'use client' import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' -import { Music } from 'lucide-react' +import { Music } from '@sim/emcn/icons' import dynamic from 'next/dynamic' import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace' import { getFileExtension } from '@/lib/uploads/utils/file-utils' @@ -398,7 +398,7 @@ const MediaPreview = memo(function MediaPreview({ return (
- +

{file.name}

{blobUrl && ( diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/preview-toolbar.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/preview-toolbar.tsx index 0d8a326a881..889fa350bd2 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/preview-toolbar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/preview-toolbar.tsx @@ -1,5 +1,5 @@ import { Chip, cn } from '@sim/emcn' -import { ChevronLeft, ChevronRight, ZoomIn, ZoomOut } from 'lucide-react' +import { ChevronLeft, ChevronRight, ZoomIn, ZoomOut } from '@sim/emcn/icons' interface PreviewNavigationControls { current: number @@ -34,10 +34,10 @@ export function PreviewToolbar({ navigation, zoom, className }: PreviewToolbarPr className )} > -
+
{navigation && }
-
{zoom && }
+
{zoom && }
) } diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/code-block.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/code-block.tsx index 2464ccaec44..d155c2540a8 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/code-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/code-block.tsx @@ -8,9 +8,9 @@ import { DropdownMenuTrigger, useCopyToClipboard, } from '@sim/emcn' +import { Check, ChevronDown, Code, Duplicate, Eye, Wrap } from '@sim/emcn/icons' import type { ReactNodeViewProps } from '@tiptap/react' import { NodeViewContent, NodeViewWrapper, ReactNodeViewRenderer } from '@tiptap/react' -import { Check, ChevronDown, Code, Copy, Eye, WrapText } from 'lucide-react' import { looksLikeMermaid, MermaidDiagram } from '../mermaid-diagram' import { MarkdownCodeBlock } from './code-block-schema' import { detectLanguage } from './detect-language' @@ -154,7 +154,7 @@ function CodeBlockView({ node, updateAttributes, editor, getPos }: ReactNodeView type='button' aria-label='Code language' className={cn( - chipVariants({ variant: 'default', flush: true }), + chipVariants({ variant: 'default' }), 'h-[24px] gap-1 px-1.5 text-[var(--text-muted)] data-[state=open]:bg-[var(--surface-active)] data-[state=open]:text-[var(--text-body)]' )} > @@ -192,7 +192,7 @@ function CodeBlockView({ node, updateAttributes, editor, getPos }: ReactNodeView wrap && 'bg-[var(--surface-active)] text-[var(--text-body)]' )} > - + )}
diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/mention/mention-list.test.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/mention/mention-list.test.tsx
index 2f5241d11ca..8ff82847a9d 100644
--- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/mention/mention-list.test.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/mention/mention-list.test.tsx
@@ -7,9 +7,9 @@
  * The second test drives the real `ReactRenderer` path the suggestion plugin actually uses.
  */
 import { act, createRef } from 'react'
+import { File } from '@sim/emcn/icons'
 import { Editor } from '@tiptap/core'
 import { EditorContent, ReactRenderer } from '@tiptap/react'
-import { File } from 'lucide-react'
 import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
 import { createMarkdownEditorExtensions } from '../editor-extensions'
 import { MentionList, type MentionListHandle } from './mention-list'
diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/mention/types.ts b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/mention/types.ts
index dd87663dcc7..a26eb56869b 100644
--- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/mention/types.ts
+++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/mention/types.ts
@@ -24,6 +24,6 @@ export interface MentionItem {
   label: string
   /** Category heading the item is shown under. */
   group: string
-  /** Optional per-item icon (Lucide category icon or a brand block icon). */
+  /** Optional per-item icon (emcn category icon or a brand block icon). */
   icon?: ComponentType<{ className?: string }>
 }
diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/bubble-menu.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/bubble-menu.tsx
index ca2acb30e0d..a30e1337510 100644
--- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/bubble-menu.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/bubble-menu.tsx
@@ -1,11 +1,6 @@
 import { useCallback, useEffect, useRef, useState } from 'react'
-import { Blimp } from '@sim/emcn/icons'
-import { posToDOMRect } from '@tiptap/core'
-import { PluginKey } from '@tiptap/pm/state'
-import type { Editor } from '@tiptap/react'
-import { useEditorState } from '@tiptap/react'
-import { BubbleMenu } from '@tiptap/react/menus'
 import {
+  Blimp,
   Bold,
   Check,
   Code,
@@ -20,7 +15,12 @@ import {
   Strikethrough,
   TextQuote,
   Unlink,
-} from 'lucide-react'
+} from '@sim/emcn/icons'
+import { posToDOMRect } from '@tiptap/core'
+import { PluginKey } from '@tiptap/pm/state'
+import type { Editor } from '@tiptap/react'
+import { useEditorState } from '@tiptap/react'
+import { BubbleMenu } from '@tiptap/react/menus'
 import { applyLink, LinkUrlInput } from './link-editing'
 import { ToolbarButton, ToolbarDivider } from './toolbar-button'
 
diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/link-hover-card.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/link-hover-card.tsx
index b56094f2e0b..015b61465ea 100644
--- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/link-hover-card.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/link-hover-card.tsx
@@ -1,9 +1,9 @@
 import { useCallback, useEffect, useRef, useState } from 'react'
 import { autoUpdate, computePosition, flip, offset, shift } from '@floating-ui/dom'
 import { useCopyToClipboard } from '@sim/emcn'
+import { Check, Duplicate, Pencil, Unlink } from '@sim/emcn/icons'
 import { getMarkRange } from '@tiptap/core'
 import type { Editor } from '@tiptap/react'
-import { Check, Copy, Pencil, Unlink } from 'lucide-react'
 import { createPortal } from 'react-dom'
 import { normalizeLinkHref } from '../markdown-fidelity'
 import { applyLink, LinkUrlInput } from './link-editing'
@@ -178,7 +178,7 @@ export function LinkHoverCard({ editor }: LinkHoverCardProps) {
             
           )}
            {
               void copy(rawHref)
diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/table-menu.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/table-menu.tsx
index e62fa8fdcb0..4d7073ccd4e 100644
--- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/table-menu.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/table-menu.tsx
@@ -1,9 +1,4 @@
 import { useCallback, useState } from 'react'
-import { posToDOMRect } from '@tiptap/core'
-import { PluginKey } from '@tiptap/pm/state'
-import type { Editor } from '@tiptap/react'
-import { useEditorState } from '@tiptap/react'
-import { BubbleMenu } from '@tiptap/react/menus'
 import {
   ArrowDown,
   ArrowLeft,
@@ -12,8 +7,13 @@ import {
   Columns3,
   Rows3,
   Table as TableIcon,
-  Trash2,
-} from 'lucide-react'
+  Trash,
+} from '@sim/emcn/icons'
+import { posToDOMRect } from '@tiptap/core'
+import { PluginKey } from '@tiptap/pm/state'
+import type { Editor } from '@tiptap/react'
+import { useEditorState } from '@tiptap/react'
+import { BubbleMenu } from '@tiptap/react/menus'
 import { ToolbarButton, ToolbarDivider } from './toolbar-button'
 
 /** Pins the toolbar to the viewport instead of tracking the (often wide) table as it scrolls horizontally. */
@@ -118,7 +118,7 @@ export function TableBubbleMenu({ editor, scrollContainerRef }: TableBubbleMenuP
         onClick={() => editor.chain().focus().toggleHeaderRow().run()}
       />
        editor.chain().focus().deleteTable().run()}
diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/toolbar-button.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/toolbar-button.tsx
index bc77d683610..b8acd061158 100644
--- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/toolbar-button.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/toolbar-button.tsx
@@ -2,7 +2,7 @@ import type { ComponentType, SVGProps } from 'react'
 import { cn, Tooltip } from '@sim/emcn'
 
 interface ToolbarButtonProps {
-  /** Any SVG icon component — Lucide icons and `@sim/emcn/icons` both satisfy this. */
+  /** Any SVG icon component, e.g. from `@sim/emcn/icons`. */
   icon: ComponentType>
   label: string
   shortcut?: string
diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/rich-markdown-editor.css b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/rich-markdown-editor.css
index 4258b9901b9..1af8faa0bd0 100644
--- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/rich-markdown-editor.css
+++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/rich-markdown-editor.css
@@ -4,7 +4,7 @@
   color: var(--text-primary);
   font-family: var(--font-inter);
   font-size: 15px;
-  font-weight: 430;
+  font-weight: 400;
   line-height: 25px;
   letter-spacing: 0;
   overflow-wrap: anywhere;
@@ -14,7 +14,7 @@
   max-width: 100%;
   height: auto;
   border-radius: 8px;
-  border: 1px solid var(--border);
+  border: var(--border-width) solid var(--border);
 }
 
 /* One consistent ring for every node that can only be selected as a whole (divider, image,
@@ -204,7 +204,7 @@
   width: 16px;
   height: 16px;
   margin: 0;
-  border: 1px solid var(--border-1);
+  border: var(--border-width) solid var(--border-1);
   border-radius: 3px;
   background: transparent;
   cursor: pointer;
@@ -224,7 +224,7 @@
 }
 
 .rich-markdown-prose blockquote {
-  border-left: 2px solid var(--divider);
+  border-left: 2px solid var(--border);
   padding-left: 1rem;
   color: var(--text-primary);
   font-style: italic;
@@ -331,7 +331,7 @@
 
 .rich-markdown-prose hr {
   border: none;
-  border-top: 1px solid var(--divider);
+  border-top: var(--border-width) solid var(--border);
   margin: 1.5em 0;
 }
 
diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/slash-command/commands.ts b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/slash-command/commands.ts
index 5878b352cb8..000a434eba4 100644
--- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/slash-command/commands.ts
+++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/slash-command/commands.ts
@@ -1,19 +1,19 @@
-import type { Editor, Range } from '@tiptap/core'
+import type { ComponentType, SVGProps } from 'react'
 import {
-  Code2,
+  Code,
   Heading1,
   Heading2,
   Heading3,
-  Image as ImageIcon,
+  ImageUp as ImageIcon,
   List,
   ListChecks,
   ListOrdered,
-  type LucideIcon,
   Minus,
   Pilcrow,
   Table as TableIcon,
   TextQuote,
-} from 'lucide-react'
+} from '@sim/emcn/icons'
+import type { Editor, Range } from '@tiptap/core'
 
 export interface SlashCommandContext {
   editor: Editor
@@ -33,7 +33,7 @@ export interface SlashCommandItem {
   title: string
   /** Group heading the item is shown under in the menu. */
   group: string
-  icon: LucideIcon
+  icon: ComponentType>
   /** Extra search terms matched against the slash query, beyond the title. */
   aliases: string[]
   /** Keyboard shortcut shown on the right of the item (omitted when there is none). */
@@ -116,7 +116,7 @@ export const SLASH_COMMANDS: readonly SlashCommandItem[] = [
   {
     title: 'Code block',
     group: 'Blocks',
-    icon: Code2,
+    icon: Code,
     aliases: ['codeblock', 'snippet', 'fence'],
     shortcut: '⌘⌥C',
     run: ({ editor, range }) => editor.chain().focus().deleteRange(range).toggleCodeBlock().run(),
diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/xlsx-preview.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/xlsx-preview.tsx
index 6e15f70cde2..5ff5c26b3f4 100644
--- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/xlsx-preview.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/xlsx-preview.tsx
@@ -115,7 +115,7 @@ export const XlsxPreview = memo(function XlsxPreview({
   return (
     
-
+
{sheetNames.map((name, i) => ( , }, - scheduledtask: { - label: 'Scheduled task', - renderIcon: ({ className }) => , - }, past_chat: { label: 'Past chat', renderIcon: ({ className }) => , diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-permission-card.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-permission-card.tsx index 830634c4a9d..89e3b367874 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-permission-card.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-permission-card.tsx @@ -143,9 +143,11 @@ export function ToolPermissionCard({ return (
- {/* Chips carry their own `mx-0.5` cluster spacing, so this row sets no gap. */} -
- +
+ {/* 16px icon + 2px + the row's 4px gap = the 22px text inset the sibling + rows below are tuned to (`pl-[22px]`, and `gap-1.5` on an unmargined + icon). Margin and gap add, so this cannot be `mr-1.5`. */} + {preview ? ( @@ -190,7 +192,7 @@ export function ToolPermissionCard({
{showBulkActions && ( -
+
{outstandingIds.length} tools need permission diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.tsx index abedf727bf4..17b24c9cd17 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.tsx @@ -40,16 +40,16 @@ const LANG_ALIASES: Record = { const PROSE_CLASSES = cn( 'prose prose-base dark:prose-invert max-w-none', - 'font-[family-name:var(--font-inter)] antialiased break-words font-[430] tracking-[0]', - 'prose-headings:font-[600] prose-headings:tracking-[0] prose-headings:text-[var(--text-primary)]', + 'font-[family-name:var(--font-inter)] antialiased break-words tracking-[0]', + 'prose-headings:font-semibold prose-headings:tracking-[0] prose-headings:text-[var(--text-primary)]', 'prose-headings:mb-3 prose-headings:mt-6 first:prose-headings:mt-0', 'prose-p:text-base prose-p:leading-[25px] prose-p:text-[var(--text-primary)]', 'prose-li:text-base prose-li:leading-[25px] prose-li:text-[var(--text-primary)]', 'prose-li:my-1', 'prose-ul:my-4 prose-ol:my-4', - 'prose-strong:font-[600] prose-strong:text-[var(--text-primary)]', + 'prose-strong:font-semibold prose-strong:text-[var(--text-primary)]', 'prose-a:text-[var(--text-primary)] prose-a:underline prose-a:decoration-dashed prose-a:underline-offset-4', - 'prose-hr:border-[var(--divider)] prose-hr:my-6', + 'prose-hr:border-[var(--border)] prose-hr:my-6', 'prose-table:my-0' ) @@ -195,7 +195,7 @@ function highlight(code: string, language: string): string { const MARKDOWN_COMPONENTS = { table({ children }: { children?: React.ReactNode }) { return ( -
+
{children}
@@ -209,7 +209,7 @@ const MARKDOWN_COMPONENTS = { return ( {children} @@ -219,7 +219,7 @@ const MARKDOWN_COMPONENTS = { return ( {children} @@ -232,7 +232,7 @@ const MARKDOWN_COMPONENTS = { if (!codeString) { return ( -
+        
           {children}
         
) @@ -241,8 +241,8 @@ const MARKDOWN_COMPONENTS = { const html = highlight(codeString.trimEnd(), language) return ( -
-
+
+
{language || 'code'}
         
@@ -349,14 +349,14 @@ const MARKDOWN_COMPONENTS = { }, inlineCode({ children }: { children?: React.ReactNode }) { return ( - + {children} ) }, blockquote({ children }: { children?: React.ReactNode }) { return ( -
+
{children}
) @@ -380,7 +380,7 @@ const MARKDOWN_COMPONENTS = { src={src} alt={alt ?? ''} loading='lazy' - className='my-4 h-auto max-w-full rounded-lg border border-[var(--divider)]' + className='my-4 h-auto max-w-full rounded-lg border border-[var(--border)]' /> ) }, diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/options/options.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/options/options.tsx index ea0f2fc82a2..080edad2af7 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/options/options.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/options/options.tsx @@ -15,7 +15,7 @@ export function Options({ items, onSelect }: OptionsProps) { key={item.id} type='button' onClick={() => onSelect?.(item.id)} - className='rounded-full border border-[var(--divider)] bg-[var(--bg)] px-3.5 py-1.5 font-[430] font-[family-name:var(--font-inter)] text-[var(--text-primary)] text-sm leading-5 transition-colors hover-hover:bg-[var(--surface-5)]' + className='rounded-full border border-[var(--border)] bg-[var(--bg)] px-3.5 py-1.5 font-[family-name:var(--font-inter)] text-[var(--text-primary)] text-sm leading-5 transition-colors hover-hover:bg-[var(--surface-5)]' > {item.label} diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/question/question.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/question/question.tsx index 53811b94c04..44b27dba5ff 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/question/question.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/question/question.tsx @@ -47,7 +47,7 @@ export function parseQuestionAnswerMessage( } const OPTION_ROW_CLASSES = - 'flex items-center gap-2 border-[var(--divider)] px-2 py-2 text-left transition-colors' + 'flex items-center gap-2 border-[var(--border)] px-2 py-2 text-left transition-colors' /** Ghost icon-button chrome shared by the stepper chevrons and the dismiss X. */ const ICON_BUTTON_CLASSES = 'relative size-[14px] flex-shrink-0 p-0' diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx index 63e32cdd908..6372643c46f 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx @@ -1404,7 +1404,7 @@ function OptionsDisplay({ data, onSelect }: OptionsDisplayProps) { disabled={disabled} onClick={() => onSelect?.(title)} className={cn( - 'flex items-center gap-2 border-[var(--divider)] px-2 py-2 text-left transition-colors', + 'flex items-center gap-2 border-[var(--border)] px-2 py-2 text-left transition-colors', disabled ? 'cursor-not-allowed' : 'hover-hover:bg-[var(--surface-5)]', i > 0 && 'border-t' )} @@ -1637,7 +1637,7 @@ function SecretInputDisplay({ data }: { data: CredentialTagData }) {
@@ -2031,13 +2031,13 @@ function UsageUpgradeDisplay({ data }: { data: UsageUpgradeTagData }) { target={isHosted ? undefined : '_blank'} rel={isHosted ? undefined : 'noopener noreferrer'} aria-label={isHosted ? undefined : `${buttonLabel} (opens in a new tab)`} - className='mt-2 inline-flex items-center gap-1 font-[500] text-amber-700 text-small underline decoration-dashed underline-offset-2 transition-colors hover-hover:text-amber-900 dark:text-amber-300 dark:hover-hover:text-amber-200' + className='mt-2 inline-flex items-center gap-1 font-medium text-amber-700 text-small underline decoration-dashed underline-offset-2 transition-colors hover-hover:text-amber-900 dark:text-amber-300 dark:hover-hover:text-amber-200' > {buttonLabel} {isHosted ? : } ) : ( -

+

{unavailableMessage}

)} diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/utils.ts b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/utils.ts index bae5654b19e..8079fc40de6 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/utils.ts @@ -19,8 +19,7 @@ import { TerminalWindow, Wrench, } from '@sim/emcn' -import { Calendar, Clock, Cursor, Table as TableIcon } from '@sim/emcn/icons' -import { Globe } from 'lucide-react' +import { Calendar, Clock, Cursor, Globe, Table as TableIcon } from '@sim/emcn/icons' import { AgentIcon, ImageIcon, TTSIcon, VideoIcon } from '@/components/icons' import type { ToolCallStatus } from '@/app/workspace/[workspaceId]/home/types' @@ -59,7 +58,6 @@ const TOOL_ICONS: Record = { search_knowledge_base: Database, table: TableIcon, query_user_table: TableIcon, - scheduled_task: Calendar, job: Calendar, agent: AgentIcon, custom_tool: Wrench, diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/components/mothership-chat-skeleton/mothership-chat-skeleton.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/components/mothership-chat-skeleton/mothership-chat-skeleton.tsx index 3e8838e2cee..9b32c21b1dd 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/components/mothership-chat-skeleton/mothership-chat-skeleton.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/components/mothership-chat-skeleton/mothership-chat-skeleton.tsx @@ -2,7 +2,7 @@ import { Skeleton } from '@sim/emcn' const LAYOUT_SKELETON_STYLES = { 'mothership-view': { - content: 'mx-auto max-w-[48rem] space-y-6', + content: 'mx-auto max-w-chat space-y-6', userRow: 'flex flex-col items-end gap-[6px] pt-3', }, 'copilot-view': { diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx index fb33d9ec1de..16e9553545c 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx @@ -116,14 +116,14 @@ const LAYOUT_STYLES = { 'mothership-view': { scrollContainer: 'mt-[var(--workspace-content-title-bar-inset)] min-h-0 flex-1 overflow-y-auto overflow-x-hidden px-6 pt-4 pb-2 [overflow-anchor:none] [scrollbar-gutter:stable_both-edges]', - sizer: 'relative mx-auto w-full max-w-[48rem]', + sizer: 'relative mx-auto w-full max-w-chat', rowGap: 'pb-6', userRow: 'flex flex-col items-end gap-[6px] pt-3', attachmentWidth: 'max-w-[70%]', userBubble: 'max-w-[70%] overflow-hidden rounded-[16px] bg-[var(--surface-5)] px-3.5 py-2', assistantRow: 'group/msg', footer: 'flex-shrink-0 px-[24px] pb-[16px]', - footerInner: 'mx-auto max-w-[48rem]', + footerInner: 'mx-auto max-w-chat', }, 'copilot-view': { scrollContainer: diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown/add-resource-dropdown.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown/add-resource-dropdown.tsx index 5e2c9093c86..6fe54a3678e 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown/add-resource-dropdown.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown/add-resource-dropdown.tsx @@ -16,7 +16,6 @@ import { Tooltip, } from '@sim/emcn' import { Folder, Plus } from '@sim/emcn/icons' -import { truncate } from '@sim/utils/string' import { isBrowserAgentAvailable } from '@/lib/browser-agent/transport' import { BROWSER_SESSION_RESOURCE_ID, @@ -43,7 +42,6 @@ import { useFolders } from '@/hooks/queries/folders' import { useKnowledgeBasesQuery } from '@/hooks/queries/kb/knowledge' import { useLogsList } from '@/hooks/queries/logs' import { useMothershipChats } from '@/hooks/queries/mothership-chats' -import { useWorkspaceSchedules } from '@/hooks/queries/schedules' import { useTablesList } from '@/hooks/queries/tables' import { useWorkflows } from '@/hooks/queries/workflows' import { useWorkspaceFileFolders } from '@/hooks/queries/workspace-file-folders' @@ -173,9 +171,6 @@ export function useAvailableResources( { enabled } ) const { data: tasks, isPending: tasksPending } = useMothershipChats(workspaceId, { enabled }) - const { data: schedules, isPending: schedulesPending } = useWorkspaceSchedules(workspaceId, { - enabled, - }) const { data: logsData, isPending: logsPending } = useLogsList( workspaceId, LOG_DROPDOWN_FILTERS, @@ -202,7 +197,6 @@ export function useAvailableResources( foldersPending || fileFoldersPending || tasksPending || - schedulesPending || logsPending) const groups = useMemo(() => { @@ -268,15 +262,6 @@ export function useAvailableResources( type: 'task' as const, items: (tasks ?? []).map((t) => ({ id: t.id, name: t.name })), }, - { - type: 'scheduledtask' as const, - items: (schedules ?? []) - .filter((s) => s.sourceType === 'job') - .map((s) => ({ - id: s.id, - name: s.jobTitle || truncate(s.prompt ?? '', 40) || 'Scheduled Task', - })), - }, { type: 'log' as const, items: logs.map((log) => { @@ -322,7 +307,6 @@ export function useAvailableResources( files, knowledgeBases, tasks, - schedules, logs, excludeTypes, ]) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-session.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-session.tsx index c6e0869218c..c30220ceaa7 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-session.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-session.tsx @@ -30,8 +30,7 @@ import { PopoverContent, PopoverItem, } from '@sim/emcn' -import { ArrowLeft, ArrowRight, Key, Link, RefreshCw, Search } from '@sim/emcn/icons' -import { Globe } from 'lucide-react' +import { ArrowLeft, ArrowRight, Globe, Key, Link, RefreshCw, Search } from '@sim/emcn/icons' import { useTheme } from 'next-themes' import { createPortal } from 'react-dom' import { @@ -1036,7 +1035,7 @@ export function BrowserSession({ type='button' aria-label='Browser menu' title='Browser menu' - className={cn(chipVariants({ flush: true }), 'flex-shrink-0')} + className={cn(chipVariants(), 'flex-shrink-0')} > diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx index 89f35608f31..f1bfdc3086c 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx @@ -3,7 +3,6 @@ import { lazy, memo, Suspense, useEffect, useMemo, useRef, useState } from 'react' import { Button, PlayOutline, Skeleton, Tooltip, toast } from '@sim/emcn' import { - Calendar, Download, FileX, Folder as FolderIcon, @@ -14,7 +13,6 @@ import { WorkflowX, } from '@sim/emcn/icons' import { createLogger } from '@sim/logger' -import { format } from 'date-fns' import { useRouter } from 'next/navigation' import { isApiClientError } from '@/lib/api/client/errors' import { useSession } from '@/lib/auth/auth-client' @@ -28,7 +26,6 @@ import { import { canonicalWorkspaceFilePath } from '@/lib/copilot/vfs/path-utils' import { triggerFileDownload } from '@/lib/uploads/client/download' import { getFileExtension, getMimeTypeFromExtension } from '@/lib/uploads/utils/file-utils' -import { parseCronToHumanReadable } from '@/lib/workflows/schedules/utils' import { FileViewer, type PreviewMode, @@ -59,7 +56,6 @@ import { useUsageLimits } from '@/app/workspace/[workspaceId]/w/[workflowId]/com import { useWorkflowExecution } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution' import { useFolders } from '@/hooks/queries/folders' import { useLogDetail } from '@/hooks/queries/logs' -import { useScheduleById } from '@/hooks/queries/schedules' import { downloadTableExport } from '@/hooks/queries/tables' import { useWorkflows } from '@/hooks/queries/workflows' import { useWorkspaceFiles } from '@/hooks/queries/workspace-files' @@ -280,15 +276,6 @@ export const ResourceContent = memo(function ResourceContent({ case 'folder': return - case 'scheduledtask': - return ( - - ) - case 'log': return ( - case 'scheduledtask': - return case 'folder': case 'generic': case 'browser': @@ -795,137 +780,6 @@ function EmbeddedFolder({ workspaceId, folderId }: EmbeddedFolderProps) { ) } -const SCHEDULE_STATUS_LABEL: Record = { - active: 'Active', - disabled: 'Paused', - completed: 'Completed', -} - -function formatScheduleInstant(iso: string | null): string { - if (!iso) return '—' - const date = new Date(iso) - return Number.isNaN(date.getTime()) ? '—' : format(date, "EEE, MMM d 'at' h:mm a") -} - -interface ScheduledTaskFieldProps { - title: string - value: string -} - -function ScheduledTaskField({ title, value }: ScheduledTaskFieldProps) { - return ( -
- {title} - {value} -
- ) -} - -interface EmbeddedScheduledTaskProps { - workspaceId: string - scheduleId: string -} - -function EmbeddedScheduledTask({ scheduleId }: EmbeddedScheduledTaskProps) { - const { data: schedule, isLoading, isError } = useScheduleById(scheduleId) - - if (isLoading && !schedule) return LOADING_SKELETON - - if (!schedule) { - const heading = isError ? "Couldn't load scheduled task" : 'Scheduled task not found' - const detail = isError - ? 'Something went wrong loading this scheduled task. Try again.' - : 'This scheduled task may have been deleted' - return ( -
- -
-

{heading}

-

{detail}

-
-
- ) - } - - const title = schedule.jobTitle || schedule.prompt || 'Scheduled task' - const timing = schedule.cronExpression - ? parseCronToHumanReadable(schedule.cronExpression, schedule.timezone) - : 'Runs once' - const status = SCHEDULE_STATUS_LABEL[schedule.status] ?? schedule.status - - return ( -
-
- -

{title}

-
- -
- - - - -
- -
- Prompt -

- {schedule.prompt || '—'} -

-
- - {schedule.jobHistory && schedule.jobHistory.length > 0 && ( -
- Recent runs -
- {schedule.jobHistory.slice(0, 5).map((run, index) => ( -
- - {formatScheduleInstant(run.timestamp)} - - {run.summary} -
- ))} -
-
- )} -
- ) -} - -interface EmbeddedScheduledTaskActionsProps { - workspaceId: string -} - -function EmbeddedScheduledTaskActions({ workspaceId }: EmbeddedScheduledTaskActionsProps) { - const router = useRouter() - - const handleOpenScheduledTasks = () => { - router.push(`/workspace/${workspaceId}/scheduled-tasks`) - } - - return ( - - - - - -

Open in scheduled tasks

-
-
- ) -} - interface EmbeddedLogProps { workspaceId: string logId: string diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry/resource-registry.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry/resource-registry.tsx index e1bbf9f9f36..5a4793713ad 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry/resource-registry.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry/resource-registry.tsx @@ -3,11 +3,11 @@ import type { ElementType, ReactNode } from 'react' import { cn } from '@sim/emcn' import { - Calendar, Connections, Database, File as FileIcon, Folder as FolderIcon, + Globe, Library, Table as TableIcon, Task, @@ -15,7 +15,6 @@ import { Workflow, } from '@sim/emcn/icons' import type { QueryClient } from '@tanstack/react-query' -import { Globe } from 'lucide-react' import { getDocumentIcon } from '@/components/icons/document-icons' import type { MothershipResource, @@ -24,7 +23,6 @@ import type { import { getBareIconStyle, type StyleableIcon } from '@/blocks/brand-icon-style' import { logKeys } from '@/hooks/queries/logs' import { mothershipChatKeys } from '@/hooks/queries/mothership-chats' -import { scheduleKeys } from '@/hooks/queries/schedules' import { folderKeys } from '@/hooks/queries/utils/folder-keys' import { invalidateWorkflowLists } from '@/hooks/queries/utils/invalidate-workflow-lists' import { knowledgeKeys } from '@/hooks/queries/utils/knowledge-keys' @@ -186,15 +184,6 @@ export const RESOURCE_REGISTRY: Record , }, - scheduledtask: { - type: 'scheduledtask', - label: 'Scheduled Tasks', - icon: Calendar, - renderTabIcon: (_resource, className) => ( - - ), - renderDropdownItem: (props) => , - }, log: { type: 'log', label: 'Logs', @@ -273,9 +262,6 @@ const RESOURCE_INVALIDATORS: Record< task: (qc, wId) => { qc.invalidateQueries({ queryKey: mothershipChatKeys.list(wId) }) }, - scheduledtask: (qc, wId) => { - qc.invalidateQueries({ queryKey: scheduleKeys.list(wId) }) - }, log: (qc, wId, id) => { qc.invalidateQueries({ queryKey: logKeys.details() }) qc.invalidateQueries({ queryKey: logKeys.detail(wId, id) }) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/queued-messages/queued-messages.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/queued-messages/queued-messages.tsx index ee45c1a1aa9..76979893694 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/queued-messages/queued-messages.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/queued-messages/queued-messages.tsx @@ -2,7 +2,7 @@ import { useCallback, useRef, useState } from 'react' import { cn, Tooltip } from '@sim/emcn' -import { ArrowUp, ChevronDown, ChevronRight, Paperclip, Pencil, Trash2, X } from 'lucide-react' +import { ArrowUp, ChevronDown, ChevronRight, Paperclip, Pencil, Trash, X } from '@sim/emcn/icons' import { UserMessageContent } from '@/app/workspace/[workspaceId]/home/components/user-message-content' import type { QueuedMessage } from '@/app/workspace/[workspaceId]/home/types' @@ -182,7 +182,7 @@ export function QueuedMessages({ }} className='rounded-md p-[5px] text-[var(--text-icon)] transition-colors hover-hover:bg-[var(--surface-active)] hover-hover:text-[var(--text-primary)]' > - + diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/suggested-actions/suggested-actions.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/suggested-actions/suggested-actions.tsx index c90e0795ee8..8a78cf03537 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/suggested-actions/suggested-actions.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/suggested-actions/suggested-actions.tsx @@ -1,8 +1,8 @@ 'use client' import { type ComponentType, type CSSProperties, useMemo, useState } from 'react' -import { ArrowRight, ChevronDown, chipVariants, cn, Expandable, ExpandableContent } from '@sim/emcn' -import { Shuffle, Table } from '@sim/emcn/icons' +import { ArrowRight, ChevronDown, cn, Expandable, ExpandableContent } from '@sim/emcn' +import { Table } from '@sim/emcn/icons' import { randomFloat } from '@sim/utils/random' import { stripVersionSuffix } from '@sim/utils/string' import { useParams } from 'next/navigation' @@ -149,7 +149,7 @@ function scoreCandidate(c: Candidate, signals: Signals): number { /** * Weighted sampling without replacement. Each pick's probability is - * proportional to its weight, so shuffles stay fresh while staying relevant. + * proportional to its weight, so the set stays varied while staying relevant. */ function weightedSample(pool: readonly T[], n: number, weightOf: (item: T) => number): T[] { const remaining = pool.map((item) => ({ item, weight: Math.max(weightOf(item), 0) })) @@ -277,8 +277,6 @@ export function SuggestedActions({ onSelectPrompt }: SuggestedActionsProps) { * above it. */ const [animationsEnabled, setAnimationsEnabled] = useState(false) - /** Incremented by the shuffle control to re-roll the weighted sample. */ - const [shuffleNonce, setShuffleNonce] = useState(0) /** * OAuth connect modal target. Setting this opens the modal; setting it back * to `null` (via `onOpenChange(false)`) closes it. Mirrors the local-state @@ -307,16 +305,16 @@ export function SuggestedActions({ onSelectPrompt }: SuggestedActionsProps) { ) /** - * Personalized suggestions, re-sampled whenever signals resolve or the user - * shuffles. Falls back to {@link INITIAL_ACTIONS} until the credential and - * service queries have loaded (and stays there for users with no - * connections, unless they shuffle), so first paint never flashes. + * Personalized suggestions, re-sampled whenever signals resolve. Falls back to + * {@link INITIAL_ACTIONS} until the credential and service queries have loaded + * — and stays there for users with no connections — so first paint never + * flashes. */ const actions = useMemo(() => { const personalized = services.length > 0 && connectedProviders.size > 0 - if (!personalized && shuffleNonce === 0) return INITIAL_ACTIONS + if (!personalized) return INITIAL_ACTIONS return computeActions(services, signals) - }, [connectedProviders, services, signals, shuffleNonce]) + }, [connectedProviders, services, signals]) const handleSelect = (action: Action, position: number) => { captureEvent(posthog, 'suggested_action_clicked', { @@ -335,14 +333,6 @@ export function SuggestedActions({ onSelectPrompt }: SuggestedActionsProps) { if (match) setOAuthTarget(match) } - const handleShuffle = () => { - captureEvent(posthog, 'suggested_actions_shuffled', { - workspace_id: workspaceId, - connected_provider_count: connectedProviders.size, - }) - setShuffleNonce((n) => n + 1) - } - const handleToggleExpanded = () => { captureEvent(posthog, 'suggested_actions_toggled', { workspace_id: workspaceId, @@ -353,41 +343,40 @@ export function SuggestedActions({ onSelectPrompt }: SuggestedActionsProps) { } return ( -
-
- - -
+ /> + - -
+ + {/* 6px, matching a sidebar section header to its first item — both headers + are an 18px box around 12px text, so equal padding reads as equal + distance. Padding an inner wrapper rather than the animated element: + `collapsible-up`/`-down` interpolate height alone, so a margin here + would hold its full value through the close and then vanish on unmount, + snapping the content below up. */} +
{actions.map((action, i) => { const Icon = action.icon return ( @@ -396,7 +385,7 @@ export function SuggestedActions({ onSelectPrompt }: SuggestedActionsProps) { type='button' onClick={() => handleSelect(action, i)} className={cn( - 'flex items-center gap-2 border-[var(--divider)] px-2 py-2 text-left transition-colors hover-hover:bg-[var(--surface-5)]', + 'flex items-center gap-2 border-[var(--border)] px-2 py-2 text-left transition-colors hover-hover:bg-[var(--surface-5)]', i > 0 && 'border-t' )} > diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/attached-files-list/attached-files-list.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/attached-files-list/attached-files-list.tsx index 1d917779162..f4e01792827 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/attached-files-list/attached-files-list.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/attached-files-list/attached-files-list.tsx @@ -2,7 +2,7 @@ import React from 'react' import { Loader, Tooltip } from '@sim/emcn' -import { X } from 'lucide-react' +import { X } from '@sim/emcn/icons' import { getDocumentIcon } from '@/components/icons/document-icons' import type { AttachedFile } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-file-attachments' diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/chip-clipboard-codec.ts b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/chip-clipboard-codec.ts index e2166acd6cc..73eb6eff658 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/chip-clipboard-codec.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/chip-clipboard-codec.ts @@ -27,7 +27,6 @@ const PORTABLE_KIND_TO_ID_FIELD = { file: 'fileId', folder: 'folderId', filefolder: 'fileFolderId', - scheduledtask: 'scheduleId', knowledge: 'knowledgeId', past_chat: 'chatId', workflow: 'workflowId', @@ -234,8 +233,6 @@ export function chipLinkToContext(link: ParsedChipLink): ChatContext { return { kind: 'folder', folderId: link.id, label: link.label } case 'filefolder': return { kind: 'filefolder', fileFolderId: link.id, label: link.label } - case 'scheduledtask': - return { kind: 'scheduledtask', scheduleId: link.id, label: link.label } case 'knowledge': return { kind: 'knowledge', knowledgeId: link.id, label: link.label } case 'past_chat': diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/constants.ts b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/constants.ts index 29e4baa4ea7..75c0b4da123 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/constants.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/constants.ts @@ -58,12 +58,12 @@ export interface PlusMenuHandle { * Box and typography shared by the textarea and its mirror overlay — both must * produce identical line wrapping so the overlay text sits exactly over the * (transparent) textarea text. The scale is the chat input's native prompt - * scale (`text-[15px]`, `-0.015em` tracking); the task modal's body inherits it + * scale (`text-[14px]`, `-0.015em` tracking); the task modal's body inherits it * so the editor reads the same whether it's the chat input or inside the modal. */ const FIELD_MIRROR_CLASSES = cn( 'm-0 box-border min-h-[24px] w-full break-words [overflow-wrap:anywhere] border-0 bg-transparent', - 'px-1 py-1 font-body text-[15px] leading-[24px] tracking-[-0.015em]' + 'px-1 py-1 font-body text-[14px] leading-[24px] tracking-[-0.015em]' ) /** @@ -75,7 +75,7 @@ export const TEXTAREA_BASE_CLASSES = cn( FIELD_MIRROR_CLASSES, 'block h-auto resize-none overflow-hidden', 'text-transparent caret-[var(--text-primary)] outline-none', - 'placeholder:font-[380] placeholder:text-[var(--text-subtle)]', + 'placeholder:text-[var(--text-muted)]', 'focus-visible:ring-0 focus-visible:ring-offset-0' ) @@ -128,7 +128,6 @@ const RESOURCE_TO_CONTEXT: Record< task: (r) => ({ kind: 'past_chat', chatId: r.id, label: r.title }), log: (r) => ({ kind: 'logs', executionId: r.id, label: r.title }), integration: (r) => ({ kind: 'integration', blockType: r.id, label: r.title }), - scheduledtask: (r) => ({ kind: 'scheduledtask', scheduleId: r.id, label: r.title }), generic: (r) => ({ kind: 'docs', label: r.title }), } diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/user-input.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/user-input.tsx index 1e1932332aa..810484c9d84 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/user-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/user-input.tsx @@ -530,8 +530,8 @@ const UserInputImpl = forwardRef(function UserI
(function UserI placeholder='Ask Sim to ' onSubmit={handleEnterSubmit} onArrowUpOnEmpty={handleArrowUpOnEmpty} - className={isInitialView ? 'max-h-[30vh]' : 'max-h-[200px]'} + className={isInitialView ? 'h-[56px]' : 'max-h-[200px]'} />
diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-message-content/user-message-content.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/user-message-content/user-message-content.tsx index b5bb7ace728..a52852d122e 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/user-message-content/user-message-content.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-message-content/user-message-content.tsx @@ -7,10 +7,10 @@ import type { ChatMessageContext } from '@/app/workspace/[workspaceId]/home/type import { getIntegrationMatcher } from '@/blocks/integration-matcher' const USER_MESSAGE_CLASSES = - 'whitespace-pre-wrap break-words [overflow-wrap:anywhere] font-[430] font-[family-name:var(--font-inter)] text-base text-[var(--text-primary)] leading-[23px] tracking-[0] antialiased' + 'whitespace-pre-wrap break-words [overflow-wrap:anywhere] font-[family-name:var(--font-inter)] text-base text-[var(--text-primary)] leading-[23px] tracking-[0] antialiased' const COMPACT_CLASSES = - 'truncate text-small leading-[20px] font-[430] font-[family-name:var(--font-inter)] text-[var(--text-primary)] tracking-[0] antialiased' + 'truncate text-small leading-[20px] font-[family-name:var(--font-inter)] text-[var(--text-primary)] tracking-[0] antialiased' interface UserMessageContentProps { content: string diff --git a/apps/sim/app/workspace/[workspaceId]/home/home.tsx b/apps/sim/app/workspace/[workspaceId]/home/home.tsx index ed9110a54ab..d27ffc2423f 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/home.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/home.tsx @@ -495,10 +495,10 @@ export function Home({ chatId, userName, userId, tableViewsEnabled }: HomeProps)
{/* Asymmetric padding biases the group up so the full cluster (heading + input + suggestions) sits at the optical center */}
-

+

What should we get done{firstName ? `, ${firstName}` : ''}?

-
+
= { superagent: 'Superagent', run: 'Run Agent', agent: 'Tools Agent', - scheduled_task: 'Scheduled Task Agent', // `job` retained as a backward-compat alias so historical transcripts still render a label. job: 'Job Agent', file: 'File Agent', diff --git a/apps/sim/app/workspace/[workspaceId]/integrations/[block]/integration-block-detail-fallback.tsx b/apps/sim/app/workspace/[workspaceId]/integrations/[block]/integration-block-detail-fallback.tsx index df7af0e6c8a..e0c67cf6f51 100644 --- a/apps/sim/app/workspace/[workspaceId]/integrations/[block]/integration-block-detail-fallback.tsx +++ b/apps/sim/app/workspace/[workspaceId]/integrations/[block]/integration-block-detail-fallback.tsx @@ -1,7 +1,7 @@ 'use client' import { ChipLink } from '@sim/emcn' -import { ArrowLeft } from 'lucide-react' +import { ArrowLeft } from '@sim/emcn/icons' import { PAGE_HEADER_BAR } from '@/components/page-header-bar' interface IntegrationBlockDetailFallbackProps { @@ -12,7 +12,7 @@ interface IntegrationBlockDetailFallbackProps { * Suspense fallback for the integration detail page — the back-link chrome * shown while {@link IntegrationBlockDetail} hydrates. * - * This MUST be a client component. The lucide `ArrowLeft` passed as `ChipLink`'s + * This MUST be a client component. The `ArrowLeft` icon passed as `ChipLink`'s * `leftIcon` is a function, and functions cannot cross the server→client * boundary as props. Rendering the fallback from the server `page.tsx` directly * threw a React Server Components error ("Functions cannot be passed directly to diff --git a/apps/sim/app/workspace/[workspaceId]/integrations/[block]/integration-block-detail.tsx b/apps/sim/app/workspace/[workspaceId]/integrations/[block]/integration-block-detail.tsx index b18dc4cafa9..dcb21376344 100644 --- a/apps/sim/app/workspace/[workspaceId]/integrations/[block]/integration-block-detail.tsx +++ b/apps/sim/app/workspace/[workspaceId]/integrations/[block]/integration-block-detail.tsx @@ -2,7 +2,7 @@ import { useEffect, useMemo, useRef, useState } from 'react' import { Chip, ChipDropdown, ChipLink, cn } from '@sim/emcn' -import { ArrowLeft, Plus } from 'lucide-react' +import { ArrowLeft, Plus } from '@sim/emcn/icons' import { useRouter } from 'next/navigation' import { useQueryState } from 'nuqs' import { HEADER_ACTION_CLUSTER, PAGE_HEADER_BAR } from '@/components/page-header-bar' diff --git a/apps/sim/app/workspace/[workspaceId]/integrations/[block]/integration-skills-section.tsx b/apps/sim/app/workspace/[workspaceId]/integrations/[block]/integration-skills-section.tsx index 575a448fe2f..13e3d9500d4 100644 --- a/apps/sim/app/workspace/[workspaceId]/integrations/[block]/integration-skills-section.tsx +++ b/apps/sim/app/workspace/[workspaceId]/integrations/[block]/integration-skills-section.tsx @@ -2,7 +2,7 @@ import { useMemo, useRef, useState } from 'react' import { Chip, toast } from '@sim/emcn' -import { Check, Plus } from 'lucide-react' +import { Check, Plus } from '@sim/emcn/icons' import { usePostHog } from 'posthog-js/react' import { captureEvent } from '@/lib/posthog/client' import { SkillTile } from '@/app/workspace/[workspaceId]/components' @@ -38,11 +38,11 @@ function SkillRow({ skill, added, pending, disabled, onAdd }: SkillRowProps) { description={skill.description} trailing={ added ? ( - + Added ) : ( - + {pending ? 'Adding...' : 'Add'} ) diff --git a/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-slack-bot-modal/connect-slack-bot-modal.tsx b/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-slack-bot-modal/connect-slack-bot-modal.tsx index 7526064411e..16c9bed5a36 100644 --- a/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-slack-bot-modal/connect-slack-bot-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-slack-bot-modal/connect-slack-bot-modal.tsx @@ -12,10 +12,10 @@ import { SecretInput, Wizard, } from '@sim/emcn' +import { Loader } from '@sim/emcn/icons' import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' -import { Loader2 } from 'lucide-react' import { SlackIcon } from '@/components/icons' import { getBaseUrl } from '@/lib/core/utils/urls' import { SLACK_CUSTOM_BOT_PROVIDER_ID } from '@/lib/oauth/types' @@ -431,7 +431,7 @@ function StepDone({ pending, created, error, onRetry }: StepDoneProps) { if (pending) { return (
- +

Verifying your bot and connecting…

) diff --git a/apps/sim/app/workspace/[workspaceId]/integrations/components/integration-tabs-header/index.ts b/apps/sim/app/workspace/[workspaceId]/integrations/components/integration-tabs-header/index.ts deleted file mode 100644 index 95e6eb0ca45..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/integrations/components/integration-tabs-header/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { IntegrationTabsHeader } from '@/app/workspace/[workspaceId]/integrations/components/integration-tabs-header/integration-tabs-header' diff --git a/apps/sim/app/workspace/[workspaceId]/integrations/components/showcase-with-explore/showcase-with-explore.tsx b/apps/sim/app/workspace/[workspaceId]/integrations/components/showcase-with-explore/showcase-with-explore.tsx index f5363986d1f..c7809c934f0 100644 --- a/apps/sim/app/workspace/[workspaceId]/integrations/components/showcase-with-explore/showcase-with-explore.tsx +++ b/apps/sim/app/workspace/[workspaceId]/integrations/components/showcase-with-explore/showcase-with-explore.tsx @@ -38,7 +38,7 @@ export function ShowcaseWithExplore({ prompt }: ShowcaseWithExploreProps) { storeCuratedPrompt(prompt) router.push(`/workspace/${workspaceId}/home`) }} - className='absolute right-0 bottom-0 mx-0' + className='absolute right-0 bottom-0' > Explore in chat
diff --git a/apps/sim/app/workspace/[workspaceId]/integrations/integrations.tsx b/apps/sim/app/workspace/[workspaceId]/integrations/integrations.tsx index c9a8aa539f4..df20fde691b 100644 --- a/apps/sim/app/workspace/[workspaceId]/integrations/integrations.tsx +++ b/apps/sim/app/workspace/[workspaceId]/integrations/integrations.tsx @@ -20,8 +20,8 @@ import { type Integration, resolveCredentialDisplay, } from '@/lib/integrations' +import { IntegrationTabsHeader } from '@/app/workspace/[workspaceId]/components' import { IntegrationSection } from '@/app/workspace/[workspaceId]/integrations/components/integration-section' -import { IntegrationTabsHeader } from '@/app/workspace/[workspaceId]/integrations/components/integration-tabs-header' import { IntegrationTile } from '@/app/workspace/[workspaceId]/integrations/components/integrations-showcase' import { ShowcaseWithExplore } from '@/app/workspace/[workspaceId]/integrations/components/showcase-with-explore' import { useScrollRestoration } from '@/app/workspace/[workspaceId]/integrations/hooks/use-scroll-restoration' @@ -301,7 +301,7 @@ export function Integrations() { /> -
setShowConnectorsModal(true)} - className={cn(chipVariants({ variant: 'filled', flush: true }), 'max-w-[180px]')} + className={cn(chipVariants({ variant: 'filled' }), 'max-w-[180px]')} > {connector.status === 'syncing' ? ( @@ -1525,7 +1523,6 @@ function TagFilterValueControl({ entry, onChange }: TagFilterValueControlProps) onChange={(value) => onChange({ value })} placeholder='From' fullWidth - flush /> to onChange({ valueTo: value })} placeholder='To' fullWidth - flush />
) @@ -1545,7 +1541,6 @@ function TagFilterValueControl({ entry, onChange }: TagFilterValueControlProps) onChange={(value) => onChange({ value })} placeholder='Select date' fullWidth - flush /> ) } @@ -1693,7 +1688,6 @@ function TagFilterSection({ tagDefinitions, entries, onChange }: TagFilterSectio matchTriggerWidth={false} contentClassName='max-h-[240px] overflow-y-auto' className='max-w-[150px]' - flush /> {entry.tagSlot && ( )}
diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/action-bar/action-bar.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/action-bar/action-bar.tsx index 052cd60622b..e6817dce63c 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/action-bar/action-bar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/action-bar/action-bar.tsx @@ -1,6 +1,6 @@ -import { Button, cn, Tooltip, Trash2 } from '@sim/emcn' +import { Button, cn, Tooltip, Trash } from '@sim/emcn' +import { Ban, Circle } from '@sim/emcn/icons' import { domAnimation, LazyMotion, m } from 'framer-motion' -import { Circle, CircleOff } from 'lucide-react' import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' interface ActionBarProps { @@ -109,7 +109,7 @@ export function ActionBar({ disabled={isLoading} className='hover-hover:!text-[var(--text-inverse)] size-[28px] rounded-lg bg-[var(--surface-5)] p-0 text-[var(--text-secondary)] hover-hover:bg-[var(--brand-secondary)]' > - + Disable @@ -125,7 +125,7 @@ export function ActionBar({ disabled={isLoading} className='hover-hover:!text-[var(--text-inverse)] size-[28px] rounded-lg bg-[var(--surface-5)] p-0 text-[var(--text-secondary)] hover-hover:bg-[var(--brand-secondary)]' > - + Delete diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/add-connector-modal.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/add-connector-modal.tsx index 661bc325188..529ac4567aa 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/add-connector-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/add-connector-modal.tsx @@ -20,7 +20,7 @@ import { handleKeyboardActivation, Search, } from '@sim/emcn' -import { ArrowLeft, Plus } from 'lucide-react' +import { ArrowLeft, Plus } from '@sim/emcn/icons' import { useParams } from 'next/navigation' import { consumeOAuthReturnContext } from '@/lib/credentials/client-state' import { diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-documents-modal/add-documents-modal.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-documents-modal/add-documents-modal.tsx index cfdc62d32af..98c33ed9754 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-documents-modal/add-documents-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-documents-modal/add-documents-modal.tsx @@ -12,8 +12,8 @@ import { cn, Loader, } from '@sim/emcn' +import { RefreshCw, X } from '@sim/emcn/icons' import { createLogger } from '@sim/logger' -import { RotateCcw, X } from 'lucide-react' import { useParams } from 'next/navigation' import { formatFileSize, validateKnowledgeBaseFile } from '@/lib/uploads/utils/file-utils' import { ACCEPT_ATTRIBUTE } from '@/lib/uploads/utils/validation' @@ -202,7 +202,7 @@ export function AddDocumentsModal({ onClick={() => handleRetryFile(index)} disabled={isUploading} > - + )} {field.description} diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.tsx index b7e7b4b6145..6368f79313c 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.tsx @@ -14,20 +14,20 @@ import { Loader, Tooltip, } from '@sim/emcn' -import { createLogger } from '@sim/logger' -import { format, formatDistanceToNow, isPast } from 'date-fns' import { - AlertCircle, - AlertTriangle, - CheckCircle2, ChevronDown, + CircleAlert, + CircleCheck, + CircleX, Pause, Play, RefreshCw, Settings, Trash, - XCircle, -} from 'lucide-react' + TriangleAlert, +} from '@sim/emcn/icons' +import { createLogger } from '@sim/logger' +import { format, formatDistanceToNow, isPast } from 'date-fns' import { consumeOAuthReturnContext, writeOAuthReturnContext } from '@/lib/credentials/client-state' import { getCanonicalScopesForProvider, getProviderIdFromServiceId } from '@/lib/oauth' import { getMissingRequiredScopes } from '@/lib/oauth/utils' @@ -378,7 +378,7 @@ function ConnectorCard({ )}
{connector.status === 'disabled' && ( - + )}
@@ -417,7 +417,7 @@ function ConnectorCard({ {connector.lastSyncError && ( - + {connector.lastSyncError} @@ -557,7 +557,7 @@ function ConnectorCard({
- + Connector disabled after repeated sync failures

@@ -710,9 +710,9 @@ function SyncHistory({ logs, isLoading }: SyncHistoryProps) { {isRunning ? ( ) : isError ? ( - + ) : ( - + )}

diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/edit-connector-modal.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/edit-connector-modal.tsx index b916a31b232..5efe253f401 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/edit-connector-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/edit-connector-modal.tsx @@ -15,8 +15,8 @@ import { Skeleton, Tooltip, } from '@sim/emcn' +import { RefreshCw, SquareArrowUpRight } from '@sim/emcn/icons' import { createLogger } from '@sim/logger' -import { ExternalLink, RotateCcw } from 'lucide-react' import { ConnectorConfigFields } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/connector-config-fields' import { hasWorkspaceMaxConnectorAccess } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/connector-entitlements' import { SYNC_INTERVALS } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/consts' @@ -459,7 +459,7 @@ function DocumentsTab({ knowledgeBaseId, connectorId }: DocumentsTabProps) { rel='noopener noreferrer' className='flex size-5 flex-shrink-0 items-center justify-center rounded-md text-[var(--text-icon)] transition-colors hover-hover:bg-[var(--surface-5)] hover-hover:text-[var(--text-primary)]' > - + Open source document @@ -479,7 +479,7 @@ function DocumentsTab({ knowledgeBaseId, connectorId }: DocumentsTabProps) { > {doc.userExcluded ? ( <> - + Restore ) : ( diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/components/create-base-modal/create-base-modal.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/components/create-base-modal/create-base-modal.tsx index 5364d754044..5d118422350 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/components/create-base-modal/create-base-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/components/create-base-modal/create-base-modal.tsx @@ -19,9 +19,9 @@ import { Loader, toast, } from '@sim/emcn' +import { X } from '@sim/emcn/icons' import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' -import { X } from 'lucide-react' import { useParams } from 'next/navigation' import { type FieldErrors, useForm } from 'react-hook-form' import { z } from 'zod' diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx index 71ed2259ca9..0ec36c650dd 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx @@ -1019,7 +1019,6 @@ export function Knowledge() { onChange={(value) => setConnectorFilter(value === 'all' ? [] : [value])} align='start' fullWidth - flush />
@@ -1041,7 +1040,6 @@ export function Knowledge() { onChange={(value) => setContentFilter(value === 'all' ? [] : [value])} align='start' fullWidth - flush />
{memberOptions.length > 0 && ( @@ -1068,7 +1066,6 @@ export function Knowledge() { searchPlaceholder='Search members...' align='start' fullWidth - flush />
)} diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/execution-snapshot/execution-snapshot.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/execution-snapshot/execution-snapshot.tsx index c1eb4880f7a..ac95638adc7 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/execution-snapshot/execution-snapshot.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/execution-snapshot/execution-snapshot.tsx @@ -16,7 +16,7 @@ import { ModalDescription, ModalHeader, } from '@sim/emcn' -import { AlertCircle } from 'lucide-react' +import { CircleAlert } from '@sim/emcn/icons' import { createPortal } from 'react-dom' import { Preview } from '@/app/workspace/[workspaceId]/w/components/preview' import { useExecutionSnapshot } from '@/hooks/queries/logs' @@ -109,7 +109,7 @@ export function ExecutionSnapshot({ style={{ height, width }} >
- + Failed to load run snapshot: {error.message}
@@ -137,7 +137,7 @@ export function ExecutionSnapshot({ style={{ height, width }} >
- + Logged State Not Found
diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view/trace-view.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view/trace-view.tsx index 6315cc4e304..01d725a9719 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view/trace-view.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view/trace-view.tsx @@ -19,7 +19,6 @@ import { Tooltip, useCopyToClipboard, } from '@sim/emcn' -import { formatDuration } from '@sim/utils/formatting' import { ArrowDown, ArrowUp, @@ -29,7 +28,8 @@ import { Clipboard, Search, X, -} from 'lucide-react' +} from '@sim/emcn/icons' +import { formatDuration } from '@sim/utils/formatting' import { createPortal } from 'react-dom' import type { TraceSpan } from '@/lib/logs/types' import { diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx index 5a353e7c0f4..bde89f51df8 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx @@ -22,9 +22,18 @@ import { Tooltip, useCopyToClipboard, } from '@sim/emcn' -import { Workflow, Wrench } from '@sim/emcn/icons' +import { + ArrowDown, + ArrowUp, + Check, + ChevronUp, + Clipboard, + Search, + Workflow, + Wrench, + X, +} from '@sim/emcn/icons' import { formatDuration } from '@sim/utils/formatting' -import { ArrowDown, ArrowUp, Check, ChevronUp, Clipboard, Search, X } from 'lucide-react' import { useParams, useRouter } from 'next/navigation' import { useQueryState } from 'nuqs' import { createPortal } from 'react-dom' @@ -543,7 +552,7 @@ export function LogDetailsContent({ log, onActiveTabChange }: LogDetailsContentP Snapshot - setIsExecutionSnapshotOpen(true)}> + setIsExecutionSnapshotOpen(true)}> View Snapshot
@@ -555,7 +564,7 @@ export function LogDetailsContent({ log, onActiveTabChange }: LogDetailsContentP Troubleshoot - + Troubleshoot in Chat
diff --git a/apps/sim/app/workspace/[workspaceId]/not-found.tsx b/apps/sim/app/workspace/[workspaceId]/not-found.tsx index db69e38864d..6a29b6c5a26 100644 --- a/apps/sim/app/workspace/[workspaceId]/not-found.tsx +++ b/apps/sim/app/workspace/[workspaceId]/not-found.tsx @@ -1,8 +1,7 @@ 'use client' import { Button, buttonVariants } from '@sim/emcn' -import { ArrowLeft, Home } from '@sim/emcn/icons' -import { Compass } from 'lucide-react' +import { ArrowLeft, Compass, Home } from '@sim/emcn/icons' import Link from 'next/link' import { useParams, useRouter } from 'next/navigation' import { ErrorShell } from '@/app/workspace/[workspaceId]/components' @@ -16,7 +15,7 @@ export default function WorkspaceNotFound() { } + icon={} > diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/billing/billing.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/billing/billing.tsx index 0505c9fb8f1..c56187b7678 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/billing/billing.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/billing/billing.tsx @@ -481,14 +481,13 @@ export function Billing({ {explorePlansLabel} ) : ( - + {explorePlansLabel} ))} @@ -580,7 +579,6 @@ export function Billing({
Payment method @@ -596,7 +594,6 @@ export function Billing({ {isCancelledAtPeriodEnd ? ( @@ -605,7 +602,6 @@ export function Billing({ ) : ( diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/browser/components/password-detail/password-detail.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/browser/components/password-detail/password-detail.tsx index bc394bbb0d7..4ebca111de6 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/browser/components/password-detail/password-detail.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/browser/components/password-detail/password-detail.tsx @@ -175,7 +175,7 @@ export function PasswordDetail({ credential, onBack, onForgotten }: PasswordDeta @@ -478,7 +478,7 @@ export function General() { aria-label='About canvas error notifications' className='inline-flex cursor-default text-[var(--text-muted)]' > - + diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/inbox/components/inbox-settings-tab/inbox-settings-tab.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/inbox/components/inbox-settings-tab/inbox-settings-tab.tsx index b9491208fe9..78ebd4b69d9 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/inbox/components/inbox-settings-tab/inbox-settings-tab.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/inbox/components/inbox-settings-tab/inbox-settings-tab.tsx @@ -16,8 +16,8 @@ import { Tooltip, useCopyToClipboard, } from '@sim/emcn' +import { Check, Clipboard, Pencil, Plus, Trash } from '@sim/emcn/icons' import { getErrorMessage } from '@sim/utils/errors' -import { Check, Clipboard, Pencil, Plus, Trash2 } from 'lucide-react' import { useParams } from 'next/navigation' import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section' import { @@ -207,8 +207,7 @@ export function InboxSettingsTab() { )}
handleRemoveSender(sender.id)} /> diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/inbox/components/inbox-task-list/inbox-task-list.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/inbox/components/inbox-task-list/inbox-task-list.tsx index e68485d5739..e31351b4169 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/inbox/components/inbox-task-list/inbox-task-list.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/inbox/components/inbox-task-list/inbox-task-list.tsx @@ -2,9 +2,8 @@ import { useCallback, useMemo } from 'react' import { Badge, ChipInput, ChipSelect, Search } from '@sim/emcn' -import { ArrowRight } from '@sim/emcn/icons' +import { ArrowRight, Paperclip } from '@sim/emcn/icons' import { formatRelativeTime } from '@sim/utils/formatting' -import { Paperclip } from 'lucide-react' import { useParams, useRouter } from 'next/navigation' import { useQueryStates } from 'nuqs' import { diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/mcp/components/mcp-server-form-modal/mcp-server-form-modal.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/mcp/components/mcp-server-form-modal/mcp-server-form-modal.tsx index f3afefe0820..1829b8da4d2 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/mcp/components/mcp-server-form-modal/mcp-server-form-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/mcp/components/mcp-server-form-modal/mcp-server-form-modal.tsx @@ -14,9 +14,9 @@ import { cn, SecretInput, } from '@sim/emcn' +import { ChevronDown, ChevronRight } from '@sim/emcn/icons' import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' -import { ChevronDown, ChevronRight } from 'lucide-react' import type { McpAuthType, McpTransport } from '@/lib/mcp/types' import { checkEnvVarTrigger, diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/mcp/mcp.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/mcp/mcp.tsx index 04158338a79..e1b39bf0089 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/mcp/mcp.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/mcp/mcp.tsx @@ -2,10 +2,9 @@ import { useEffect, useRef, useState } from 'react' import { Badge, Button, Chip, ChipConfirmModal, cn, Tooltip, toast } from '@sim/emcn' -import { ArrowLeft } from '@sim/emcn/icons' +import { ArrowLeft, ChevronDown, Plus } from '@sim/emcn/icons' import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' -import { ChevronDown, Plus } from 'lucide-react' import { useParams } from 'next/navigation' import { useQueryState } from 'nuqs' import { McpIcon } from '@/components/icons' diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/row-actions-menu/row-actions-menu.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/row-actions-menu/row-actions-menu.tsx index f082654c8c7..5c3d29bb494 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/row-actions-menu/row-actions-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/row-actions-menu/row-actions-menu.tsx @@ -39,11 +39,7 @@ export function RowActionsMenu({ label, actions, triggerClassName }: RowActionsM return ( - diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/settings-resource-row/settings-resource-row.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/settings-resource-row/settings-resource-row.tsx index 01f354c27b2..eb2c57ce186 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/settings-resource-row/settings-resource-row.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/settings-resource-row/settings-resource-row.tsx @@ -81,8 +81,7 @@ interface SettingsResourceRowProps { /** * Appends the canonical navigation chevron after `trailing`. Set it on rows that * open a detail page; leave it off for rows whose `onClick` performs an action - * in place. The row owns the glyph so callers never pick an arrow themselves — - * `lucide-react` and `@sim/emcn/icons` ship visibly different ones. + * in place. The row owns the glyph so callers never pick an arrow themselves. */ navigable?: boolean /** diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/team-management/components/team-seats-overview/team-seats-overview.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/team-management/components/team-seats-overview/team-seats-overview.tsx index 63720940891..8df81724bb1 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/team-management/components/team-seats-overview/team-seats-overview.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/team-management/components/team-seats-overview/team-seats-overview.tsx @@ -46,7 +46,7 @@ export function TeamSeatsOverview({
{billingHref && ( - + View plans )} diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/teammates/teammates.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/teammates/teammates.tsx index 96d6d722af7..78f00902510 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/teammates/teammates.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/teammates/teammates.tsx @@ -36,7 +36,7 @@ import { useWorkspacePermissionsQuery, useWorkspacesQuery, } from '@/hooks/queries/workspace' -import { usePermissionConfig } from '@/hooks/use-permission-config' +import { useWorkspaceInvitePolicy } from '@/hooks/use-workspace-invite-policy' const ROLE_OPTIONS = [ { value: 'read', label: 'Read' }, @@ -90,7 +90,7 @@ export function Teammates() { const router = useRouter() const queryClient = useQueryClient() - const { isInvitationsDisabled: isInvitationsDisabledByConfig } = usePermissionConfig() + const { inviteDisabledReason, isInvitationsDisabled } = useWorkspaceInvitePolicy(workspaceId) const resendInvitation = useResendWorkspaceInvitation() const cancelInvitation = useCancelWorkspaceInvitation() @@ -104,8 +104,6 @@ export function Teammates() { }) const activeWorkspace = workspaces?.find((workspace) => workspace.id === workspaceId) - const inviteDisabledReason = activeWorkspace?.inviteDisabledReason ?? null - const isInvitationsDisabled = isInvitationsDisabledByConfig || inviteDisabledReason !== null const upgradeHref = buildUpgradeHref(workspaceId, 'seats') diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/workflow-mcp-servers/workflow-mcp-servers.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/workflow-mcp-servers/workflow-mcp-servers.tsx index 53e4a100ec0..6364e07a6e4 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/workflow-mcp-servers/workflow-mcp-servers.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/workflow-mcp-servers/workflow-mcp-servers.tsx @@ -21,10 +21,9 @@ import { Label, useCopyToClipboard, } from '@sim/emcn' -import { ArrowLeft } from '@sim/emcn/icons' +import { ArrowLeft, Check, Clipboard, Plus, Server } from '@sim/emcn/icons' import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' -import { Check, Clipboard, Plus, Server } from 'lucide-react' import { useParams } from 'next/navigation' import { useQueryState } from 'nuqs' import { McpIcon } from '@/components/icons' @@ -597,7 +596,7 @@ function ServerDetailView({ {activeConfigTab === 'cursor' && ( onChange(localPartsToDateValue(day, parts.time, timeZone))} placeholder='Select date' - flush className='flex-1' />
diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-find.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-find.tsx index d8d176406a8..58b9220bdcb 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-find.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-find.tsx @@ -2,8 +2,7 @@ import type React from 'react' import { Button, ChipInput } from '@sim/emcn' -import { Loader, X } from '@sim/emcn/icons' -import { ChevronDown, ChevronUp } from 'lucide-react' +import { ChevronDown, ChevronUp, Loader, X } from '@sim/emcn/icons' export interface TableFindProps { query: string diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/workflow-sidebar/workflow-sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/workflow-sidebar/workflow-sidebar.tsx index d5d67dc2a83..8468c202805 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/workflow-sidebar/workflow-sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/workflow-sidebar/workflow-sidebar.tsx @@ -18,11 +18,10 @@ import { Tooltip, toast, } from '@sim/emcn' -import { ArrowLeft, ChevronDown, X } from '@sim/emcn/icons' +import { ArrowLeft, ChevronDown, Repeat, Split, SquareArrowUpRight, X } from '@sim/emcn/icons' import { toError } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' import { useMutation, useQueryClient } from '@tanstack/react-query' -import { ExternalLink, RepeatIcon, SplitIcon } from 'lucide-react' import { findValidationIssue, isValidationError } from '@/lib/api/client/errors' import { requestJson } from '@/lib/api/client/request' import type { @@ -475,8 +474,8 @@ export function WorkflowSidebarBody({ .charAt(0) .toUpperCase() if (blockConfig?.icon) blockIcon = blockConfig.icon - else if (f.blockType === 'loop') blockIcon = RepeatIcon - else if (f.blockType === 'parallel') blockIcon = SplitIcon + else if (f.blockType === 'loop') blockIcon = Repeat + else if (f.blockType === 'parallel') blockIcon = Split group = { blockId: f.blockId, blockName: f.blockName, @@ -902,7 +901,7 @@ export function WorkflowSidebarBody({ } className='absolute right-[6px] bottom-1.5 z-10 size-[24px] cursor-pointer border border-[var(--border)] bg-[var(--surface-2)] p-0 hover-hover:bg-[var(--surface-4)]' > - + Open workflow diff --git a/apps/sim/app/workspace/[workspaceId]/upgrade/components/comparison-table/comparison-table.tsx b/apps/sim/app/workspace/[workspaceId]/upgrade/components/comparison-table/comparison-table.tsx index 90e060302f0..261d903b1b1 100644 --- a/apps/sim/app/workspace/[workspaceId]/upgrade/components/comparison-table/comparison-table.tsx +++ b/apps/sim/app/workspace/[workspaceId]/upgrade/components/comparison-table/comparison-table.tsx @@ -176,7 +176,7 @@ export function ComparisonTable({ disabled={cta.disabled} aria-label={`${cta.label} — ${col.name}`} className={cn( - chipVariants({ variant: cta.variant, fullWidth: true, flush: true }), + chipVariants({ variant: cta.variant, fullWidth: true }), 'mt-2 w-full justify-center' )} > diff --git a/apps/sim/app/workspace/[workspaceId]/upgrade/components/plan-card/plan-card.tsx b/apps/sim/app/workspace/[workspaceId]/upgrade/components/plan-card/plan-card.tsx index a8b6cbeae41..8df810bd590 100644 --- a/apps/sim/app/workspace/[workspaceId]/upgrade/components/plan-card/plan-card.tsx +++ b/apps/sim/app/workspace/[workspaceId]/upgrade/components/plan-card/plan-card.tsx @@ -100,7 +100,6 @@ export function UpgradePlanCard({ chipVariants({ variant: highlighted ? 'primary' : 'border-shadow', fullWidth: true, - flush: true, }), 'w-full justify-center' )} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/action-bar/action-bar.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/action-bar/action-bar.tsx index 08bde247dbc..979d96bfcf9 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/action-bar/action-bar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/action-bar/action-bar.tsx @@ -1,6 +1,6 @@ import { memo, useCallback } from 'react' -import { Button, cn, Duplicate, PlayOutline, Tooltip, Trash2, toast } from '@sim/emcn' -import { ArrowLeftRight, ArrowUpDown, Circle, CircleOff, Lock, LogOut, Unlock } from 'lucide-react' +import { Button, cn, Duplicate, PlayOutline, Tooltip, Trash, toast } from '@sim/emcn' +import { ArrowLeftRight, ArrowUpDown, Ban, Circle, Lock, LogOut, Unlock } from '@sim/emcn/icons' import { useShallow } from 'zustand/react/shallow' import { isInputDefinitionTrigger } from '@/lib/workflows/triggers/input-definition-triggers' import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' @@ -210,7 +210,7 @@ export const ActionBar = memo( disabled || isLocked || isParentLocked || (!isEnabled && isParentDisabled) } > - {isEnabled ? : } + {isEnabled ? : } @@ -345,7 +345,7 @@ export const ActionBar = memo( className={ACTION_BUTTON_STYLES} disabled={disabled || isLocked || isParentLocked} > - + diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.test.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.test.tsx index 682e8ddc52b..9469d62fd28 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.test.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.test.tsx @@ -103,12 +103,9 @@ vi.mock('@sim/emcn', () => ({ })) vi.mock('@sim/emcn/icons', () => ({ - Download: () => , -})) - -vi.mock('lucide-react', () => ({ - AlertCircle: () => , ArrowUp: () => , + CircleAlert: () => , + Download: () => , MoreVertical: () => , Paperclip: () => , Square: () => , diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx index b002bc41896..ecaba8b177c 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx @@ -14,10 +14,9 @@ import { Tooltip, Trash, } from '@sim/emcn' -import { Download } from '@sim/emcn/icons' +import { ArrowUp, CircleAlert, Download, MoreVertical, Paperclip, Square, X } from '@sim/emcn/icons' import { createLogger } from '@sim/logger' import { generateId } from '@sim/utils/id' -import { AlertCircle, ArrowUp, MoreVertical, Paperclip, Square, X } from 'lucide-react' import { useShallow } from 'zustand/react/shallow' import { useSession } from '@/lib/auth/auth-client' import { @@ -957,7 +956,7 @@ export function Chat() { className='!p-1.5 -m-1.5' onClick={(e) => e.stopPropagation()} > - +
- +
File upload error @@ -1122,10 +1121,7 @@ export function Chat() { : 'bg-[#808080] dark:bg-[#808080]' )} > - + )}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/chat-message/chat-message.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/chat-message/chat-message.tsx index 7ac8d75415f..aa7f25a3ef8 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/chat-message/chat-message.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/chat-message/chat-message.tsx @@ -91,7 +91,7 @@ export function ChatMessage({ message }: ChatMessageProps) { return (
-
+
{message.isStreaming && }
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/output-select/output-select.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/output-select/output-select.tsx index c234898474c..c4400aff539 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/output-select/output-select.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/output-select/output-select.tsx @@ -3,7 +3,7 @@ import type React from 'react' import { useMemo } from 'react' import { ChipCombobox, Combobox, type ComboboxOptionGroup, cn } from '@sim/emcn' -import { RepeatIcon, SplitIcon } from 'lucide-react' +import { Repeat, Split } from '@sim/emcn/icons' import { useShallow } from 'zustand/react/shallow' import { type FlattenOutputsBlockInput, @@ -271,9 +271,9 @@ export function OutputSelect({ if (blockConfig?.icon) { blockIcon = blockConfig.icon } else if (firstOutput.blockType === 'loop') { - blockIcon = RepeatIcon + blockIcon = Repeat } else if (firstOutput.blockType === 'parallel') { - blockIcon = SplitIcon + blockIcon = Split } return { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/command-list/command-list.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/command-list/command-list.tsx index cac027c4a73..9d79ae7bfa8 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/command-list/command-list.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/command-list/command-list.tsx @@ -2,8 +2,8 @@ import { useCallback } from 'react' import { Button, cn, handleKeyboardActivation, Library } from '@sim/emcn' +import { Search } from '@sim/emcn/icons' import { createLogger } from '@sim/logger' -import { Search } from 'lucide-react' import Image from 'next/image' import { useParams, useRouter } from 'next/navigation' import { AgentIcon } from '@/components/icons' @@ -18,7 +18,7 @@ const logger = createLogger('WorkflowCommandList') interface CommandItem { /** Display label for the command */ label: string - /** Icon component from lucide-react */ + /** Icon component rendered beside the label */ icon: React.ComponentType<{ className?: string }> /** Keyboard shortcut keys (can be single or array for multiple keys) */ shortcut: string | string[] diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/error/index.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/error/index.tsx index c236ee2e5b6..b5d28d9de17 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/error/index.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/error/index.tsx @@ -2,8 +2,8 @@ import { Component, type ReactNode, useEffect } from 'react' import { Button } from '@sim/emcn' +import { RefreshCw } from '@sim/emcn/icons' import { createLogger } from '@sim/logger' -import { RefreshCw } from 'lucide-react' import { ReactFlowProvider } from 'reactflow' import { Panel } from '@/app/workspace/[workspaceId]/w/[workflowId]/components' import { usePreventZoom } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/api/api.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/api/api.tsx index c468acb2a3d..04b6ce60157 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/api/api.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/api/api.tsx @@ -11,7 +11,7 @@ import { Skeleton, Tooltip, } from '@sim/emcn' -import { Check, Clipboard } from 'lucide-react' +import { Check, Clipboard } from '@sim/emcn/icons' import { AGENT_STREAM_PROTOCOL_HEADER_LABEL, AGENT_STREAM_PROTOCOL_V1, diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat.tsx index 33828be3b4b..47fe58efc96 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat.tsx @@ -16,9 +16,9 @@ import { Textarea, Tooltip, } from '@sim/emcn' +import { Check, TriangleAlert } from '@sim/emcn/icons' import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' -import { AlertTriangle, Check } from 'lucide-react' import { GeneratedPasswordInput } from '@/components/ui' import { isSsoEnabled } from '@/lib/core/config/env-flags' import { getBaseUrl, getEmailDomain } from '@/lib/core/utils/urls' @@ -341,7 +341,7 @@ export function ChatDeploy({ > {errors.general && (
- + {errors.general}
)} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/components/versions.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/components/versions.tsx index 868bca830f8..305d3c708fc 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/components/versions.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/components/versions.tsx @@ -12,8 +12,8 @@ import { Skeleton, Tooltip, } from '@sim/emcn' +import { FileText, MoreVertical, Pencil, RefreshCw, SendToBack } from '@sim/emcn/icons' import { formatDateTime } from '@sim/utils/formatting' -import { FileText, MoreVertical, Pencil, RotateCcw, SendToBack } from 'lucide-react' import type { WorkflowDeploymentVersionResponse } from '@/lib/workflows/persistence/utils' import { formatVersionLabel } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/format-version-label' import { useUpdateDeploymentVersion } from '@/hooks/queries/deployments' @@ -374,7 +374,7 @@ export function Versions({ {!v.isActive && ( handlePromote(v.version)}> - + Promote to live )} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/connection-blocks/components/field-item/field-item.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/connection-blocks/components/field-item/field-item.tsx index 916c284eb9b..a88b1726e19 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/connection-blocks/components/field-item/field-item.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/connection-blocks/components/field-item/field-item.tsx @@ -2,9 +2,9 @@ import { useCallback } from 'react' import { Badge, handleKeyboardActivation } from '@sim/emcn' +import { ChevronDown } from '@sim/emcn/icons' import { createLogger } from '@sim/logger' import clsx from 'clsx' -import { ChevronDown } from 'lucide-react' import type { ConnectedBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/hooks/use-block-connections' import { normalizeName } from '@/executor/constants' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/connection-blocks/connection-blocks.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/connection-blocks/connection-blocks.tsx index d28620e2a38..0e8688c8899 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/connection-blocks/connection-blocks.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/connection-blocks/connection-blocks.tsx @@ -2,9 +2,9 @@ import { useCallback, useRef, useState } from 'react' import { ChevronDown, handleKeyboardActivation } from '@sim/emcn' +import { Repeat, Split } from '@sim/emcn/icons' import { createLogger } from '@sim/logger' import clsx from 'clsx' -import { RepeatIcon, SplitIcon } from 'lucide-react' import { useShallow } from 'zustand/react/shallow' import { FieldItem, @@ -118,10 +118,10 @@ function ConnectionItem({ if (!blockConfig) { if (connection.type === 'loop') { - Icon = RepeatIcon as typeof Icon + Icon = Repeat bgColor = '#2FB3FF' } else if (connection.type === 'parallel') { - Icon = SplitIcon as typeof Icon + Icon = Split bgColor = '#FEE12B' } } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/checkbox-list/checkbox-list.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/checkbox-list/checkbox-list.tsx index 992aeeb0164..403827b6660 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/checkbox-list/checkbox-list.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/checkbox-list/checkbox-list.tsx @@ -1,5 +1,5 @@ import { Checkbox, Label, Tooltip } from '@sim/emcn' -import { Info } from 'lucide-react' +import { CircleInfo } from '@sim/emcn/icons' import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text' import { getWorkflowSearchLabelHighlight } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workflow-search-highlight' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' @@ -85,7 +85,7 @@ function CheckboxItem({ {option.description && ( - +

{option.description}

diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/code/code.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/code/code.tsx index a0d18f4d63b..8cfef78abef 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/code/code.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/code/code.tsx @@ -10,8 +10,8 @@ import { highlight, languages, } from '@sim/emcn' +import { Check, Wand } from '@sim/emcn/icons' import { createLogger } from '@sim/logger' -import { Check, Wand2 } from 'lucide-react' import { useParams } from 'next/navigation' import Editor from 'react-simple-code-editor' import { Button } from '@/components/ui/button' @@ -865,7 +865,7 @@ export const Code = memo(function Code({ aria-label='Generate code with AI' className='size-8 rounded-full border border-transparent bg-muted/80 text-muted-foreground shadow-sm transition-all duration-200 hover-hover:border-primary/20 hover-hover:bg-muted hover-hover:text-foreground hover-hover:shadow' > - + )}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/condition-input/condition-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/condition-input/condition-input.tsx index 1099461437e..c0bcab52da0 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/condition-input/condition-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/condition-input/condition-input.tsx @@ -11,9 +11,8 @@ import { Textarea, Tooltip, } from '@sim/emcn' -import { Trash } from '@sim/emcn/icons' +import { ChevronDown, ChevronsUpDown, ChevronUp, Plus, Trash } from '@sim/emcn/icons' import { createLogger } from '@sim/logger' -import { ChevronDown, ChevronsUpDown, ChevronUp, Plus } from 'lucide-react' import { useParams } from 'next/navigation' import Editor from 'react-simple-code-editor' import { isElseConditionTitle } from '@/lib/workflows/conditions' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/credential-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/credential-selector.tsx index 46910e0a005..ab96212d980 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/credential-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/credential-selector.tsx @@ -2,7 +2,7 @@ import { useCallback, useMemo, useState } from 'react' import { Button, Combobox, type ComboboxOptionGroup } from '@sim/emcn' -import { ExternalLink, KeyRound } from 'lucide-react' +import { Key, SquareArrowUpRight } from '@sim/emcn/icons' import { useParams } from 'next/navigation' import { consumeOAuthReturnContext, writeOAuthReturnContext } from '@/lib/credentials/client-state' import { @@ -231,7 +231,7 @@ export function CredentialSelector({ const baseProviderConfig = OAUTH_PROVIDERS[baseProvider] if (!baseProviderConfig) { - return + return } const Icon: StyleableIcon = baseProviderConfig.icon return @@ -278,7 +278,7 @@ export function CredentialSelector({ ? `Connect another ${getProviderName(provider)} account` : `Connect ${getProviderName(provider)} account`, value: '__connect_account__', - iconElement: , + iconElement: , }) } @@ -315,7 +315,7 @@ export function CredentialSelector({ { label: labels?.oauthConnect ?? `Connect ${getProviderName(provider)} account`, value: '__connect_account__', - iconElement: , + iconElement: , }, ], }, @@ -333,7 +333,7 @@ export function CredentialSelector({ serviceAccountTarget?.label ?? `Add ${getProviderName(provider)} key`, value: '__connect_service_account__', - iconElement: , + iconElement: , }, ]), ], @@ -365,7 +365,7 @@ export function CredentialSelector({ return (
- +
{formatDisplayText(displayValue, { workflowSearchHighlight })} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/document-tag-entry/document-tag-entry.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/document-tag-entry/document-tag-entry.tsx index 4e51a715a66..524e7461d47 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/document-tag-entry/document-tag-entry.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/document-tag-entry/document-tag-entry.tsx @@ -12,8 +12,8 @@ import { Label, Trash, } from '@sim/emcn' +import { Plus } from '@sim/emcn/icons' import { generateId } from '@sim/utils/id' -import { Plus } from 'lucide-react' import { FIELD_TYPE_LABELS, getPlaceholderForFieldType } from '@/lib/knowledge/constants' import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text' import { TagDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/env-var-dropdown/env-var-dropdown.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/env-var-dropdown/env-var-dropdown.tsx index dfa0412e510..b20ba4efc87 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/env-var-dropdown/env-var-dropdown.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/env-var-dropdown/env-var-dropdown.tsx @@ -8,7 +8,7 @@ import { PopoverScrollArea, PopoverSection, } from '@sim/emcn' -import { Plus } from 'lucide-react' +import { Plus } from '@sim/emcn/icons' import { writePendingCredentialCreateRequest } from '@/lib/credentials/client-state' import type { WorkspaceEnvironmentData } from '@/lib/environment/api' import { usePersonalEnvironment, useWorkspaceEnvironment } from '@/hooks/queries/environment' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/eval-input/eval-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/eval-input/eval-input.tsx index 621e07b38e9..fe7ee8c53af 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/eval-input/eval-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/eval-input/eval-input.tsx @@ -1,8 +1,7 @@ import { useMemo, useRef } from 'react' import { Button, cn, Input, Label, Textarea, Tooltip } from '@sim/emcn' -import { Trash } from '@sim/emcn/icons' +import { Plus, Trash } from '@sim/emcn/icons' import { generateId } from '@sim/utils/id' -import { Plus } from 'lucide-react' import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text' import { TagDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown' import { getActiveWorkflowSearchHighlight } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workflow-search-highlight' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/file-upload/file-upload.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/file-upload/file-upload.tsx index 4b0368813d7..2b7356c6697 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/file-upload/file-upload.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/file-upload/file-upload.tsx @@ -2,12 +2,12 @@ import { useMemo, useRef, useState } from 'react' import { Button, Combobox, cn } from '@sim/emcn' +import { X } from '@sim/emcn/icons' import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' import { generateShortId } from '@sim/utils/id' import { randomFloat } from '@sim/utils/random' import { useQueryClient } from '@tanstack/react-query' -import { X } from 'lucide-react' import { useParams } from 'next/navigation' import { Progress } from '@/components/ui/progress' import { isApiClientError } from '@/lib/api/client/errors' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/filter-builder/components/filter-rule-row.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/filter-builder/components/filter-rule-row.tsx index 9cf97b73144..b0f33f1dded 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/filter-builder/components/filter-rule-row.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/filter-builder/components/filter-rule-row.tsx @@ -10,7 +10,7 @@ import { Label, Trash, } from '@sim/emcn' -import { Plus } from 'lucide-react' +import { Plus } from '@sim/emcn/icons' import type { FilterRule } from '@/lib/table/query-builder/constants' import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text' import { TagDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/filter-builder/filter-builder.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/filter-builder/filter-builder.tsx index 4075a2f72de..4df315eb4c4 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/filter-builder/filter-builder.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/filter-builder/filter-builder.tsx @@ -2,7 +2,7 @@ import { useCallback, useMemo } from 'react' import { Button } from '@sim/emcn' -import { Plus } from 'lucide-react' +import { Plus } from '@sim/emcn/icons' import { useTableColumns } from '@/lib/table/hooks' import type { FilterRule } from '@/lib/table/query-builder/constants' import { useFilterBuilder } from '@/lib/table/query-builder/use-query-builder' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/grouped-checkbox-list/grouped-checkbox-list.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/grouped-checkbox-list/grouped-checkbox-list.tsx index ac2673216a3..5d4b10ea038 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/grouped-checkbox-list/grouped-checkbox-list.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/grouped-checkbox-list/grouped-checkbox-list.tsx @@ -10,7 +10,7 @@ import { ChipModalHeader, cn, } from '@sim/emcn' -import { Settings2 } from 'lucide-react' +import { ManageWorkspace } from '@sim/emcn/icons' import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text' import { getWorkflowSearchLabelHighlight } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workflow-search-highlight' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' @@ -135,7 +135,7 @@ export function GroupedCheckboxList({ )} > - + Configure PII Types - +
)} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/messages-input/messages-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/messages-input/messages-input.tsx index 6f1367e2141..a010caf0eef 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/messages-input/messages-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/messages-input/messages-input.tsx @@ -8,10 +8,9 @@ import { useState, } from 'react' import { Button, cn, Popover, PopoverContent, PopoverItem, PopoverTrigger } from '@sim/emcn' -import { Trash } from '@sim/emcn/icons' +import { ChevronDown, ChevronsUpDown, ChevronUp, Plus, Trash } from '@sim/emcn/icons' import { generateShortId } from '@sim/utils/id' import { isEqual } from 'es-toolkit' -import { ChevronDown, ChevronsUpDown, ChevronUp, Plus } from 'lucide-react' import { EnvVarDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/env-var-dropdown' import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text' import { TagDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/selector-combobox/selector-combobox.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/selector-combobox/selector-combobox.tsx index 6768c2e0e87..920177208e4 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/selector-combobox/selector-combobox.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/selector-combobox/selector-combobox.tsx @@ -1,7 +1,7 @@ import type React from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { Button, Combobox as EditableCombobox } from '@sim/emcn' -import { X } from 'lucide-react' +import { X } from '@sim/emcn/icons' import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text' import { SubBlockInputController } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/sub-block-input-controller' import { getWorkflowSearchLabelHighlight } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workflow-search-highlight' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/short-input/short-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/short-input/short-input.tsx index d6befd56167..fd9181674e8 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/short-input/short-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/short-input/short-input.tsx @@ -1,6 +1,6 @@ import { memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react' import { cn, Input } from '@sim/emcn' -import { Wand2 } from 'lucide-react' +import { Wand } from '@sim/emcn/icons' import { useReactFlow } from 'reactflow' import { Button } from '@/components/ui/button' import { @@ -418,7 +418,7 @@ export const ShortInput = memo(function ShortInput({ aria-label='Generate content with AI' className='size-8 rounded-full border border-transparent bg-muted/80 text-muted-foreground shadow-sm transition-all duration-200 hover-hover:border-primary/20 hover-hover:bg-muted hover-hover:text-foreground hover-hover:shadow' > - +
)} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/skill-input/skill-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/skill-input/skill-input.tsx index ac2282c5b62..c7f83381d6e 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/skill-input/skill-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/skill-input/skill-input.tsx @@ -2,7 +2,7 @@ import { useCallback, useMemo, useState } from 'react' import { Combobox, type ComboboxOptionGroup } from '@sim/emcn' -import { Plus, XIcon } from 'lucide-react' +import { Plus, X } from '@sim/emcn/icons' import { useParams } from 'next/navigation' import { AgentSkillsIcon } from '@/components/icons' import { SkillModal } from '@/app/workspace/[workspaceId]/skills/components/skill-modal' @@ -183,7 +183,7 @@ export function SkillInput({ className='flex items-center justify-center text-[var(--text-tertiary)] transition-colors hover-hover:text-[var(--text-primary)]' aria-label='Remove skill' > - + )}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-setup-wizard/slack-setup-wizard.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-setup-wizard/slack-setup-wizard.tsx index 3d3745abb4c..a201310d374 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-setup-wizard/slack-setup-wizard.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-setup-wizard/slack-setup-wizard.tsx @@ -2,7 +2,7 @@ import { type ReactNode, useCallback, useMemo, useState } from 'react' import { Checkbox, cn, Input, Label, SecretInput, Tooltip, Wizard } from '@sim/emcn' -import { Check, ChevronRight, Clipboard, Info } from 'lucide-react' +import { Check, ChevronRight, CircleInfo, Clipboard } from '@sim/emcn/icons' import { useShallow } from 'zustand/react/shallow' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' import { useWebhookManagement } from '@/hooks/use-webhook-management' @@ -541,7 +541,7 @@ function CapabilityRow({ blockId, capability, checked, disabled }: CapabilityRow - +

{capability.description}

diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/sort-builder/components/sort-rule-row.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/sort-builder/components/sort-rule-row.tsx index 35c68663cde..94d320d805b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/sort-builder/components/sort-rule-row.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/sort-builder/components/sort-rule-row.tsx @@ -8,7 +8,7 @@ import { Label, Trash, } from '@sim/emcn' -import { Plus } from 'lucide-react' +import { Plus } from '@sim/emcn/icons' import type { SortRule } from '@/lib/table/query-builder/constants' import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text' import { getWorkflowSearchLabelHighlight } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workflow-search-highlight' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/sort-builder/sort-builder.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/sort-builder/sort-builder.tsx index 9427c7a8ca5..c9963668a00 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/sort-builder/sort-builder.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/sort-builder/sort-builder.tsx @@ -2,8 +2,8 @@ import { useCallback, useMemo } from 'react' import { Button, type ComboboxOption } from '@sim/emcn' +import { Plus } from '@sim/emcn/icons' import { generateId } from '@sim/utils/id' -import { Plus } from 'lucide-react' import { useTableColumns } from '@/lib/table/hooks' import { SORT_DIRECTION_OPTIONS, type SortRule } from '@/lib/table/query-builder/constants' import { useCanonicalSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-canonical-sub-block-value' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/starter/input-format.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/starter/input-format.tsx index 25324d61f91..b26fd8ddc3b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/starter/input-format.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/starter/input-format.tsx @@ -17,8 +17,7 @@ import { languages, Tooltip, } from '@sim/emcn' -import { Trash } from '@sim/emcn/icons' -import { ArrowLeftRight, Plus } from 'lucide-react' +import { ArrowLeftRight, Plus, Trash } from '@sim/emcn/icons' import Editor from 'react-simple-code-editor' import { createDefaultInputFormatField, diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown.tsx index cd8b9df42cf..69fb1b5a74e 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown.tsx @@ -11,8 +11,8 @@ import { PopoverSection, usePopoverContext, } from '@sim/emcn' +import { Repeat, Split } from '@sim/emcn/icons' import { isEqual } from 'es-toolkit' -import { RepeatIcon, SplitIcon } from 'lucide-react' import { useShallow } from 'zustand/react/shallow' import { useStoreWithEqualityFn } from 'zustand/traditional' import { @@ -1767,9 +1767,9 @@ export const TagDropdown: React.FC = ({ if (blockConfig?.icon) { tagIcon = blockConfig.icon } else if (group.blockType === 'loop') { - tagIcon = RepeatIcon + tagIcon = Repeat } else if (group.blockType === 'parallel') { - tagIcon = SplitIcon + tagIcon = Split } const normalizedBlockName = normalizeName(group.blockName) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/credential-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/credential-selector.tsx index d11ab696cad..624f8bf34d6 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/credential-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/credential-selector.tsx @@ -2,7 +2,7 @@ import { createElement, useCallback, useMemo, useRef, useState } from 'react' import { Button, Combobox } from '@sim/emcn' -import { ExternalLink } from 'lucide-react' +import { SquareArrowUpRight } from '@sim/emcn/icons' import { useParams } from 'next/navigation' import { consumeOAuthReturnContext, writeOAuthReturnContext } from '@/lib/credentials/client-state' import { @@ -30,7 +30,7 @@ const getProviderIcon = (providerName: OAuthProvider) => { const baseProviderConfig = OAUTH_PROVIDERS[baseProvider] if (!baseProviderConfig) { - return + return } return createElement(baseProviderConfig.icon, { className: 'size-3' }) } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/parameter.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/parameter.tsx index f756c6d127e..7bd4d1d339c 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/parameter.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/parameter.tsx @@ -3,7 +3,7 @@ import type React from 'react' import { useRef, useState } from 'react' import { Button, cn, Input, Label, Tooltip } from '@sim/emcn' -import { ArrowLeftRight, ArrowUp } from 'lucide-react' +import { ArrowLeftRight, ArrowUp } from '@sim/emcn/icons' import type { WandControlHandlers } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block' /** diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx index 0b34f353382..f31d80571fd 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx @@ -14,8 +14,8 @@ import { Switch, Tooltip, } from '@sim/emcn' +import { ArrowLeft, ChevronRight, Server, Wrench, X } from '@sim/emcn/icons' import { createLogger } from '@sim/logger' -import { ArrowLeft, ChevronRight, ServerIcon, WrenchIcon, XIcon } from 'lucide-react' import { useParams } from 'next/navigation' import { McpIcon, WorkflowIcon } from '@/components/icons' import { @@ -1415,7 +1415,7 @@ export const ToolInput = memo(function ToolInput({ serverToolItems.push({ label: `Use all ${toolCount} tools`, value: `mcp-server-all-${mcpServerDrilldown}`, - iconElement: createToolIcon('#6366F1', ServerIcon), + iconElement: createToolIcon('#6366F1', Server), onSelect: () => { if (allAlreadySelected) return // Remove existing individual tools from this server to avoid duplicates @@ -1497,7 +1497,7 @@ export const ToolInput = memo(function ToolInput({ actionItems.push({ label: 'Create Tool', value: 'action-create-tool', - icon: WrenchIcon, + icon: Wrench, onSelect: () => { setCustomToolModalOpen(true) setOpen(false) @@ -1535,7 +1535,7 @@ export const ToolInput = memo(function ToolInput({ return { label: customTool.title, value: `custom-${customTool.id}`, - iconElement: createToolIcon('#3B82F6', WrenchIcon), + iconElement: createToolIcon('#3B82F6', Wrench), disabled: isPreview || alreadySelected, onSelect: () => { if (alreadySelected) return @@ -1568,7 +1568,7 @@ export const ToolInput = memo(function ToolInput({ serverItems.push({ label: `${serverName} (${toolCount} tools)`, value: `mcp-server-folder-${serverId}`, - iconElement: createToolIcon('#6366F1', ServerIcon), + iconElement: createToolIcon('#6366F1', Server), suffixElement: , onSelect: () => { setMcpServerDrilldown(serverId) @@ -1883,7 +1883,7 @@ export const ToolInput = memo(function ToolInput({ }} > {isCustomTool ? ( - + ) : isMcpTool ? ( - + - + )}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/variables-input/variables-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/variables-input/variables-input.tsx index 4c2e668c459..7c83e20d8d9 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/variables-input/variables-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/variables-input/variables-input.tsx @@ -11,9 +11,8 @@ import { Textarea, Tooltip, } from '@sim/emcn' -import { Trash } from '@sim/emcn/icons' +import { ArrowLeftRight, Plus, Trash } from '@sim/emcn/icons' import { generateId } from '@sim/utils/id' -import { ArrowLeftRight, Plus } from 'lucide-react' import { useParams } from 'next/navigation' import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text' import { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx index 8832dd0ef54..d9fd577e04f 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx @@ -1,14 +1,14 @@ import { type JSX, type MouseEvent, memo, useCallback, useMemo, useRef, useState } from 'react' import { Button, cn, Input, Label, Tooltip } from '@sim/emcn' -import { isEqual } from 'es-toolkit' import { - AlertTriangle, ArrowLeftRight, ArrowUp, Check, Clipboard, - ExternalLink, -} from 'lucide-react' + SquareArrowUpRight, + TriangleAlert, +} from '@sim/emcn/icons' +import { isEqual } from 'es-toolkit' import { useParams } from 'next/navigation' import type { FilterRule, SortRule } from '@/lib/table/query-builder/constants' import { @@ -263,7 +263,7 @@ const renderLabel = ( - + @@ -364,7 +364,7 @@ const renderLabel = ( onClick={externalLink?.onClick} aria-label={externalLink?.tooltip} > - + diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/subflow-editor/subflow-editor.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/subflow-editor/subflow-editor.tsx index d7f22cb3354..318d5d30052 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/subflow-editor/subflow-editor.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/subflow-editor/subflow-editor.tsx @@ -8,7 +8,7 @@ import { Input, Label, } from '@sim/emcn' -import { ChevronUp } from 'lucide-react' +import { ChevronUp } from '@sim/emcn/icons' import SimpleCodeEditor from 'react-simple-code-editor' import { WORKFLOW_SEARCH_SUBFLOW_FIELD_IDS } from '@/lib/workflows/search-replace/subflow-fields' import { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx index 95a26a33e95..79a09e5d348 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx @@ -2,17 +2,17 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { Button, cn, DashedDividerLine, FieldDivider, Loader, Tooltip } from '@sim/emcn' -import { isEqual } from 'es-toolkit' import { BookOpen, Check, ChevronDown, ChevronUp, - ExternalLink, Lock, Pencil, + SquareArrowUpRight, Unlock, -} from 'lucide-react' +} from '@sim/emcn/icons' +import { isEqual } from 'es-toolkit' import { useParams } from 'next/navigation' import { usePostHog } from 'posthog-js/react' import { useShallow } from 'zustand/react/shallow' @@ -568,7 +568,7 @@ export function Editor() { onClick={handleOpenChildWorkflow} className='absolute right-[6px] bottom-1.5 z-10 size-[24px] cursor-pointer border border-[var(--border)] bg-[var(--surface-2)] p-0 hover-hover:bg-[var(--surface-4)]' > - + Open workflow diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/toolbar/toolbar.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/toolbar/toolbar.tsx index 7abb72d398c..ff6b7e76f75 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/toolbar/toolbar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/toolbar/toolbar.tsx @@ -20,8 +20,8 @@ import { handleKeyboardActivation, Info, } from '@sim/emcn' +import { ChevronDown, Search } from '@sim/emcn/icons' import clsx from 'clsx' -import { ChevronDown, Search } from 'lucide-react' import { useParams } from 'next/navigation' import { usePostHog } from 'posthog-js/react' import { captureEvent } from '@/lib/posthog/client' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx index 5b16f42ee9c..301614bd007 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx @@ -23,11 +23,10 @@ import { Trash, toast, } from '@sim/emcn' -import { Download, Lock, Unlock } from '@sim/emcn/icons' +import { BubbleChatDelay, Download, Lock, Plus, Square, Unlock } from '@sim/emcn/icons' import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' import { useQueryClient } from '@tanstack/react-query' -import { History, Plus, Square } from 'lucide-react' import { useParams, useRouter } from 'next/navigation' import { usePostHog } from 'posthog-js/react' import { useShallow } from 'zustand/react/shallow' @@ -858,7 +857,7 @@ export const Panel = memo(function Panel() { > diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/search-replace/workflow-search-replace.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/search-replace/workflow-search-replace.tsx index 650170cae4a..73fe7d0d980 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/search-replace/workflow-search-replace.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/search-replace/workflow-search-replace.tsx @@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { Button, cn, Input, toast } from '@sim/emcn' -import { ChevronDown, ChevronRight, ChevronUp, X } from 'lucide-react' +import { ChevronDown, ChevronRight, ChevronUp, X } from '@sim/emcn/icons' import { useParams } from 'next/navigation' import { useShallow } from 'zustand/react/shallow' import { getWorkflowSearchDependentClears } from '@/lib/workflows/search-replace/dependencies' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/loop/loop-config.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/loop/loop-config.ts index e63a995c4e8..5eb91b7cd55 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/loop/loop-config.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/loop/loop-config.ts @@ -1,4 +1,4 @@ -import { RepeatIcon } from 'lucide-react' +import { Repeat } from '@sim/emcn/icons' /** * Loop tool configuration for the toolbar. @@ -7,7 +7,7 @@ import { RepeatIcon } from 'lucide-react' export const LoopTool = { type: 'loop', name: 'Loop', - icon: RepeatIcon, + icon: Repeat, bgColor: '#2FB3FF', docsLink: 'https://docs.sim.ai/workflows/blocks/loop', } as const diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/parallel/parallel-config.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/parallel/parallel-config.ts index 5b70ada1efe..71600a4b024 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/parallel/parallel-config.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/parallel/parallel-config.ts @@ -1,4 +1,4 @@ -import { SplitIcon } from 'lucide-react' +import { Split } from '@sim/emcn/icons' /** * Parallel tool configuration for the toolbar. @@ -7,7 +7,7 @@ import { SplitIcon } from 'lucide-react' export const ParallelTool = { type: 'parallel', name: 'Parallel', - icon: SplitIcon, + icon: Split, bgColor: '#FEE12B', docsLink: 'https://docs.sim.ai/workflows/blocks/parallel', } as const diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/filter-popover/filter-popover.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/filter-popover/filter-popover.tsx index 06bb376eac6..3ba40426fd0 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/filter-popover/filter-popover.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/filter-popover/filter-popover.tsx @@ -11,8 +11,8 @@ import { PopoverSection, PopoverTrigger, } from '@sim/emcn' +import { ListFilter } from '@sim/emcn/icons' import clsx from 'clsx' -import { Filter } from 'lucide-react' import type { BlockInfo, TerminalFilters, @@ -53,7 +53,9 @@ export const FilterPopover = memo(function FilterPopover({ onClick={(e) => e.stopPropagation()} aria-label='Filters' > - + - + diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/toggle-button/toggle-button.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/toggle-button/toggle-button.tsx index edf4e0e8401..c23902878da 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/toggle-button/toggle-button.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/toggle-button/toggle-button.tsx @@ -3,8 +3,8 @@ import type React from 'react' import { memo } from 'react' import { Button } from '@sim/emcn' +import { ChevronDown } from '@sim/emcn/icons' import clsx from 'clsx' -import { ChevronDown } from 'lucide-react' export interface ToggleButtonProps { isExpanded: boolean diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx index 6d945a8bcc2..bdba4bacde1 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx @@ -12,11 +12,19 @@ import { PopoverTrigger, Tooltip, } from '@sim/emcn' -import { Download } from '@sim/emcn/icons' +import { + ArrowDown, + ArrowUp, + Database, + Download, + MoreHorizontal, + Palette, + Pause, + Trash, +} from '@sim/emcn/icons' import { formatDuration } from '@sim/utils/formatting' import { useVirtualizer } from '@tanstack/react-virtual' import clsx from 'clsx' -import { ArrowDown, ArrowUp, Database, MoreHorizontal, Palette, Pause, Trash2 } from 'lucide-react' import Link from 'next/link' import { getEnv, isTruthy } from '@/lib/core/config/env' import { sendMothershipMessage } from '@/lib/mothership/events' @@ -1434,7 +1442,7 @@ export const Terminal = memo(function Terminal() { aria-label='Clear console' className='!p-1.5 -m-1.5' > - + diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/utils.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/utils.ts index f7f5400fbfd..bd7ae34a07e 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/utils.ts @@ -1,12 +1,5 @@ import type React from 'react' -import { - AlertTriangleIcon, - BanIcon, - NetworkIcon, - RepeatIcon, - SplitIcon, - XCircleIcon, -} from 'lucide-react' +import { Ban, CircleX, Repeat, Split, TriangleAlert, Workflow } from '@sim/emcn/icons' import { getBlock } from '@/blocks' import { isWorkflowBlockType } from '@/executor/constants' import { TERMINAL_BLOCK_COLUMN_WIDTH } from '@/stores/constants' @@ -43,27 +36,27 @@ export function getBlockIcon( } if (blockType === 'loop') { - return RepeatIcon + return Repeat } if (blockType === 'parallel') { - return SplitIcon + return Split } if (blockType === 'workflow') { - return NetworkIcon + return Workflow } if (blockType === 'error') { - return XCircleIcon + return CircleX } if (blockType === 'validation') { - return AlertTriangleIcon + return TriangleAlert } if (blockType === 'cancelled') { - return BanIcon + return Ban } return null diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/variables/variables.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/variables/variables.tsx index eff41e09f82..d6676387379 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/variables/variables.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/variables/variables.tsx @@ -15,8 +15,7 @@ import { Label, languages, } from '@sim/emcn' -import { Trash } from '@sim/emcn/icons' -import { Plus, X } from 'lucide-react' +import { Plus, Trash, X } from '@sim/emcn/icons' import Editor from 'react-simple-code-editor' import { useShallow } from 'zustand/react/shallow' import { validateName } from '@/lib/core/utils/validation' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/wand-prompt-bar/wand-prompt-bar.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/wand-prompt-bar/wand-prompt-bar.tsx index 89848f74c12..e0ddc69b313 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/wand-prompt-bar/wand-prompt-bar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/wand-prompt-bar/wand-prompt-bar.tsx @@ -1,6 +1,6 @@ import { useEffect, useRef, useState } from 'react' import { cn } from '@sim/emcn' -import { SendIcon, XIcon } from 'lucide-react' +import { Send, X } from '@sim/emcn/icons' import { Button } from '@/components/ui/button' interface WandPromptBarProps { @@ -120,7 +120,7 @@ export function WandPromptBar({ onClick={handleCancel} className='size-8 rounded-full text-muted-foreground hover-hover:bg-accent/50 hover-hover:text-foreground' > - + {!isStreaming && ( @@ -131,7 +131,7 @@ export function WandPromptBar({ className='size-8 rounded-full text-muted-foreground hover-hover:bg-primary/10 hover-hover:text-foreground' disabled={isLoading || isStreaming || !promptValue.trim()} > - + )}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-controls/workflow-controls.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-controls/workflow-controls.tsx index bbdab26c41f..0b41a7caf2b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-controls/workflow-controls.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-controls/workflow-controls.tsx @@ -15,8 +15,8 @@ import { Tooltip, Undo, } from '@sim/emcn' +import { SelectAll } from '@sim/emcn/icons' import { createLogger } from '@sim/logger' -import { Scan } from 'lucide-react' import { useReactFlow } from 'reactflow' import { useShallow } from 'zustand/react/shallow' import { useSession } from '@/lib/auth/auth-client' @@ -142,7 +142,7 @@ export const WorkflowControls = memo(function WorkflowControls() {
-
+
@@ -176,7 +176,7 @@ export const WorkflowControls = memo(function WorkflowControls() { -
+
@@ -185,7 +185,7 @@ export const WorkflowControls = memo(function WorkflowControls() { className='size-[28px] rounded-md p-0 hover-hover:bg-[var(--surface-5)]' onClick={handleFitToView} > - + diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-editor/preview-editor.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-editor/preview-editor.tsx index 6bbeafd5977..130096badc6 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-editor/preview-editor.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-editor/preview-editor.tsx @@ -4,7 +4,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { Badge, Button, - ChevronDown, Code, Combobox, cn, @@ -14,21 +13,21 @@ import { Label, Tooltip, } from '@sim/emcn' -import { formatDuration } from '@sim/utils/formatting' import { ArrowDown, ArrowUp, Check, - ChevronDown as ChevronDownIcon, + ChevronDown, ChevronUp, Clipboard, - ExternalLink, - Maximize2, - RepeatIcon, + Expand, + Repeat, Search, - SplitIcon, + Split, + SquareArrowUpRight, X, -} from 'lucide-react' +} from '@sim/emcn/icons' +import { formatDuration } from '@sim/utils/formatting' import { useParams } from 'next/navigation' import { ReactFlowProvider } from 'reactflow' import { extractReferencePrefixes } from '@/lib/workflows/sanitization/references' @@ -404,7 +403,7 @@ function ConnectionsSection({ {connection.blockName} {hasFields && ( - Variables - Secrets - {isExecutionMode && onDrillDown ? ( - + ) : ( - + )} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-workflow/components/subflow/subflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-workflow/components/subflow/subflow.tsx index 922dc37dad9..fe45e4280f0 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-workflow/components/subflow/subflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-workflow/components/subflow/subflow.tsx @@ -2,8 +2,8 @@ import { memo } from 'react' import { Badge, cn } from '@sim/emcn' +import { Repeat, Split } from '@sim/emcn/icons' import { HANDLE_POSITIONS } from '@sim/workflow-renderer' -import { RepeatIcon, SplitIcon } from 'lucide-react' import { Handle, type NodeProps, Position } from 'reactflow' import { getTileIconColorClass } from '@/blocks/icon-color' @@ -42,7 +42,7 @@ function WorkflowPreviewSubflowInner({ data }: NodeProps ariaLabel?: string children: React.ReactNode - className?: string primaryAction?: { label: string onSelect: () => void @@ -185,7 +183,7 @@ interface CollapsedWorkflowFlyoutItemProps { const EDIT_ROW_CLASS = cn( chipVariants({ active: true, fullWidth: true }), - 'mx-0 min-w-0 cursor-default select-none text-small' + 'min-w-0 cursor-default select-none text-small' ) export function CollapsedSidebarMenu({ @@ -193,11 +191,10 @@ export function CollapsedSidebarMenu({ hover, ariaLabel, children, - className, primaryAction, }: CollapsedSidebarMenuProps) { return ( -
+
{ diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/file-list/file-list.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/file-list/file-list.tsx index ef8aab99bdb..85158ee7b9d 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/file-list/file-list.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/file-list/file-list.tsx @@ -2,7 +2,7 @@ import { memo, useMemo, useState } from 'react' import { cn } from '@sim/emcn' -import { ChevronRight } from 'lucide-react' +import { ChevronRight } from '@sim/emcn/icons' import Link from 'next/link' import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace' import type { WorkspaceFileFolderApi } from '@/hooks/queries/workspace-file-folders' @@ -92,9 +92,7 @@ const FileTreeNodeItem = memo(function FileTreeNodeItem({ style={{ paddingLeft: `${8 + level * INDENT_PER_LEVEL + CHEVRON_WIDTH}px` }} > {FILE_ICON} - - {node.name} - + {node.name} ) } @@ -132,7 +130,7 @@ const FileTreeNodeItem = memo(function FileTreeNodeItem({ )} - + {node.name} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/help-modal/help-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/help-modal/help-modal.tsx index 41fe122cd38..6e734ec5c31 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/help-modal/help-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/help-modal/help-modal.tsx @@ -9,10 +9,10 @@ import { ChipModalFooter, ChipModalHeader, } from '@sim/emcn' +import { X } from '@sim/emcn/icons' import { createLogger } from '@sim/logger' import { useMutation } from '@tanstack/react-query' import imageCompression from 'browser-image-compression' -import { X } from 'lucide-react' import Image from 'next/image' import { Controller, useForm } from 'react-hook-form' import { z } from 'zod' @@ -250,8 +250,8 @@ export function HelpModal({ open, onOpenChange, workflowId, workspaceId }: HelpM } return ( - - onOpenChange(false)}>Help & support + + onOpenChange(false)}>Contact support
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/sidebar-footer/index.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/sidebar-footer/index.ts new file mode 100644 index 00000000000..c181faf748e --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/sidebar-footer/index.ts @@ -0,0 +1 @@ +export { SidebarFooter } from './sidebar-footer' diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/sidebar-footer/sidebar-footer.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/sidebar-footer/sidebar-footer.tsx new file mode 100644 index 00000000000..ff0adf795e6 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/sidebar-footer/sidebar-footer.tsx @@ -0,0 +1,259 @@ +'use client' + +import type { ComponentType } from 'react' +import { + Chip, + chipContentLabelClass, + chipVariants, + cn, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + Skeleton, +} from '@sim/emcn' +import { BookOpen, Credit, HelpCircle, Settings, Trash, Users } from '@sim/emcn/icons' +import { SlackIcon } from '@/components/icons' +import { useSession } from '@/lib/auth/auth-client' +import { canViewWorkspaceBillingSettings } from '@/lib/billing/workspace-permissions' +import { isBillingEnabled } from '@/lib/core/config/env-flags' +import { getUserColor } from '@/lib/workspaces/colors' +import { useWorkspaceHostContext } from '@/app/workspace/[workspaceId]/providers/workspace-host-provider' +import type { SettingsSection } from '@/app/workspace/[workspaceId]/settings/navigation' +import { SIDEBAR_ITEM_GAP_CLASS } from '@/app/workspace/[workspaceId]/w/components/sidebar/constants' +import { SidebarTooltip } from '@/app/workspace/[workspaceId]/w/components/sidebar/sidebar' +import { useUserProfile } from '@/hooks/queries/user-profile' +import { useWorkspaceInvitePolicy } from '@/hooks/use-workspace-invite-policy' + +/** + * Settings destinations reachable from the profile menu, in display order. Labels + * and icons mirror the settings navigation entries they open, so the menu and the + * settings sidebar never disagree about what a section is called. + * + * Which of them a given viewer actually gets is decided in {@link SidebarFooter} — + * the same gates the settings sidebar and the section route apply, so the menu + * never lists a page the server would refuse. + */ +const PROFILE_MENU_ITEMS: readonly { + section: SettingsSection + label: string + icon: ComponentType<{ className?: string }> +}[] = [ + { section: 'general', label: 'Settings', icon: Settings }, + { section: 'billing', label: 'Subscription', icon: Credit }, + { section: 'teammates', label: 'Teammates', icon: Users }, + { section: 'recently-deleted', label: 'Recently deleted', icon: Trash }, +] + +interface SidebarFooterProps { + workspaceId: string + isCollapsed: boolean + showCollapsedTooltips: boolean + onOpenSettings: (section: SettingsSection) => void + onOpenDocs: () => void + onJoinSlack: () => void + onContactSupport: () => void +} + +/** + * Pinned bottom bar of the workspace sidebar: the viewer's avatar and name, which + * open a menu of their settings destinations, plus a help menu. + * + * Expanded, the two share one row — the profile claims the free width so the help + * button lands hard right, mirroring the collapse control in the workspace header. + * Collapsed, the rail is too narrow for a row, so they stack as icon chips with + * help on top and the profile resting at the foot of the rail. + * + * Both layouts are the same two elements — only the container's direction and the + * children's classes change — because `isCollapsed` flips in one frame while the + * rail takes 200ms to widen, so the row spends that window laid out at a width it + * does not fit in. Neither element may give ground there: the profile stops at its + * avatar (no `min-w-0`) and the help button never shrinks, so the row overflows the + * narrow rail and the aside's `overflow-hidden` clips it. The avatar keeps the exact + * position it holds collapsed, and the `?` rides in on the opening edge — paced by + * the rail itself rather than by a duration of its own. + * + * Collapsed reverses the flex direction instead of reordering the DOM, which is what + * keeps both elements (and the help menu's trigger) alive across a toggle. The cost + * is bottom-up focus order there, a smaller price for one pair of adjacent controls + * than remounting a trigger mid-animation. + */ +export function SidebarFooter({ + workspaceId, + isCollapsed, + showCollapsedTooltips, + onOpenSettings, + onOpenDocs, + onJoinSlack, + onContactSupport, +}: SidebarFooterProps) { + const { data: profile } = useUserProfile() + const { data: session } = useSession() + const hostContext = useWorkspaceHostContext() + const { isInvitationsDisabled } = useWorkspaceInvitePolicy(workspaceId) + + const name = profile ? profile.name?.trim() || profile.email : '' + + /** + * Subscription is dropped for viewers the Billing page would turn away — a + * deployment with billing off, or anyone who is not the payer (on an + * organization-hosted workspace, every member who is not an org admin). The + * settings sidebar hides its own Billing entry on exactly this test. + */ + const menuItems = PROFILE_MENU_ITEMS.filter( + (item) => + item.section !== 'billing' || canViewWorkspaceBillingSettings(hostContext, session?.user?.id) + ) + + /** + * Teammates is a dead end on a plan that cannot invite, so a blocked viewer is + * sent to the plan itself instead — which resolves to the upgrade page for + * anyone who cannot manage the payer. With billing off there is nowhere to send + * them and no upgrade to make, so the row simply does nothing. This is the gate + * the workspace switcher's "Manage workspace" entry carried before this menu + * took the section over. + */ + const handleSelectSection = (section: SettingsSection) => { + if (section === 'teammates' && isInvitationsDisabled) { + if (isBillingEnabled) onOpenSettings('billing') + return + } + onOpenSettings(section) + } + + /** + * Built from plain `img`/`div` rather than the emcn `Avatar`, whose Radix root + * renders a `` — and globals fade every `span` in the collapsed rail to + * `opacity: 0`, which blanked the avatar exactly where it is the only thing + * left to see. The workspace header's logo sidesteps the same rule the same way. + */ + const avatar = !profile ? ( + + ) : profile.image ? ( + + ) : ( +
+ {name.charAt(0).toUpperCase()} +
+ ) + + /** + * Expanded, the chip hugs its content (`max-w-full` so a long name truncates + * rather than overflowing) and the free width belongs to the wrapper it sits in, + * so hover highlights only the avatar and name. Collapsed, `fullWidth` fills the + * narrow rail instead. Both mirror the workspace header's chip exactly. + * + * No `min-w-0`: the label already truncates on its own, and letting the chip + * shrink past its avatar is what let the help button ride onto the photo while + * the rail was still narrow (see {@link SidebarFooter}). Its automatic minimum + * is exactly the icon-only chip, so the avatar holds the same spot at any width. + * + * The name is the button's accessible name — no `aria-label`, which would + * override the visible text. Radix contributes the menu role and expanded state. + */ + const profileMenu = ( + + + + + + + + {menuItems.map(({ section, label, icon: Icon }) => ( + handleSelectSection(section)}> + + {label} + + ))} + + + ) + + /** + * The same `Chip` the workspace header uses for Search and Collapse, so the two + * ends of the rail carry identical chrome — box, radius, hover fill, and glyph + * size all come from the component rather than a local class string. + * + * One node across both states; only `fullWidth` changes (collapsed it fills the + * rail, expanded it hugs its icon). Toggling a prop rather than rendering two + * buttons keeps the same DOM node — and the same Radix menu — alive through the + * transition instead of tearing one trigger down and mounting another mid-animation. + * + * Icon-only in both: the collapsed rail hides labels anyway. The tooltip names it + * while the rail is collapsed. + */ + const helpMenu = ( + + + + + + + {/* Anchored to whichever edge the trigger sits on, so the menu never overhangs the rail. */} + + + + Docs + + + + Join Slack + + + + Contact support + + + + ) + + return ( +
+ {/* Expanded, claims the row's free width so the help button lands hard right — + the same wrapper the workspace header puts around its chip. `flex` makes the + inline-flex chip a flex item rather than an inline one, so the wrapper is + exactly the chip's 30px instead of a line box padded by the strut's + half-leading, which would deepen the bar below the chip. Collapsed, it + stretches to the rail on its own and the chip fills it. */} +
{profileMenu}
+ {helpMenu} +
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/sidebar-section/index.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/sidebar-section/index.ts new file mode 100644 index 00000000000..225ee6fb60e --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/sidebar-section/index.ts @@ -0,0 +1 @@ +export { SidebarSection } from './sidebar-section' diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/sidebar-section/sidebar-section.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/sidebar-section/sidebar-section.tsx new file mode 100644 index 00000000000..341f8f8091c --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/sidebar-section/sidebar-section.tsx @@ -0,0 +1,119 @@ +'use client' + +import { type ReactNode, useState } from 'react' +import { ChevronDown, cn, Expandable, ExpandableContent } from '@sim/emcn' + +/** + * Title-row layout, shared by the toggle and its rail-collapsed counterpart so the + * label lands identically either way. The row's gutter lives here rather than on the + * row itself: the toggle stretches edge to edge and insets only its own content, so + * the full width of the rail is clickable rather than just the text and chevron. + */ +const TITLE_ROW_CLASS = 'flex h-full min-w-0 flex-1 items-center gap-2 px-4' + +/** + * One transition covering both of the chevron's states — the hover fade and the + * collapse rotation — so the two can never drift apart. 150ms on Tailwind's default + * `cubic-bezier(0.4, 0, 0.2, 1)` is the same curve the `collapsible-up`/`-down` + * keyframes use for the body, so the whole section animates as one gesture. + */ +const CHEVRON_TRANSITION_CLASS = 'transition-[opacity,transform] duration-150' + +interface SidebarSectionProps { + title: string + /** Controls pinned to the right of the title (e.g. the Workflows create/more buttons). */ + action?: ReactNode + /** + * True while the rail is collapsed. Only icons are visible there, so the title + * renders as a static label — no chevron, nothing focusable — and the content + * stays open regardless of what the user last toggled. + */ + railCollapsed?: boolean + /** Layout classes for the section wrapper (section gap, positioning). */ + className?: string + children: ReactNode +} + +/** + * A titled, collapsible group of sidebar items ("Chats", "Workspace", "Workflows", + * and each settings group). + * + * Owns the section's vertical rhythm so every section matches: an 18px header row + * — exactly the title's line box at `text-caption` — then a 6px gap to the content, + * sitting between the 2px item gap and the 16px section gap. The gap rides inside + * the collapsing content, so it closes along with it. Consumers supply only the + * item container. + * + * The title carries `sidebar-collapse-hide` so it fades out with the rail while the + * row keeps its height, holding the collapsed rail on the expanded rail's grid. + */ +export function SidebarSection({ + title, + action, + railCollapsed = false, + className, + children, +}: SidebarSectionProps) { + const [expanded, setExpanded] = useState(true) + /** + * Collapse animations are enabled only after the first user toggle, so sections + * render at full height on mount instead of replaying the open animation. + */ + const [animationsEnabled, setAnimationsEnabled] = useState(false) + + const handleToggle = () => { + setAnimationsEnabled(true) + setExpanded((prev) => !prev) + } + + const label = ( + + {title} + + ) + + return ( +
+
+ {railCollapsed ? ( +
{label}
+ ) : ( + + )} + {/* Carries the gutter the row gave up so the toggle can reach the rail's edge. */} + {action ?
{action}
: null} +
+ + + {/* The header gap pads an inner wrapper rather than the animated element: + `collapsible-up`/`-down` interpolate height alone, so a margin here would + hold its full 6px for the whole close and then vanish on unmount, snapping + the next section up. Padding on the content itself can't work either — + border-box keeps it rendered at `height: 0`, so the section never shuts. */} +
{children}
+
+
+
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu.tsx index 87bb0ad375b..98bb5442769 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu.tsx @@ -18,14 +18,14 @@ import { LogOut, Mail, Pencil, + Pin, + PinOff, Plus, SquareArrowUpRight, Trash, Unlock, - Workflow, X, } from '@sim/emcn/icons' -import { Pin, PinOff } from 'lucide-react' interface ContextMenuProps { isOpen: boolean @@ -33,7 +33,6 @@ interface ContextMenuProps { menuRef: React.RefObject onClose: () => void onOpenInNewTab?: () => void - onFindReferences?: () => void onMarkAsRead?: () => void onMarkAsUnread?: () => void onTogglePin?: () => void @@ -62,7 +61,6 @@ interface ContextMenuProps { */ onCloseTab?: () => void showOpenInNewTab?: boolean - showFindReferences?: boolean showMarkAsRead?: boolean showMarkAsUnread?: boolean showPin?: boolean @@ -104,7 +102,6 @@ export function ContextMenu({ menuRef, onClose, onOpenInNewTab, - onFindReferences, onMarkAsRead, onMarkAsUnread, onTogglePin, @@ -117,7 +114,6 @@ export function ContextMenu({ onDelete, onCloseTab, showOpenInNewTab = false, - showFindReferences = false, showMarkAsRead = false, showMarkAsUnread = false, showPin = false, @@ -148,8 +144,7 @@ export function ContextMenu({ showUploadLogo = false, disableUploadLogo = false, }: ContextMenuProps) { - const hasNavigationSection = - (showOpenInNewTab && onOpenInNewTab) || (showFindReferences && onFindReferences) + const hasNavigationSection = showOpenInNewTab && onOpenInNewTab const hasStatusSection = (showMarkAsRead && onMarkAsRead) || (showMarkAsUnread && onMarkAsUnread) || @@ -211,17 +206,6 @@ export function ContextMenu({ Open in new tab )} - {showFindReferences && onFindReferences && ( - { - onFindReferences() - onClose() - }} - > - - Show references - - )} {hasNavigationSection && (hasStatusSection || hasEditSection || hasCopySection) && ( )} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/folder-item/folder-item.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/folder-item/folder-item.tsx index 9cec8e5a508..8d0d15b1544 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/folder-item/folder-item.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/folder-item/folder-item.tsx @@ -2,12 +2,11 @@ import { memo, useCallback, useMemo, useRef, useState } from 'react' import { chipVariants, cn, toast } from '@sim/emcn' -import { Lock } from '@sim/emcn/icons' +import { ChevronRight, Folder, FolderOpen, Lock, MoreHorizontal } from '@sim/emcn/icons' import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' import clsx from 'clsx' -import { ChevronRight, Folder, FolderOpen, MoreHorizontal } from 'lucide-react' import { useRouter } from 'next/navigation' import { SIM_RESOURCES_DRAG_TYPE } from '@/lib/copilot/resource-types' import { generateSubfolderName } from '@/lib/workspaces/naming' diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/references-modal/components/reference-tree/reference-tree.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/references-modal/components/reference-tree/reference-tree.tsx deleted file mode 100644 index ae4def07afa..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/references-modal/components/reference-tree/reference-tree.tsx +++ /dev/null @@ -1,68 +0,0 @@ -'use client' - -import { Workflow } from '@sim/emcn/icons' -import type { ReferenceNode } from '@/lib/api/contracts/workflow-references' - -const CONFIG = { - /** Horizontal indent added per tree level, in pixels. */ - INDENT_PER_LEVEL: 16, - /** Base row padding: lands depth-0 content on the modal's px-4 text gutter. */ - BASE_INDENT: 8, -} as const - -interface ReferenceTreeProps { - nodes: ReferenceNode[] - /** Invoked with a workflow id when a row is activated. */ - onNavigate: (workflowId: string) => void -} - -interface ReferenceTreeItemProps { - node: ReferenceNode - depth: number - onNavigate: (workflowId: string) => void -} - -function ReferenceTreeItem({ node, depth, onNavigate }: ReferenceTreeItemProps) { - return ( -
- - {node.children.length > 0 && ( -
- {node.children.map((child) => ( - - ))} -
- )} -
- ) -} - -/** - * Read-only recursive tree of workflow references. Each row navigates to its - * workflow on click; cyclic leaves are marked and render no children. - */ -export function ReferenceTree({ nodes, onNavigate }: ReferenceTreeProps) { - return ( -
- {nodes.map((node) => ( - - ))} -
- ) -} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/references-modal/references-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/references-modal/references-modal.tsx deleted file mode 100644 index e507111c956..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/references-modal/references-modal.tsx +++ /dev/null @@ -1,74 +0,0 @@ -'use client' - -import { useState } from 'react' -import { ChipModal, ChipModalBody, ChipModalHeader, ChipModalTabs } from '@sim/emcn' -import { useRouter } from 'next/navigation' -import { ReferenceTree } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/references-modal/components/reference-tree/reference-tree' -import { useWorkflowReferences } from '@/hooks/queries/workflow-references' - -type ReferencesTab = 'callers' | 'callees' - -const TABS = [ - { value: 'callers', label: 'Used by' }, - { value: 'callees', label: 'Uses' }, -] as const - -const EMPTY_MESSAGE: Record = { - callers: 'No workflows call this workflow.', - callees: "This workflow doesn't call any other workflows.", -} - -interface ReferencesModalProps { - onClose: () => void - workspaceId: string - workflowId: string - workflowName: string -} - -/** - * IDE-style reference viewer for a workflow. "Used by" lists the workflows that - * call it (inbound); "Uses" lists the workflows it calls (outbound). Both are - * recursive trees whose rows navigate to the referenced workflow. Mounted on - * demand by the owning row, so state initializes fresh per open. - */ -export function ReferencesModal({ - onClose, - workspaceId, - workflowId, - workflowName, -}: ReferencesModalProps) { - const router = useRouter() - const [activeTab, setActiveTab] = useState('callers') - - const { data, isPending, isError } = useWorkflowReferences(workflowId) - - const handleNavigate = (targetId: string) => { - router.push(`/workspace/${workspaceId}/w/${targetId}`) - onClose() - } - - const nodes = data?.[activeTab] ?? [] - - return ( - !next && onClose()} srTitle='References'> - References — {workflowName} - - setActiveTab(value as ReferencesTab)} - aria-label='Reference direction' - /> - {isPending ? ( -

Loading references…

- ) : isError ? ( -

Failed to load references.

- ) : nodes.length === 0 ? ( -

{EMPTY_MESSAGE[activeTab]}

- ) : ( - - )} -
-
- ) -} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/workflow-item/workflow-item.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/workflow-item/workflow-item.tsx index 372d093b014..5eb559647d7 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/workflow-item/workflow-item.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/workflow-item/workflow-item.tsx @@ -2,15 +2,13 @@ import { memo, useCallback, useMemo, useRef, useState } from 'react' import { chipVariants, cn } from '@sim/emcn' -import { Lock } from '@sim/emcn/icons' +import { Lock, MoreHorizontal } from '@sim/emcn/icons' import clsx from 'clsx' -import { MoreHorizontal } from 'lucide-react' import Link from 'next/link' import { SIM_RESOURCES_DRAG_TYPE } from '@/lib/copilot/resource-types' import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' import { ContextMenu } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu' import { DeleteModal } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/delete-modal/delete-modal' -import { ReferencesModal } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/references-modal/references-modal' import { Avatars } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/workflow-item/avatars/avatars' import { useContextMenu, @@ -81,7 +79,6 @@ export const WorkflowItem = memo(function WorkflowItem({ const { canDeleteWorkflows, canDeleteFolder } = useCanDelete({ workspaceId }) const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false) - const [isReferencesOpen, setIsReferencesOpen] = useState(false) const [deleteItemType, setDeleteItemType] = useState<'workflow' | 'mixed'>('workflow') const [deleteModalNames, setDeleteModalNames] = useState('') const [canDeleteSelection, setCanDeleteSelection] = useState(true) @@ -400,10 +397,6 @@ export const WorkflowItem = memo(function WorkflowItem({ [shouldPreventClickRef, workflow.id, onWorkflowClick, isEditing] ) - const handleFindReferences = useCallback(() => { - setIsReferencesOpen(true) - }, []) - return ( <> - - {isReferencesOpen && ( - setIsReferencesOpen(false)} - workspaceId={workspaceId} - workflowId={workflow.id} - workflowName={workflow.name} - /> - )} ) }) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/pending-invitations/view-invitations-menu-item.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/pending-invitations/view-invitations-menu-item.tsx index 4fe2657eff8..c0d1e12e5c2 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/pending-invitations/view-invitations-menu-item.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/pending-invitations/view-invitations-menu-item.tsx @@ -22,7 +22,7 @@ export function ViewInvitationsMenuItem({ onOpen }: ViewInvitationsMenuItemProps } return ( - + View invitations ) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/workspace-header.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/workspace-header.tsx index 09839ddf978..6c76d2c69a5 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/workspace-header.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/workspace-header.tsx @@ -2,10 +2,11 @@ import { memo, type ReactElement, useEffect, useRef, useState } from 'react' import { - ChevronDown, Chip, + ChipChevronDown, ChipConfirmModal, ChipInput, + chipContentLabelClass, chipGeometryClass, chipVariants, cn, @@ -18,10 +19,9 @@ import { Skeleton, Tooltip, } from '@sim/emcn' -import { ManageWorkspace, PanelLeft } from '@sim/emcn/icons' +import { MoreHorizontal, PanelLeft, Search } from '@sim/emcn/icons' import { createLogger } from '@sim/logger' import { useQueryClient } from '@tanstack/react-query' -import { MoreHorizontal, Search } from 'lucide-react' import { isBillingEnabled } from '@/lib/core/config/env-flags' import { InviteModal } from '@/app/workspace/[workspaceId]/components/invite-modal' import { useWorkspacePermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' @@ -38,6 +38,7 @@ import { } from '@/hooks/queries/workspace' import { usePermissionConfig } from '@/hooks/use-permission-config' import { useSettingsNavigation } from '@/hooks/use-settings-navigation' +import { SIDEBAR_WIDTH } from '@/stores/constants' const logger = createLogger('WorkspaceHeader') @@ -261,6 +262,11 @@ function WorkspaceHeaderImpl({ * server refused the send. */ const { userPermissions } = useWorkspacePermissionsContext() + /** + * Derived from the `workspaces` prop rather than {@link useWorkspaceInvitePolicy}: + * this component is already handed the list it would otherwise re-read, and the + * same object supplies the logo, color, and organization below. + */ const inviteDisabledReason = activeWorkspaceFull?.inviteDisabledReason ?? null const isInvitationsDisabled = isInvitationsDisabledByConfig || inviteDisabledReason !== null @@ -493,10 +499,8 @@ function WorkspaceHeaderImpl({ )} {!isCollapsed && activeWorkspace?.name && ( <> - - {activeWorkspace.name} - - + {activeWorkspace.name} + )} @@ -507,7 +511,7 @@ function WorkspaceHeaderImpl({ sideOffset={isCollapsed ? 16 : 8} className='flex max-h-none flex-col overflow-hidden' style={{ - width: '248px', + width: `${SIDEBAR_WIDTH.DEFAULT}px`, maxWidth: 'calc(100vw - 24px)', }} onCloseAutoFocus={(e) => e.preventDefault()} @@ -595,9 +599,7 @@ function WorkspaceHeaderImpl({ } > {editingWorkspaceId === workspace.id ? ( -
+
{workspace.logoUrl ? ( -
- - - - { - setIsWorkspaceMenuOpen(false) - if (isInvitationsDisabled) { - if (isBillingEnabled) navigateToSettings({ section: 'billing' }) - return - } - setIsInviteModalOpen(true) - }} - fullWidth - flush - className='select-none' - > - Invite teammates - - - { - setIsWorkspaceMenuOpen(false) - setIsViewInvitationsOpen(true) - }} - /> - - { + + { + setIsWorkspaceMenuOpen(false) + if (isInvitationsDisabled) { + if (isBillingEnabled) navigateToSettings({ section: 'billing' }) + return + } + setIsInviteModalOpen(true) + }} + fullWidth + className='select-none' + > + Invite teammates + + + { setIsWorkspaceMenuOpen(false) - if (isInvitationsDisabled) { - if (isBillingEnabled) navigateToSettings({ section: 'billing' }) - return - } - navigateToSettings({ section: 'teammates' }) + setIsViewInvitationsOpen(true) }} - fullWidth - flush - className='select-none' - > - Manage workspace - - + /> +
)} @@ -814,11 +793,10 @@ function WorkspaceHeaderImpl({ diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/constants.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/constants.ts index 88a25c6de88..1b2df2b4460 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/constants.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/constants.ts @@ -6,11 +6,29 @@ * rhythm changes, update these values and every consumer follows. */ -/** Vertical gap between sibling sidebar sections (12px). */ -export const SIDEBAR_SECTION_GAP_CLASS = 'mt-3' +/** Vertical gap between sibling sidebar sections (16px). */ +export const SIDEBAR_SECTION_GAP_CLASS = 'mt-4' -/** Vertical gap between items within a sidebar section (2px). */ -export const SIDEBAR_ITEM_GAP_CLASS = 'gap-0.5' +/** + * Vertical gap between items within a sidebar section (1px). + * + * Written as an arbitrary value, not `gap-px`: the `px` spacing key is remapped + * to `--border-width`, which thins to 0.5px on hidpi displays so hairline borders + * stay hairlines. That is right for a rule and wrong for a gap — this one is a + * literal pixel at every density. + */ +export const SIDEBAR_ITEM_GAP_CLASS = 'gap-[1px]' + +/** + * Halves of {@link SIDEBAR_SECTION_GAP_CLASS} straddling the scroll region's + * divider: the pinned block above carries the top half, the scroll region below + * carries the bottom half. Split this way the divider sits centered in a gap that + * reads as one section gap, so the first section header is spaced from the block + * above it exactly like every other section boundary. Keep both in step with the + * section gap. + */ +export const SIDEBAR_DIVIDER_PAD_ABOVE_CLASS = 'pb-2' +export const SIDEBAR_DIVIDER_PAD_BELOW_CLASS = 'pt-2' /** * Nested-selector variants for cmdk-based surfaces (e.g. the search modal). @@ -18,7 +36,7 @@ export const SIDEBAR_ITEM_GAP_CLASS = 'gap-0.5' */ /** Matches {@link SIDEBAR_SECTION_GAP_CLASS} applied to adjacent cmdk groups. */ -export const CMDK_SECTION_GAP_CLASS = '[&_[cmdk-group]+[cmdk-group]]:mt-3' +export const CMDK_SECTION_GAP_CLASS = '[&_[cmdk-group]+[cmdk-group]]:mt-4' /** Matches {@link SIDEBAR_ITEM_GAP_CLASS} applied to cmdk item containers. */ -export const CMDK_ITEM_GAP_CLASS = '[&_[cmdk-group-items]]:gap-0.5' +export const CMDK_ITEM_GAP_CLASS = '[&_[cmdk-group-items]]:gap-[1px]' diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx index c47212fa5e5..e3f0741b269 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx @@ -20,26 +20,22 @@ import { Upload, } from '@sim/emcn' import { - BookOpen, - Calendar, Database, Files, - HelpCircle, Integration, + MoreHorizontal, PanelLeft, + Pin, Plus, Search, - Settings, Table, Task, Workflow, } from '@sim/emcn/icons' import { createLogger } from '@sim/logger' -import { MoreHorizontal, Pin } from 'lucide-react' import Link from 'next/link' import { useParams, usePathname, useRouter } from 'next/navigation' import { usePostHog } from 'posthog-js/react' -import { SlackIcon } from '@/components/icons' import { useSession } from '@/lib/auth/auth-client' import { SIM_RESOURCES_DRAG_TYPE } from '@/lib/copilot/resource-types' import { isChatEnabled } from '@/lib/core/config/env-flags' @@ -48,6 +44,7 @@ import { buildFolderTree, getFolderPathNames } from '@/lib/folders/tree' import { captureEvent } from '@/lib/posthog/client' import { useRegisterGlobalCommands } from '@/app/workspace/[workspaceId]/providers/global-commands-provider' import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' +import type { SettingsSection } from '@/app/workspace/[workspaceId]/settings/navigation' import { createCommands } from '@/app/workspace/[workspaceId]/utils/commands-utils' import { CollapsedChatFlyoutItem, @@ -58,6 +55,8 @@ import { NavItemContextMenu, SearchModal, SettingsSidebar, + SidebarFooter, + SidebarSection, WorkflowList, WorkspaceHeader, } from '@/app/workspace/[workspaceId]/w/components/sidebar/components' @@ -68,6 +67,8 @@ import { import { ContextMenu } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu' import { DeleteModal } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/delete-modal/delete-modal' import { + SIDEBAR_DIVIDER_PAD_ABOVE_CLASS, + SIDEBAR_DIVIDER_PAD_BELOW_CLASS, SIDEBAR_ITEM_GAP_CLASS, SIDEBAR_SECTION_GAP_CLASS, } from '@/app/workspace/[workspaceId]/w/components/sidebar/constants' @@ -155,9 +156,10 @@ export function SidebarTooltip({ ) } +/** Stands in for a chip row while a list loads, so it carries no margin either. */ function SidebarItemSkeleton() { return ( -
+
) @@ -268,7 +270,7 @@ const SidebarChatItem = memo(function SidebarChatItem({ isMenuOpen && 'opacity-100' )} > - +
)} @@ -355,6 +357,13 @@ export const SIDEBAR_SCROLL_EVENT = 'sidebar-scroll-to-item' const HIDDEN_STYLE = { display: 'none' } as const +/** + * Opts a control out of the desktop shell's window-drag region. The header row is + * draggable chrome, so anything clickable inside it has to say so or the click is + * swallowed by the drag handler. + */ +const DRAG_EXEMPT_CLASS = '[-webkit-app-region:no-drag]' + /** * Sidebar component with resizable width that persists across page refreshes. * @@ -404,7 +413,7 @@ export const Sidebar = memo(function Sidebar({ const { data: sessionData, isPending: sessionLoading } = useSession() const { canEdit, isLoading: permissionsLoading } = useUserPermissionsContext() const { config: permissionConfig, filterBlocks } = usePermissionConfig() - const { navigateToSettings, getSettingsHref } = useSettingsNavigation() + const { navigateToSettings } = useSettingsNavigation() const initializeSearchData = useSearchModalStore((state) => state.initializeData) const customBlockOverlayVersion = useCustomBlockOverlayVersion() const providers = useProvidersStore((state) => state.providers) @@ -759,29 +768,18 @@ export const Sidebar = memo(function Sidebar({ // on a workflow the server declined to create. hidden: !isChatEnabled && !permissionsLoading && !canEdit, }, - { - id: 'search', - label: 'Search', - icon: Search, - onClick: openSearchModal, - }, { id: 'integrations', label: 'Integrations', icon: Integration, href: `/workspace/${workspaceId}/integrations`, + /* Skills is a tab of this surface, not its own nav item — keep the entry + lit while the user is on it. */ additionalActivePaths: [`/workspace/${workspaceId}/skills`], hidden: permissionConfig.hideIntegrationsTab, }, ].filter((item) => !item.hidden), - [ - workspaceId, - openSearchModal, - createWorkflow, - canEdit, - permissionsLoading, - permissionConfig.hideIntegrationsTab, - ] + [workspaceId, createWorkflow, canEdit, permissionsLoading, permissionConfig.hideIntegrationsTab] ) const workspaceNavItems = useMemo( @@ -808,13 +806,6 @@ export const Sidebar = memo(function Sidebar({ href: `/workspace/${workspaceId}/knowledge`, hidden: permissionConfig.hideKnowledgeBaseTab, }, - { - id: 'scheduled-tasks', - label: 'Scheduled tasks', - icon: Calendar, - href: `/workspace/${workspaceId}/scheduled-tasks`, - hidden: !isChatEnabled, - }, { id: 'logs', label: 'Logs', @@ -830,22 +821,14 @@ export const Sidebar = memo(function Sidebar({ ] ) - const footerItems = useMemo( - () => [ - { - id: 'settings', - label: 'Settings', - icon: Settings, - href: getSettingsHref(), - onClick: () => { - if (!isCollapsedRef.current) { - setSidebarWidth(SIDEBAR_WIDTH.MIN) - } - navigateToSettings() - }, - }, - ], - [navigateToSettings, getSettingsHref, setSidebarWidth] + const handleOpenSettings = useCallback( + (section: SettingsSection) => { + if (!isCollapsedRef.current) { + setSidebarWidth(SIDEBAR_WIDTH.MIN) + } + navigateToSettings({ section }) + }, + [navigateToSettings, setSidebarWidth] ) const { data: fetchedChats = EMPTY_CHATS, isLoading: chatsLoading } = useMothershipChats( @@ -1051,7 +1034,6 @@ export const Sidebar = memo(function Sidebar({ ) const [hasOverflowTop, setHasOverflowTop] = useState(false) - const [hasOverflowBottom, setHasOverflowBottom] = useState(false) useEffect(() => { const container = scrollContainerRef.current @@ -1059,9 +1041,6 @@ export const Sidebar = memo(function Sidebar({ const updateScrollState = () => { setHasOverflowTop(container.scrollTop > 1) - setHasOverflowBottom( - container.scrollHeight > container.scrollTop + container.clientHeight + 1 - ) } updateScrollState() @@ -1368,25 +1347,57 @@ export const Sidebar = memo(function Sidebar({ isCollapsed={isCollapsed} onExpandSidebar={toggleCollapsed} /> - - - + + +
{isOnSettingsPage ? ( @@ -1400,7 +1411,8 @@ export const Sidebar = memo(function Sidebar({ className={cn( SIDEBAR_SECTION_GAP_CLASS, SIDEBAR_ITEM_GAP_CLASS, - 'flex flex-shrink-0 flex-col px-2 pb-1.5' + SIDEBAR_DIVIDER_PAD_ABOVE_CLASS, + 'flex flex-shrink-0 flex-col px-2' )} > {topNavItems.map((item) => ( @@ -1417,22 +1429,23 @@ export const Sidebar = memo(function Sidebar({
{isChatEnabled && ( -
-
-
Chats
-
+ {isCollapsed ? ( {chatsLoading ? ( @@ -1463,7 +1476,7 @@ export const Sidebar = memo(function Sidebar({ )} ) : ( -
+
{chatsLoading ? ( ) : ( @@ -1474,10 +1487,10 @@ export const Sidebar = memo(function Sidebar({
) : null} {/* `selectChatOnly` populates `selectedChats` on every click, so - a single entry just means "last clicked" — already conveyed by - `isCurrentRoute`. Highlight from selection only for explicit - multi-selection (size > 1), otherwise it lingers after navigating - away from a chat. */} + a single entry just means "last clicked" — already conveyed by + `isCurrentRoute`. Highlight from selection only for explicit + multi-selection (size > 1), otherwise it lingers after navigating + away from a chat. */} {chats.slice(0, visibleChatCount).map((chat) => { const isCurrentRoute = pathname === chat.href const isRenaming = chatFlyoutRename.editingId === chat.id @@ -1544,13 +1557,14 @@ export const Sidebar = memo(function Sidebar({ )}
)} -
+ )} -
-
-
Workspace
-
+
{workspaceNavItems.map((item) => ( ))}
-
- -
-
-
Workflows
- {!isCollapsed && ( + + + @@ -1580,13 +1591,13 @@ export const Sidebar = memo(function Sidebar({ @@ -1620,7 +1631,7 @@ export const Sidebar = memo(function Sidebar({
- )} -
+ ) + } + > {isCollapsed ? ( {workflowsLoading && regularWorkflows.length === 0 ? ( @@ -1701,7 +1712,7 @@ export const Sidebar = memo(function Sidebar({ )} ) : ( -
+
{workflowsLoading && regularWorkflows.length === 0 ? ( ) : ( @@ -1719,58 +1730,19 @@ export const Sidebar = memo(function Sidebar({ )}
)} -
+
-
- - - - - - - - - - Docs - - - - Slack Community - - - - Report an issue - - - - - {footerItems.map((item) => ( - - ))} -
+ ({ // does not re-export. Widen it rather than rewriting the source's imports. vi.mock('@sim/db', () => ({ ...databaseMock, ...schemaMock })) -import { - applyScheduleFailureUpdate, - readScheduledMothershipErrorResponse, - readScheduledMothershipJsonResponse, - releaseScheduleLock, -} from '@/background/schedule-execution' -import type { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' +import { applyScheduleFailureUpdate, releaseScheduleLock } from '@/background/schedule-execution' const BASE = { scheduleId: 'schedule-1', @@ -105,71 +99,3 @@ describe('releaseScheduleLock', () => { expect(notifyMock).not.toHaveBeenCalled() }) }) - -describe('scheduled Mothership response handling', () => { - it('parses JSON responses', async () => { - const response = new Response(JSON.stringify({ content: 'ok' }), { - headers: { 'content-type': 'application/json' }, - }) - - await expect(readScheduledMothershipJsonResponse(response)).resolves.toEqual({ content: 'ok' }) - }) - - it('preserves staging acceptance for responses above the generic tool cap', async () => { - const response = new Response(JSON.stringify({ content: 'unchanged' }), { - headers: { - 'content-length': String(10 * 1024 * 1024 + 1), - 'content-type': 'application/json', - }, - }) - - await expect(readScheduledMothershipJsonResponse(response)).resolves.toEqual({ - content: 'unchanged', - }) - }) - - it('does not include malformed response content in parse failures', async () => { - const response = new Response('secret-bearing-non-json') - const error = await readScheduledMothershipJsonResponse(response).catch((caught) => caught) - - expect(error).toBeInstanceOf(Error) - expect(error.message).toBe('Sim execution returned an invalid response') - expect(error.message).not.toContain('secret-bearing-non-json') - }) - - it('preserves the functional error body when no private metadata is present', async () => { - const registry = { - markIncomplete: vi.fn(), - importProvenance: vi.fn(), - } as unknown as ResolvedSecretTraceRegistry - const response = new Response('secret-bearing-error-body', { status: 500 }) - - const message = await readScheduledMothershipErrorResponse(response, registry) - - expect(message).toBe('secret-bearing-error-body') - expect(registry.markIncomplete).toHaveBeenCalledTimes(1) - }) - - it('strips private provenance metadata without replacing the provider error', async () => { - const registry = { - markIncomplete: vi.fn(), - importProvenance: vi.fn().mockReturnValue(true), - } as unknown as ResolvedSecretTraceRegistry - const response = new Response( - JSON.stringify({ - error: 'provider error detail', - __resolvedSecretTraceProvenance: { version: 1, complete: true, entries: [] }, - }), - { - status: 500, - headers: { 'x-sim-private-tool-metadata': 'resolved-secret-provenance-v1' }, - } - ) - - const message = await readScheduledMothershipErrorResponse(response, registry) - - expect(JSON.parse(message)).toEqual({ error: 'provider error detail' }) - expect(message).not.toContain('__resolvedSecretTraceProvenance') - expect(registry.importProvenance).toHaveBeenCalledOnce() - }) -}) diff --git a/apps/sim/background/schedule-execution.ts b/apps/sim/background/schedule-execution.ts index 53f0fced6ed..cc49beba638 100644 --- a/apps/sim/background/schedule-execution.ts +++ b/apps/sim/background/schedule-execution.ts @@ -1,24 +1,14 @@ import { trace } from '@opentelemetry/api' -import { - db, - jobExecutionLogs, - workflow, - workflowDeploymentVersion, - workflowSchedule, -} from '@sim/db' +import { db, workflow, workflowDeploymentVersion, workflowSchedule } from '@sim/db' import { createLogger, runWithRequestContext } from '@sim/logger' import { describeError, toError } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' -import { isPlainRecord } from '@sim/utils/object' import { task } from '@trigger.dev/sdk' import { Cron } from 'croner' import { and, eq, isNull, ne, type SQL, sql } from 'drizzle-orm' import { assertBillingAttributionSnapshot, - BILLING_ATTRIBUTION_HEADER, type BillingAttributionSnapshot, - resolveBillingAttribution, - serializeBillingAttributionHeader, } from '@/lib/billing/core/billing-attribution' import { classifyTransientAdmissionFailure } from '@/lib/core/admission/transient-failure' import type { AsyncExecutionCorrelation } from '@/lib/core/async-jobs/types' @@ -26,23 +16,11 @@ import { describeRetryableInfrastructureError, isRetryableInfrastructureError, } from '@/lib/core/errors/retryable-infrastructure' -import { - createTimeoutAbortController, - getExecutionTimeout, - getTimeoutErrorMessage, -} from '@/lib/core/execution-limits' +import { createTimeoutAbortController, getTimeoutErrorMessage } from '@/lib/core/execution-limits' import type { DbOrTx } from '@/lib/db/types' import { preprocessExecution } from '@/lib/execution/preprocessing' -import { - PRIVATE_TOOL_METADATA_REQUEST_HEADER, - RESOLVED_SECRET_PROVENANCE_FIELD, - RESOLVED_SECRET_PROVENANCE_METADATA_V1, - responseHasPrivateToolMetadata, -} from '@/lib/execution/private-tool-metadata' import { LoggingSession } from '@/lib/logs/execution/logging-session' -import { projectTraceSpansForSecrets } from '@/lib/logs/execution/trace-secret-projection' import { buildTraceSpans } from '@/lib/logs/execution/trace-spans/trace-spans' -import type { TraceSpan } from '@/lib/logs/types' import { cleanupExecutionBase64Cache } from '@/lib/uploads/utils/user-file-base64.server' import { executeWorkflowCore, @@ -61,16 +39,12 @@ import { calculateScheduleInfraRetryDelayMs } from '@/lib/workflows/schedules/re import { type BlockState, calculateNextRunTime as calculateNextTime, - computeNextRunAt, getScheduleTimeValues, getSubBlockValue, } from '@/lib/workflows/schedules/utils' -import { getWorkspaceById } from '@/lib/workspaces/permissions/utils' import { ExecutionSnapshot } from '@/executor/execution/snapshot' import type { ExecutionMetadata } from '@/executor/execution/types' import { hasExecutionResult } from '@/executor/utils/errors' -import { buildAPIUrl, buildAuthHeaders } from '@/executor/utils/http' -import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' import { MAX_CONSECUTIVE_FAILURES } from '@/triggers/constants' const logger = createLogger('ScheduleExecution') @@ -162,7 +136,6 @@ async function applyScheduleUpdate( context: string, options: { expectedLastQueuedAt?: Date | null - allowCompleted?: boolean /** * Set at call sites that can transition the row to `disabled`. Presence both * opts the site into the auto-disable email and adds a `status <> 'disabled'` @@ -185,16 +158,10 @@ async function applyScheduleUpdate( ? isNull(workflowSchedule.lastQueuedAt) : eq(workflowSchedule.lastQueuedAt, options.expectedLastQueuedAt) - // A run that completes itself mid-execution (complete_scheduled_task, or a - // manage_scheduled_task update) sets status='completed'. The post-run - // bookkeeping that follows would otherwise write status='active' and a - // fresh nextRunAt straight back over it — the claim guard does not catch - // this, because completing the job does not touch lastQueuedAt. Terminal - // means terminal: only callers that explicitly opt in may move a completed - // row. - const notCompletedGuard = options.allowCompleted - ? undefined - : ne(workflowSchedule.status, 'completed') + // Terminal means terminal: a completed row is never moved back to active + // with a fresh nextRunAt. The claim guard does not cover this on its own, + // because reaching 'completed' does not touch lastQueuedAt. + const notCompletedGuard = ne(workflowSchedule.status, 'completed') /** * `RETURNING` yields the NEW row, so `status === 'disabled'` alone only means @@ -1129,571 +1096,6 @@ export async function executeScheduleJob(payload: ScheduleExecutionPayload) { }) } -export type JobExecutionPayload = { - scheduleId: string - cronExpression?: string - failedCount?: number - now: string -} - -function buildJobPrompt(jobRecord: { - id: string - jobTitle: string | null - prompt: string | null - lifecycle: string - successCondition: string | null - runCount: number - maxRuns: number | null - sourceTaskName: string | null - sourceChatId: string | null - jobHistory: Array<{ timestamp: string; summary: string }> | null -}): string { - const parts: string[] = [] - - parts.push('--- JOB EXECUTION ---') - parts.push(`Job ID: ${jobRecord.id}`) - if (jobRecord.jobTitle) parts.push(`Title: ${jobRecord.jobTitle}`) - - if (jobRecord.lifecycle === 'until_complete') { - parts.push(`Lifecycle: until_complete`) - if (jobRecord.successCondition) { - parts.push(`Success Condition: ${jobRecord.successCondition}`) - } - const runDisplay = jobRecord.maxRuns - ? `${jobRecord.runCount + 1} / ${jobRecord.maxRuns}` - : `${jobRecord.runCount + 1}` - parts.push(`Run: ${runDisplay}`) - } - - parts.push('') - parts.push('TASK:') - parts.push(jobRecord.prompt || '') - - if (jobRecord.sourceTaskName) { - parts.push('') - parts.push(`RELATED TASK: ${jobRecord.sourceTaskName}`) - } - - if (jobRecord.sourceChatId) { - parts.push("Read the task's session.md in the VFS for conversation context.") - } - - if (jobRecord.jobHistory && jobRecord.jobHistory.length > 0) { - parts.push('') - parts.push('PREVIOUS RUN HISTORY (for idempotency -- do NOT reprocess items already handled):') - const recentHistory = jobRecord.jobHistory.slice(-10) - for (const entry of recentHistory) { - parts.push(`- [${entry.timestamp}] ${entry.summary}`) - } - parts.push('') - parts.push( - 'Use this history to avoid duplicate work. After completing meaningful work this run, call update_scheduled_task_history to record what you did.' - ) - } else if (jobRecord.runCount > 0) { - parts.push('') - parts.push( - 'No previous run history recorded. After completing meaningful work, call update_scheduled_task_history to record what you did for future runs.' - ) - } else { - parts.push('') - parts.push( - 'This is the first run. After completing meaningful work, call update_scheduled_task_history to record what you did so future runs have context.' - ) - } - - if (jobRecord.lifecycle === 'until_complete') { - parts.push('') - parts.push('COMPLETION PROTOCOL:') - parts.push('This is a poll-until-done job. After executing the task above:') - parts.push( - `- If the success condition is met, take the required action, then call complete_scheduled_task(jobId: "${jobRecord.id}") to stop the scheduled task.` - ) - parts.push( - '- If the success condition is NOT met, do nothing extra. The job will run again on schedule.' - ) - } - - parts.push('--- END JOB EXECUTION ---') - - return parts.join('\n') -} - -async function consumeJobTraceProvenance( - response: Response, - payload: Record, - registry: ResolvedSecretTraceRegistry -): Promise { - const hasProvenance = Object.hasOwn(payload, RESOLVED_SECRET_PROVENANCE_FIELD) - const provenance = payload[RESOLVED_SECRET_PROVENANCE_FIELD] - delete payload[RESOLVED_SECRET_PROVENANCE_FIELD] - - if ( - !responseHasPrivateToolMetadata(response.headers, RESOLVED_SECRET_PROVENANCE_METADATA_V1) || - !hasProvenance - ) { - registry.markIncomplete() - return false - } - - return registry.importProvenance(provenance, { trusted: true }) -} - -/** Reads and validates a scheduled Mothership JSON response without changing its size contract. */ -export async function readScheduledMothershipJsonResponse( - response: Response -): Promise> { - let payload: unknown - - try { - payload = await response.json() - } catch { - throw new Error('Sim execution returned an invalid response') - } - - if (!isPlainRecord(payload)) { - throw new Error('Sim execution returned an invalid response') - } - return payload -} - -/** Reads the functional error body and strips private provenance metadata when present. */ -export async function readScheduledMothershipErrorResponse( - response: Response, - registry: ResolvedSecretTraceRegistry -): Promise { - const responseText = await response.text() - const hasPrivateMarker = responseHasPrivateToolMetadata( - response.headers, - RESOLVED_SECRET_PROVENANCE_METADATA_V1 - ) - const mayContainPrivateProvenance = responseText.includes(`"${RESOLVED_SECRET_PROVENANCE_FIELD}"`) - - if (!hasPrivateMarker && !mayContainPrivateProvenance) { - registry.markIncomplete() - return responseText - } - - let payload: unknown - - try { - payload = JSON.parse(responseText) - } catch { - registry.markIncomplete() - return responseText - } - - if (!isPlainRecord(payload)) { - registry.markIncomplete() - return responseText - } - - const hadPrivateProvenance = Object.hasOwn(payload, RESOLVED_SECRET_PROVENANCE_FIELD) - try { - await consumeJobTraceProvenance(response, payload, registry) - } catch { - registry.markIncomplete() - } - return hadPrivateProvenance ? JSON.stringify(payload) : responseText -} - -async function createJobLogEntry(params: { - scheduleId: string - workspaceId: string - userId: string - jobTitle: string | null - startTime: Date - endTime: Date - durationMs: number - success: boolean - resolvedSecretTraceRegistry: ResolvedSecretTraceRegistry - responseBody?: Record - errorMessage?: string -}): Promise { - try { - const { - scheduleId, - workspaceId, - userId, - jobTitle, - startTime, - endTime, - durationMs, - success, - resolvedSecretTraceRegistry, - responseBody, - } = params - const name = jobTitle || 'Sim Job' - const executionId = generateId() - - const toolCallsList = (responseBody?.toolCalls || []).map((tc: Record) => ({ - name: tc.name, - input: tc.params || {}, - output: tc.result - ? typeof tc.result === 'object' - ? tc.result - : { result: tc.result } - : undefined, - error: tc.error, - duration: (tc.durationMs as number) || 0, - startTime: startTime.toISOString(), - endTime: endTime.toISOString(), - status: tc.error ? 'error' : 'success', - })) - - const traceSpan: TraceSpan = { - id: generateId(), - name, - type: 'mothership', - duration: durationMs, - startTime: startTime.toISOString(), - endTime: endTime.toISOString(), - status: success ? 'success' : 'error', - output: { - content: responseBody?.content || '', - model: responseBody?.model || 'mothership', - tokens: responseBody?.tokens || {}, - }, - toolCalls: toolCallsList.length > 0 ? toolCallsList : undefined, - cost: responseBody?.cost || undefined, - tokens: responseBody?.tokens || undefined, - } - const [projectedTraceSpan] = await projectTraceSpansForSecrets([traceSpan], { - registry: resolvedSecretTraceRegistry, - store: { - workspaceId, - executionId, - userId, - }, - }) - - await db.insert(jobExecutionLogs).values({ - id: generateId(), - scheduleId, - workspaceId, - executionId, - level: success ? 'info' : 'error', - status: success ? 'completed' : 'failed', - trigger: 'mothership', - startedAt: startTime, - endedAt: endTime, - totalDurationMs: durationMs, - executionData: { - enhanced: true, - traceSpans: [projectedTraceSpan], - finalOutput: responseBody?.content ? { content: responseBody.content } : undefined, - executionState: { - resolvedSecretTraceProvenance: resolvedSecretTraceRegistry.exportProvenance(), - }, - trigger: { - type: 'mothership', - source: name, - timestamp: startTime.toISOString(), - }, - }, - cost: responseBody?.cost - ? { - total: responseBody.cost.total || 0, - input: responseBody.cost.input || 0, - output: responseBody.cost.output || 0, - tokens: responseBody.tokens || {}, - } - : null, - }) - } catch (error) { - logger.error('Failed to create job log entry', { - error: toError(error).message, - }) - } -} - -export async function executeJobInline(payload: JobExecutionPayload) { - const requestId = generateId().slice(0, 8) - const now = new Date(payload.now) - - logger.info(`[${requestId}] Starting job execution`, { scheduleId: payload.scheduleId }) - - const [jobRecord] = await db - .select() - .from(workflowSchedule) - .where(and(eq(workflowSchedule.id, payload.scheduleId), isNull(workflowSchedule.archivedAt))) - .limit(1) - - if (!jobRecord || !jobRecord.prompt || !jobRecord.sourceUserId || !jobRecord.sourceWorkspaceId) { - logger.error(`[${requestId}] Job record missing required fields`, { - scheduleId: payload.scheduleId, - }) - await releaseScheduleLock( - payload.scheduleId, - requestId, - now, - `Failed to release job ${payload.scheduleId} after missing fields`, - undefined, - { expectedLastQueuedAt: now } - ) - return - } - - if (!jobRecord.lastQueuedAt || jobRecord.lastQueuedAt.getTime() !== now.getTime()) { - logger.info(`[${requestId}] Job claim no longer matches payload, skipping execution`, { - scheduleId: payload.scheduleId, - claimedAt: now.toISOString(), - currentLastQueuedAt: jobRecord.lastQueuedAt?.toISOString(), - }) - return - } - - const activeWorkspace = await getWorkspaceById(jobRecord.sourceWorkspaceId) - if (!activeWorkspace || jobRecord.status === 'disabled') { - logger.info(`[${requestId}] Job is archived, disabled, or workspace is inactive`, { - scheduleId: payload.scheduleId, - }) - await releaseScheduleLock( - payload.scheduleId, - requestId, - now, - `Failed to release job ${payload.scheduleId} after archive/disabled check`, - undefined, - { expectedLastQueuedAt: now } - ) - return - } - - if (jobRecord.status === 'completed') { - logger.info(`[${requestId}] Job already completed, skipping`, { - scheduleId: payload.scheduleId, - }) - await releaseScheduleLock( - payload.scheduleId, - requestId, - now, - `Failed to release job ${payload.scheduleId} after completed skip`, - undefined, - { expectedLastQueuedAt: now } - ) - return - } - - const promptText = buildJobPrompt(jobRecord) - const resolvedSecretTraceRegistry = new ResolvedSecretTraceRegistry([], { - userId: jobRecord.sourceUserId, - workspaceId: jobRecord.sourceWorkspaceId, - }) - - try { - const billingAttribution = await resolveBillingAttribution({ - actorUserId: jobRecord.sourceUserId, - workspaceId: jobRecord.sourceWorkspaceId, - }) - const mothershipJobTimeoutMs = getExecutionTimeout( - billingAttribution.payerSubscription?.plan, - 'sync' - ) - const url = buildAPIUrl('/api/mothership/execute') - const headers = await buildAuthHeaders(jobRecord.sourceUserId) - headers[BILLING_ATTRIBUTION_HEADER] = serializeBillingAttributionHeader(billingAttribution) - headers[PRIVATE_TOOL_METADATA_REQUEST_HEADER] = RESOLVED_SECRET_PROVENANCE_METADATA_V1 - - const body = { - messages: [{ role: 'user', content: promptText }], - workspaceId: jobRecord.sourceWorkspaceId, - userId: jobRecord.sourceUserId, - chatId: jobRecord.sourceChatId || generateId(), - secretScope: jobRecord.secretScope, - mountedSecrets: jobRecord.mountedSecrets, - ...(jobRecord.contexts && jobRecord.contexts.length > 0 - ? { contexts: jobRecord.contexts } - : {}), - } - - const startTime = new Date() - const timeoutController = createTimeoutAbortController(mothershipJobTimeoutMs) - try { - const response = await fetch(url.toString(), { - method: 'POST', - headers, - body: JSON.stringify(body), - signal: timeoutController.signal, - }) - - if (!response.ok) { - const errorText = await readScheduledMothershipErrorResponse( - response, - resolvedSecretTraceRegistry - ).catch(() => { - resolvedSecretTraceRegistry.markIncomplete() - if (timeoutController.isTimedOut()) { - throw new Error(getTimeoutErrorMessage(null, timeoutController.timeoutMs)) - } - return 'Unknown error' - }) - const endTime = new Date() - const durationMs = endTime.getTime() - startTime.getTime() - - await createJobLogEntry({ - scheduleId: payload.scheduleId, - workspaceId: jobRecord.sourceWorkspaceId, - userId: jobRecord.sourceUserId, - jobTitle: jobRecord.jobTitle, - startTime, - endTime, - durationMs, - success: false, - resolvedSecretTraceRegistry, - errorMessage: errorText, - }) - - throw new Error(`Sim execution failed (${response.status}): ${errorText}`) - } - - let responseBody: Record = {} - let wasCompletedByTool = false - try { - const payload = await readScheduledMothershipJsonResponse(response) - responseBody = payload - try { - await consumeJobTraceProvenance(response, payload, resolvedSecretTraceRegistry) - } catch { - resolvedSecretTraceRegistry.markIncomplete() - } - const toolCalls = responseBody?.toolCalls as Array<{ name?: string }> | undefined - wasCompletedByTool = toolCalls?.some((tc) => tc.name === 'complete_scheduled_task') ?? false - } catch { - resolvedSecretTraceRegistry.markIncomplete() - if (timeoutController.isTimedOut()) { - throw new Error(getTimeoutErrorMessage(null, timeoutController.timeoutMs)) - } - } - const endTime = new Date() - const durationMs = endTime.getTime() - startTime.getTime() - - await createJobLogEntry({ - scheduleId: payload.scheduleId, - workspaceId: jobRecord.sourceWorkspaceId, - userId: jobRecord.sourceUserId, - jobTitle: jobRecord.jobTitle, - startTime, - endTime, - durationMs, - success: true, - resolvedSecretTraceRegistry, - responseBody, - }) - - const newRunCount = (jobRecord.runCount || 0) + 1 - - logger.info(`[${requestId}] Job executed successfully`, { - scheduleId: payload.scheduleId, - runCount: newRunCount, - wasCompletedByTool, - }) - - if (wasCompletedByTool) { - await applyScheduleUpdate( - payload.scheduleId, - { - lastRanAt: now, - updatedAt: now, - runCount: newRunCount, - failedCount: 0, - lastQueuedAt: null, - }, - requestId, - `Error updating job ${payload.scheduleId} after completion`, - // The tool already set status='completed'; this is bookkeeping on a - // deliberately terminal row, so it opts past the not-completed guard. - { expectedLastQueuedAt: now, allowCompleted: true } - ) - return - } - - const isOneTime = !jobRecord.cronExpression - let nextRunAt: Date | null = null - - if (!isOneTime && jobRecord.cronExpression) { - nextRunAt = computeNextRunAt({ - cronExpression: jobRecord.cronExpression, - timezone: jobRecord.timezone || 'UTC', - from: now, - excludedDates: jobRecord.excludedDates, - endsAt: jobRecord.endsAt, - }) - } - - const maxRunsReached = Boolean(jobRecord.maxRuns && newRunCount >= jobRecord.maxRuns) - if (maxRunsReached) { - logger.info(`[${requestId}] Job hit maxRuns limit`, { - scheduleId: payload.scheduleId, - maxRuns: jobRecord.maxRuns, - runCount: newRunCount, - }) - } - - const isComplete = isOneTime || maxRunsReached || !nextRunAt - - await applyScheduleUpdate( - payload.scheduleId, - { - lastRanAt: now, - updatedAt: now, - nextRunAt: isComplete ? null : nextRunAt, - failedCount: 0, - lastQueuedAt: null, - runCount: newRunCount, - status: isComplete ? 'completed' : 'active', - }, - requestId, - `Error updating job ${payload.scheduleId} after success`, - { expectedLastQueuedAt: now } - ) - } finally { - timeoutController.cleanup() - } - } catch (error) { - const errorMessage = toError(error).message - logger.error(`[${requestId}] Job execution failed`, { - scheduleId: payload.scheduleId, - error: errorMessage, - }) - - const newFailedCount = (payload.failedCount || 0) + 1 - const shouldDisable = newFailedCount >= MAX_CONSECUTIVE_FAILURES - const newRunCount = (jobRecord.runCount || 0) + 1 - - let nextRunAt: Date | null = null - if (jobRecord.cronExpression) { - nextRunAt = computeNextRunAt({ - cronExpression: jobRecord.cronExpression, - timezone: jobRecord.timezone || 'UTC', - from: now, - excludedDates: jobRecord.excludedDates, - endsAt: jobRecord.endsAt, - }) - } - - const maxRunsReached = Boolean(jobRecord.maxRuns && newRunCount >= jobRecord.maxRuns) - const isComplete = !jobRecord.cronExpression || maxRunsReached || !nextRunAt - - await applyScheduleUpdate( - payload.scheduleId, - { - updatedAt: now, - nextRunAt: shouldDisable || !isComplete ? nextRunAt : null, - failedCount: newFailedCount, - lastFailedAt: now, - lastQueuedAt: null, - runCount: newRunCount, - status: shouldDisable ? 'disabled' : isComplete ? 'completed' : 'active', - }, - requestId, - `Error updating job ${payload.scheduleId} after failure`, - { - expectedLastQueuedAt: now, - disableReason: shouldDisable ? 'consecutive_failures' : undefined, - } - ) - } -} - export const scheduleExecutionTaskOptions = { id: 'schedule-execution', machine: 'medium-2x' as const, diff --git a/apps/sim/blocks/blocks/chat_trigger.ts b/apps/sim/blocks/blocks/chat_trigger.ts index f4379575407..ec1745beed4 100644 --- a/apps/sim/blocks/blocks/chat_trigger.ts +++ b/apps/sim/blocks/blocks/chat_trigger.ts @@ -1,10 +1,6 @@ -import type { SVGProps } from 'react' -import { createElement } from 'react' -import { MessageCircle } from 'lucide-react' +import { Task } from '@sim/emcn/icons' import type { BlockConfig } from '@/blocks/types' -const ChatTriggerIcon = (props: SVGProps) => createElement(MessageCircle, props) - export const ChatTriggerBlock: BlockConfig = { type: 'chat_trigger', triggerAllowed: true, @@ -18,7 +14,7 @@ export const ChatTriggerBlock: BlockConfig = { hideFromToolbar: true, sunset: { status: 'legacy', replacedBy: 'start_trigger' }, bgColor: '#6F3DFA', - icon: ChatTriggerIcon, + icon: Task, subBlocks: [], tools: { access: [], diff --git a/apps/sim/blocks/blocks/clickhouse.ts b/apps/sim/blocks/blocks/clickhouse.ts index 8f09b3934f4..3bfd6735fb9 100644 --- a/apps/sim/blocks/blocks/clickhouse.ts +++ b/apps/sim/blocks/blocks/clickhouse.ts @@ -1,13 +1,4 @@ -import { - Bell, - ClipboardList, - Database, - File, - Search, - Server, - TrashOutline, - Wrench, -} from '@sim/emcn/icons' +import { Bell, ClipboardList, Database, File, Search, Server, Trash, Wrench } from '@sim/emcn/icons' import { getErrorMessage } from '@sim/utils/errors' import { ClickHouseIcon } from '@/components/icons' import type { BlockConfig, BlockMeta } from '@/blocks/types' @@ -525,7 +516,7 @@ export const ClickHouseBlockMeta = { tags: ['data-warehouse', 'maintenance'], }, { - icon: TrashOutline, + icon: Trash, title: 'ClickHouse partition cleanup', prompt: 'Build a scheduled workflow that enforces a retention policy on my ClickHouse events table: take an explicit cutoff date as input, list the table partitions, select only the partitions on that exact table whose range ends strictly before the cutoff, and drop just those. Never infer the cutoff and never drop a partition that is not clearly past it.', diff --git a/apps/sim/blocks/blocks/generic_webhook.ts b/apps/sim/blocks/blocks/generic_webhook.ts index 6d83b3860ed..6aa5b8de995 100644 --- a/apps/sim/blocks/blocks/generic_webhook.ts +++ b/apps/sim/blocks/blocks/generic_webhook.ts @@ -1,17 +1,13 @@ -import type { SVGProps } from 'react' -import { createElement } from 'react' -import { Webhook } from 'lucide-react' +import { Webhook } from '@sim/emcn/icons' import type { BlockConfig } from '@/blocks/types' import { getTrigger } from '@/triggers' -const WebhookIcon = (props: SVGProps) => createElement(Webhook, props) - export const GenericWebhookBlock: BlockConfig = { type: 'generic_webhook', name: 'Webhook Trigger', description: 'Receive webhooks from any service by configuring a custom webhook.', category: 'triggers', - icon: WebhookIcon, + icon: Webhook, bgColor: '#10B981', // Green color for triggers docsLink: 'https://docs.sim.ai/workflows/triggers/webhook', triggerAllowed: true, diff --git a/apps/sim/blocks/blocks/input_trigger.ts b/apps/sim/blocks/blocks/input_trigger.ts index c2891e5fa58..e7a5675fd8a 100644 --- a/apps/sim/blocks/blocks/input_trigger.ts +++ b/apps/sim/blocks/blocks/input_trigger.ts @@ -1,10 +1,6 @@ -import type { SVGProps } from 'react' -import { createElement } from 'react' -import { FormInput } from 'lucide-react' +import { FormInput } from '@sim/emcn/icons' import type { BlockConfig } from '@/blocks/types' -const InputTriggerIcon = (props: SVGProps) => createElement(FormInput, props) - export const InputTriggerBlock: BlockConfig = { type: 'input_trigger', triggerAllowed: true, @@ -21,7 +17,7 @@ export const InputTriggerBlock: BlockConfig = { hideFromToolbar: true, sunset: { status: 'legacy', replacedBy: 'start_trigger' }, bgColor: '#3B82F6', - icon: InputTriggerIcon, + icon: FormInput, subBlocks: [ { id: 'inputFormat', diff --git a/apps/sim/blocks/blocks/manual_trigger.ts b/apps/sim/blocks/blocks/manual_trigger.ts index 6ba153886f6..3194a100dbb 100644 --- a/apps/sim/blocks/blocks/manual_trigger.ts +++ b/apps/sim/blocks/blocks/manual_trigger.ts @@ -1,10 +1,6 @@ -import type { SVGProps } from 'react' -import { createElement } from 'react' -import { Play } from 'lucide-react' +import { Play } from '@sim/emcn/icons' import type { BlockConfig } from '@/blocks/types' -const ManualTriggerIcon = (props: SVGProps) => createElement(Play, props) - export const ManualTriggerBlock: BlockConfig = { type: 'manual_trigger', triggerAllowed: true, @@ -20,7 +16,7 @@ export const ManualTriggerBlock: BlockConfig = { hideFromToolbar: true, sunset: { status: 'legacy', replacedBy: 'start_trigger' }, bgColor: '#2563EB', - icon: ManualTriggerIcon, + icon: Play, subBlocks: [], tools: { access: [], diff --git a/apps/sim/blocks/blocks/schedule.ts b/apps/sim/blocks/blocks/schedule.ts index a65ac58792f..7dbda3c7fd4 100644 --- a/apps/sim/blocks/blocks/schedule.ts +++ b/apps/sim/blocks/blocks/schedule.ts @@ -1,10 +1,6 @@ -import type { SVGProps } from 'react' -import { createElement } from 'react' -import { Clock } from 'lucide-react' +import { Clock } from '@sim/emcn/icons' import type { BlockConfig } from '@/blocks/types' -const ScheduleIcon = (props: SVGProps) => createElement(Clock, props) - export const ScheduleBlock: BlockConfig = { type: 'schedule', triggerAllowed: true, @@ -19,7 +15,7 @@ export const ScheduleBlock: BlockConfig = { `, category: 'triggers', bgColor: '#6366F1', - icon: ScheduleIcon, + icon: Clock, subBlocks: [ { diff --git a/apps/sim/blocks/blocks/sftp.ts b/apps/sim/blocks/blocks/sftp.ts index 5e5d1928a9d..dc2fc15e224 100644 --- a/apps/sim/blocks/blocks/sftp.ts +++ b/apps/sim/blocks/blocks/sftp.ts @@ -1,12 +1,4 @@ -import { - ClipboardList, - Download, - File, - Search, - Server, - TrashOutline, - Upload, -} from '@sim/emcn/icons' +import { ClipboardList, Download, File, Search, Server, Trash, Upload } from '@sim/emcn/icons' import { SftpIcon } from '@/components/icons' import type { BlockConfig, BlockMeta } from '@/blocks/types' import { AuthMode, IntegrationType } from '@/blocks/types' @@ -352,7 +344,7 @@ export const SftpBlockMeta = { tags: ['files', 'sync', 'sftp'], }, { - icon: TrashOutline, + icon: Trash, title: 'Archive and delete old SFTP files', prompt: 'Build a workflow that runs nightly, lists an SFTP directory, downloads files older than a threshold to archive them, and then deletes the originals from the remote server.', diff --git a/apps/sim/blocks/blocks/wait.ts b/apps/sim/blocks/blocks/wait.ts index a3f6f56bf35..46012672b17 100644 --- a/apps/sim/blocks/blocks/wait.ts +++ b/apps/sim/blocks/blocks/wait.ts @@ -1,10 +1,6 @@ -import type { SVGProps } from 'react' -import { createElement } from 'react' -import { PauseCircle } from 'lucide-react' +import { CirclePause } from '@sim/emcn/icons' import type { BlockConfig } from '@/blocks/types' -const WaitIcon = (props: SVGProps) => createElement(PauseCircle, props) - export const WaitBlock: BlockConfig = { type: 'wait', name: 'Wait', @@ -19,7 +15,7 @@ export const WaitBlock: BlockConfig = { `, category: 'blocks', bgColor: '#F59E0B', - icon: WaitIcon, + icon: CirclePause, docsLink: 'https://docs.sim.ai/workflows/blocks/wait', subBlocks: [ { diff --git a/apps/sim/blocks/custom/custom-block-icon.tsx b/apps/sim/blocks/custom/custom-block-icon.tsx index 4151fa13284..f14ef79ed88 100644 --- a/apps/sim/blocks/custom/custom-block-icon.tsx +++ b/apps/sim/blocks/custom/custom-block-icon.tsx @@ -2,7 +2,7 @@ import { memo, type SVGProps } from 'react' import { cn } from '@sim/emcn' -import { Box } from 'lucide-react' +import { Box } from '@sim/emcn/icons' import type { BlockIcon } from '@/blocks/types' const cache = new Map() @@ -41,8 +41,7 @@ export function makeImageIcon(url: string): BlockIcon { } /** Fallback icon for custom blocks published without an uploaded image. */ -// double-cast-allowed: a lucide icon component fills the SVG-typed BlockIcon slot -export const DefaultCustomBlockIcon: BlockIcon = Box as unknown as BlockIcon +export const DefaultCustomBlockIcon: BlockIcon = Box /** * Resolve a custom block's icon: the uploaded image, else the org's whitelabel logo diff --git a/apps/sim/components.json b/apps/sim/components.json index eea8c1fbb86..5c03e841bc2 100644 --- a/apps/sim/components.json +++ b/apps/sim/components.json @@ -17,6 +17,5 @@ "lib": "@/lib", "hooks": "@/hooks", "blocks": "@/blocks" - }, - "iconLibrary": "lucide" + } } diff --git a/apps/sim/components/agent-stream/agent-stream-chrome.tsx b/apps/sim/components/agent-stream/agent-stream-chrome.tsx index 5c34c1dd25a..ff7b3b832a0 100644 --- a/apps/sim/components/agent-stream/agent-stream-chrome.tsx +++ b/apps/sim/components/agent-stream/agent-stream-chrome.tsx @@ -2,7 +2,7 @@ import { useEffect, useLayoutEffect, useRef, useState } from 'react' import { cn } from '@sim/emcn' -import { Check, ChevronDown, Circle, Square, X } from 'lucide-react' +import { Check, ChevronDown, Circle, Square, X } from '@sim/emcn/icons' import type { AgentStreamToolCall, AgentStreamToolStatus, @@ -129,7 +129,6 @@ export function AgentStreamThinkingChrome({ 'size-[14px] transition-transform duration-150', open ? 'rotate-0' : '-rotate-90' )} - strokeWidth={2} /> {isStreaming ? ( + return } if (status === 'error') { - return + return } if (status === 'cancelled') { - return + return } - return + return } export interface AgentStreamToolCallsChromeProps { @@ -224,7 +223,6 @@ export function AgentStreamToolCallsChrome({ > {isStreaming ? 'Using tools…' : 'Tools'} diff --git a/apps/sim/components/emails/notifications/schedule-disabled-email.tsx b/apps/sim/components/emails/notifications/schedule-disabled-email.tsx index a5116be4246..7ec78752a65 100644 --- a/apps/sim/components/emails/notifications/schedule-disabled-email.tsx +++ b/apps/sim/components/emails/notifications/schedule-disabled-email.tsx @@ -9,14 +9,12 @@ import { getBrandConfig } from '@/ee/whitelabeling' interface ScheduleDisabledEmailProps { recipientName?: string - /** Drives the noun and the CTA label. */ - kind: 'workflow' | 'job' - /** Workflow name or job title. Absent when the source row could not be read. */ + /** Workflow name. Absent when the source row could not be read. */ resourceName?: string reason: ScheduleDisableReason /** Consecutive failures at disable time. Rendered only for `consecutive_failures`. */ failedCount?: number - /** Deep link to the workflow or scheduled tasks. Absent when the workspace is unknown. */ + /** Deep link to the workflow. Absent when the workspace is unknown. */ manageLink?: string } @@ -26,15 +24,13 @@ interface ScheduleDisabledEmailProps { */ export function ScheduleDisabledEmail({ recipientName, - kind, resourceName, reason, failedCount, manageLink, }: ScheduleDisabledEmailProps) { const brand = getBrandConfig() - const resourceLabel = - resourceName ?? (kind === 'job' ? 'a scheduled task' : 'a scheduled workflow') + const resourceLabel = resourceName ?? 'a scheduled workflow' const reasonCopy = reason === 'consecutive_failures' && failedCount ? `It failed ${failedCount.toLocaleString()} times in a row.` @@ -64,9 +60,7 @@ export function ScheduleDisabledEmail({ {manageLink ? ( - - {kind === 'job' ? 'Open scheduled tasks' : 'Open workflow'} - + Open workflow ) : null} diff --git a/apps/sim/components/emails/render-notifications.test.ts b/apps/sim/components/emails/render-notifications.test.ts index 67d45e0736a..b33ef4d8757 100644 --- a/apps/sim/components/emails/render-notifications.test.ts +++ b/apps/sim/components/emails/render-notifications.test.ts @@ -15,7 +15,6 @@ describe('renderScheduleDisabledEmail', () => { it('renders the failure count for a threshold disable', async () => { const html = await renderScheduleDisabledEmail({ recipientName: 'John', - kind: 'workflow', resourceName: 'Daily digest', reason: 'consecutive_failures', failedCount: 100, @@ -30,20 +29,18 @@ describe('renderScheduleDisabledEmail', () => { it('renders reason copy instead of a count for a single-strike disable', async () => { const html = await renderScheduleDisabledEmail({ - kind: 'job', resourceName: 'Weekly report', reason: 'authentication_error', - manageLink: 'https://sim.ai/workspace/ws_123/scheduled-tasks', + manageLink: 'https://sim.ai/workspace/ws_123/w/wf_456', }) expect(html).toContain('could not be authenticated') expect(html).not.toContain('times in a row') - expect(html).toContain('Open scheduled tasks') + expect(html).toContain('Open workflow') }) it('falls back to a generic noun and omits the CTA when the source is unknown', async () => { const html = await renderScheduleDisabledEmail({ - kind: 'workflow', reason: 'workflow_not_found', }) diff --git a/apps/sim/components/emails/render.ts b/apps/sim/components/emails/render.ts index 2510be6cbc6..3b8f7e694f2 100644 --- a/apps/sim/components/emails/render.ts +++ b/apps/sim/components/emails/render.ts @@ -152,7 +152,6 @@ export async function renderUsageLimitReachedEmail(params: { export async function renderScheduleDisabledEmail(params: { recipientName?: string - kind: 'workflow' | 'job' resourceName?: string reason: ScheduleDisableReason failedCount?: number diff --git a/apps/sim/components/permissions/member-row.tsx b/apps/sim/components/permissions/member-row.tsx index 8a235584204..8e184e5633a 100644 --- a/apps/sim/components/permissions/member-row.tsx +++ b/apps/sim/components/permissions/member-row.tsx @@ -81,7 +81,7 @@ export function MemberRow({ /> {canManage && ( - + Remove )} diff --git a/apps/sim/components/pii/custom-patterns-editor.tsx b/apps/sim/components/pii/custom-patterns-editor.tsx index 4be1204f5b0..f441e048cd1 100644 --- a/apps/sim/components/pii/custom-patterns-editor.tsx +++ b/apps/sim/components/pii/custom-patterns-editor.tsx @@ -1,7 +1,7 @@ 'use client' import { Chip, ChipInput } from '@sim/emcn' -import { Plus, Trash2 } from 'lucide-react' +import { Plus, Trash } from '@sim/emcn/icons' import type { CustomPiiPattern } from '@/lib/guardrails/pii-entities' import { validateRegexPattern } from '@/lib/guardrails/validate_regex' @@ -71,7 +71,7 @@ export function CustomPatternsEditor({ patterns, onChange }: CustomPatternsEdito onClick={() => removeRow(index)} className='flex size-[30px] flex-shrink-0 items-center justify-center rounded-md text-[var(--text-icon)] transition-colors hover-hover:bg-[var(--surface-active)] hover-hover:text-[var(--text-error)]' > - +
{error && {error}} diff --git a/apps/sim/components/settings/navigation.ts b/apps/sim/components/settings/navigation.ts index 10d64d602f3..0f7560809c8 100644 --- a/apps/sim/components/settings/navigation.ts +++ b/apps/sim/components/settings/navigation.ts @@ -2,7 +2,9 @@ import type { ComponentType } from 'react' import { ClipboardList, Clock, + Credit, Database, + Globe, HexSimple, Key, KeySquare, @@ -17,14 +19,12 @@ import { Shuffle, Sprout, TerminalWindow, - TrashOutline, + Trash, Upload, - User, Users, Wrench, } from '@sim/emcn/icons' import { type PermissionType, permissionSatisfies } from '@sim/platform-authz/workspace' -import { Globe } from 'lucide-react' import { CodeIcon, McpIcon } from '@/components/icons' import { getEnv, isTruthy } from '@/lib/core/config/env' import { @@ -477,7 +477,7 @@ export const SETTINGS_SECTION_REGISTRY: readonly SettingsSectionRegistryEntry[] }, { label: 'Subscription', - icon: ClipboardList, + icon: Credit, unified: { id: 'billing', description: 'Manage your plan, pricing, and invoices.', @@ -508,7 +508,7 @@ export const SETTINGS_SECTION_REGISTRY: readonly SettingsSectionRegistryEntry[] }, { label: 'Teammates', - icon: User, + icon: Users, unified: { id: 'teammates', description: 'Manage your teammates in this workspace.', @@ -678,7 +678,7 @@ export const SETTINGS_SECTION_REGISTRY: readonly SettingsSectionRegistryEntry[] }, { label: 'Recently deleted', - icon: TrashOutline, + icon: Trash, unified: { id: 'recently-deleted', description: 'Restore items deleted in the last 30 days.', diff --git a/apps/sim/components/settings/standalone-settings-shell.tsx b/apps/sim/components/settings/standalone-settings-shell.tsx index c7d45633788..0ef8997c7b2 100644 --- a/apps/sim/components/settings/standalone-settings-shell.tsx +++ b/apps/sim/components/settings/standalone-settings-shell.tsx @@ -26,6 +26,7 @@ import { SettingsSectionProvider } from '@/components/settings/settings-panel' import { SettingsSidebar } from '@/components/settings/settings-sidebar' import { useSettingsBeforeUnload } from '@/components/settings/use-settings-before-unload' import { isBillingEnabled, isHosted } from '@/lib/core/config/env-flags' +import { SIDEBAR_WIDTH } from '@/stores/constants' interface StandaloneSettingsShellBaseProps { children: ReactNode @@ -142,7 +143,8 @@ export function StandaloneSettingsShell(props: StandaloneSettingsShellProps) { */}
{filteredCoreBlocks.length === 0 && filteredToolBlocks.length === 0 && ( @@ -1766,10 +1757,7 @@ export function GroupDetail({ setBlocksAllowed(filteredCoreBlocks, !coreBlocksAllAllowed)} - > + setBlocksAllowed(filteredCoreBlocks, !coreBlocksAllAllowed)}> {coreBlocksAllAllowed ? 'Deselect All' : 'Select All'} } @@ -1821,10 +1809,7 @@ export function GroupDetail({ } action={ - setBlocksAllowed(filteredToolBlocks, !toolBlocksAllAllowed)} - > + setBlocksAllowed(filteredToolBlocks, !toolBlocksAllAllowed)}> {toolBlocksAllAllowed ? 'Deselect All' : 'Select All'} } @@ -1871,7 +1856,6 @@ export function GroupDetail({ ), })) } - flush disabled={filteredPlatformFeatures.length === 0} > {platformAllVisible ? 'Deselect All' : 'Select All'} diff --git a/apps/sim/ee/custom-blocks/components/custom-block-detail.tsx b/apps/sim/ee/custom-blocks/components/custom-block-detail.tsx index d4d22822858..8d7b5ccaf1c 100644 --- a/apps/sim/ee/custom-blocks/components/custom-block-detail.tsx +++ b/apps/sim/ee/custom-blocks/components/custom-block-detail.tsx @@ -18,8 +18,8 @@ import { Switch, toast, } from '@sim/emcn' +import { ArrowLeft, ChevronDown, ImageUp as ImageIcon, X } from '@sim/emcn/icons' import { getErrorMessage } from '@sim/utils/errors' -import { ArrowLeft, ChevronDown, Image as ImageIcon, X } from 'lucide-react' import { saveDiscardActions } from '@/components/settings/save-discard-actions' import { type FlattenOutputsBlockInput, diff --git a/apps/sim/ee/custom-blocks/components/custom-blocks.tsx b/apps/sim/ee/custom-blocks/components/custom-blocks.tsx index 428b433da5f..d21c047e725 100644 --- a/apps/sim/ee/custom-blocks/components/custom-blocks.tsx +++ b/apps/sim/ee/custom-blocks/components/custom-blocks.tsx @@ -2,7 +2,7 @@ import { useMemo, useState } from 'react' import { ChipTag } from '@sim/emcn' -import { Plus } from 'lucide-react' +import { Plus } from '@sim/emcn/icons' import { useParams } from 'next/navigation' import { useQueryState } from 'nuqs' import { canMutateWorkspaceSettingsSection } from '@/components/settings/navigation' diff --git a/apps/sim/ee/data-retention/components/data-retention-settings.tsx b/apps/sim/ee/data-retention/components/data-retention-settings.tsx index 81e49ceb12e..cbc49c956a4 100644 --- a/apps/sim/ee/data-retention/components/data-retention-settings.tsx +++ b/apps/sim/ee/data-retention/components/data-retention-settings.tsx @@ -14,11 +14,10 @@ import { Search, toast, } from '@sim/emcn' -import { ArrowLeft } from '@sim/emcn/icons' +import { ArrowLeft, Plus } from '@sim/emcn/icons' import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' -import { Plus } from 'lucide-react' import { CustomPatternsEditor } from '@/components/pii/custom-patterns-editor' import { saveDiscardActions } from '@/components/settings/save-discard-actions' import type { SettingsAction } from '@/components/settings/settings-header' diff --git a/apps/sim/ee/sso/components/sso-settings.tsx b/apps/sim/ee/sso/components/sso-settings.tsx index b7f3df18a85..6b31562a006 100644 --- a/apps/sim/ee/sso/components/sso-settings.tsx +++ b/apps/sim/ee/sso/components/sso-settings.tsx @@ -14,9 +14,9 @@ import { Switch, toast, } from '@sim/emcn' +import { ChevronDown, Eye, EyeOff } from '@sim/emcn/icons' import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' -import { ChevronDown, Eye, EyeOff } from 'lucide-react' import { saveDiscardActions } from '@/components/settings/save-discard-actions' import type { SettingsAction } from '@/components/settings/settings-header' import type { SsoRegistrationBody } from '@/lib/api/contracts/auth' diff --git a/apps/sim/ee/whitelabeling/components/whitelabeling-settings.tsx b/apps/sim/ee/whitelabeling/components/whitelabeling-settings.tsx index 64dedd8a589..fa42fed2bce 100644 --- a/apps/sim/ee/whitelabeling/components/whitelabeling-settings.tsx +++ b/apps/sim/ee/whitelabeling/components/whitelabeling-settings.tsx @@ -2,9 +2,9 @@ import { useEffect, useRef, useState } from 'react' import { Button, ChipInput, cn, Label, Loader, toast } from '@sim/emcn' +import { ImageUp as ImageIcon, X } from '@sim/emcn/icons' import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' -import { Image as ImageIcon, X } from 'lucide-react' import Image from 'next/image' import { saveDiscardActions } from '@/components/settings/save-discard-actions' import { isEnterprise } from '@/lib/billing/plan-helpers' diff --git a/apps/sim/ee/workspace-forking/components/fork-workspace-modal/fork-workspace-modal.tsx b/apps/sim/ee/workspace-forking/components/fork-workspace-modal/fork-workspace-modal.tsx index 0623ffc0de4..0f0c10fa431 100644 --- a/apps/sim/ee/workspace-forking/components/fork-workspace-modal/fork-workspace-modal.tsx +++ b/apps/sim/ee/workspace-forking/components/fork-workspace-modal/fork-workspace-modal.tsx @@ -11,7 +11,7 @@ import { ChipModalHeader, toast, } from '@sim/emcn' -import { AlertTriangle } from 'lucide-react' +import { TriangleAlert } from '@sim/emcn/icons' import { useRouter } from 'next/navigation' import type { GetForkResourcesResponse } from '@/lib/api/contracts/workspace-fork' import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section' @@ -261,7 +261,7 @@ export function ForkWorkspaceModal({ )} {hasDeselection ? (
- + Some resources are not selected — references to them in your workflows will be cleared in the fork. diff --git a/apps/sim/ee/workspace-forking/components/forks.tsx b/apps/sim/ee/workspace-forking/components/forks.tsx index 550be464c49..6cc9789acd1 100644 --- a/apps/sim/ee/workspace-forking/components/forks.tsx +++ b/apps/sim/ee/workspace-forking/components/forks.tsx @@ -2,9 +2,8 @@ import { useState } from 'react' import { ChipConfirmModal, toast } from '@sim/emcn' -import { ArrowLeft } from '@sim/emcn/icons' +import { ArrowLeft, Plus, TriangleAlert } from '@sim/emcn/icons' import { getErrorMessage } from '@sim/utils/errors' -import { AlertTriangle, Plus } from 'lucide-react' import { useParams, useRouter } from 'next/navigation' import { useQueryState } from 'nuqs' import { saveDiscardActions } from '@/components/settings/save-discard-actions' @@ -550,7 +549,7 @@ export function Forks() { }} >
- + This cannot be undone — the saved mappings and sync history for this pair are deleted, and forking again creates a brand-new workspace. @@ -576,7 +575,7 @@ export function Forks() { }} >
- + Resources copied into this workspace during syncs may remain afterward — rollback restores workflows to their prior versions but does not remove copied resources. diff --git a/apps/sim/enrichments/company-domain/company-domain.ts b/apps/sim/enrichments/company-domain/company-domain.ts index 03349d07247..82ae54dca6d 100644 --- a/apps/sim/enrichments/company-domain/company-domain.ts +++ b/apps/sim/enrichments/company-domain/company-domain.ts @@ -1,4 +1,4 @@ -import { Globe } from 'lucide-react' +import { Globe } from '@sim/emcn/icons' import { normalizeDomain, str, toolProvider } from '@/enrichments/providers' import type { EnrichmentConfig } from '@/enrichments/types' diff --git a/apps/sim/enrichments/company-info/company-info.ts b/apps/sim/enrichments/company-info/company-info.ts index 407b7db95bd..43fbde12a23 100644 --- a/apps/sim/enrichments/company-info/company-info.ts +++ b/apps/sim/enrichments/company-info/company-info.ts @@ -1,5 +1,5 @@ +import { Building } from '@sim/emcn/icons' import { filterUndefined } from '@sim/utils/object' -import { Building2 } from 'lucide-react' import { normalizeDomain, str, toolProvider } from '@/enrichments/providers' import type { EnrichmentConfig } from '@/enrichments/types' @@ -15,7 +15,7 @@ export const companyInfoEnrichment: EnrichmentConfig = { id: 'company-info', name: 'Company Info', description: "Look up a company's size and description from its domain.", - icon: Building2, + icon: Building, inputs: [{ id: 'domain', name: 'Company domain', type: 'string', required: true }], outputs: [ { id: 'employeeCount', name: 'employee count', type: 'string' }, diff --git a/apps/sim/enrichments/phone-number/phone-number.ts b/apps/sim/enrichments/phone-number/phone-number.ts index 270135e20fd..c135270181a 100644 --- a/apps/sim/enrichments/phone-number/phone-number.ts +++ b/apps/sim/enrichments/phone-number/phone-number.ts @@ -1,5 +1,5 @@ +import { Phone } from '@sim/emcn/icons' import { filterUndefined } from '@sim/utils/object' -import { Phone } from 'lucide-react' import { firstNonEmpty, normalizeDomain, str, toolProvider } from '@/enrichments/providers' import type { EnrichmentConfig } from '@/enrichments/types' diff --git a/apps/sim/hooks/queries/schedules.ts b/apps/sim/hooks/queries/schedules.ts index def9e736bdd..a2db0ad31e9 100644 --- a/apps/sim/hooks/queries/schedules.ts +++ b/apps/sim/hooks/queries/schedules.ts @@ -1,22 +1,13 @@ -import { toast } from '@sim/emcn' import { createLogger } from '@sim/logger' -import { getErrorMessage } from '@sim/utils/errors' import { keepPreviousData, useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { isApiClientError } from '@/lib/api/client/errors' import { requestJson } from '@/lib/api/client/request' import { deployWorkflowContract } from '@/lib/api/contracts/deployments' import { - type CreateScheduleBody, - createScheduleContract, - deleteScheduleContract, - disableScheduleContract, - excludeOccurrenceContract, getScheduleByIdContract, getScheduleContract, listWorkspaceSchedulesContract, reactivateScheduleContract, - type UpdateScheduleBody, - updateScheduleContract, type WorkflowScheduleRow, type WorkspaceScheduleRow, } from '@/lib/api/contracts/schedules' @@ -92,18 +83,15 @@ export function useWorkspaceSchedules(workspaceId?: string, options?: { enabled? staleTime: SCHEDULE_LIST_STALE_TIME, placeholderData: keepPreviousData, // Pinned off (not inheriting the QueryClient default, which is on in the - // desktop app): a background refetch regenerates occurrence ids, which - // would close an open scheduled-task modal and drop its draft. See the - // taskById note in scheduled-tasks/hooks/use-scheduled-tasks.ts. + // desktop app): a background refetch regenerates occurrence ids, so any + // consumer holding one across a refetch would lose it mid-edit. refetchOnWindowFocus: false, }) } /** - * Fetch a single schedule (job) by id. Used by the mothership resource viewer so - * opening a scheduled-task artifact does a lightweight by-id read instead of the - * whole-workspace `useWorkspaceSchedules` fetch (which contended with the chat - * stream connection and stalled start/resume). + * Fetch a single workflow schedule by id — a lightweight by-id read instead of + * the whole-workspace `useWorkspaceSchedules` fetch. */ export function useScheduleById(scheduleId?: string) { return useQuery({ @@ -229,223 +217,6 @@ export function useReactivateSchedule() { }) } -/** - * Mutation to disable an active schedule or job - */ -export function useDisableSchedule() { - const queryClient = useQueryClient() - - return useMutation({ - mutationFn: async ({ - scheduleId, - workspaceId, - }: { - scheduleId: string - workspaceId: string - }) => { - await requestJson(disableScheduleContract, { - params: { id: scheduleId }, - body: { action: 'disable' }, - }) - - return { workspaceId } - }, - onSuccess: () => { - toast.success('Task paused') - }, - onError: (error) => { - logger.error('Failed to disable schedule', { error }) - toast.error("Couldn't pause task", { description: getErrorMessage(error) }) - }, - onSettled: async (data) => { - if (!data) return - await Promise.all([ - queryClient.invalidateQueries({ queryKey: scheduleKeys.list(data.workspaceId) }), - queryClient.invalidateQueries({ queryKey: scheduleKeys.details() }), - ]) - }, - }) -} - -/** - * Mutation to resume (reactivate) a paused standalone job schedule. Keyed by - * `workspaceId` so it invalidates the workspace list; the workflow-block variant - * {@link useReactivateSchedule} keys by `workflowId`/`blockId` instead. Resuming - * recomputes `nextRunAt` from the schedule's cron, so it applies to recurring - * tasks only — one-time tasks carry no cadence to resume. - */ -export function useResumeSchedule() { - const queryClient = useQueryClient() - - return useMutation({ - mutationFn: async ({ - scheduleId, - workspaceId, - }: { - scheduleId: string - workspaceId: string - }) => { - await requestJson(reactivateScheduleContract, { - params: { id: scheduleId }, - body: { action: 'reactivate' }, - }) - - return { workspaceId } - }, - onSuccess: () => { - toast.success('Task resumed') - }, - onError: (error) => { - logger.error('Failed to resume schedule', { error }) - toast.error("Couldn't resume task", { description: getErrorMessage(error) }) - }, - onSettled: async (data) => { - if (!data) return - await Promise.all([ - queryClient.invalidateQueries({ queryKey: scheduleKeys.list(data.workspaceId) }), - queryClient.invalidateQueries({ queryKey: scheduleKeys.details() }), - ]) - }, - }) -} - -/** - * Mutation to delete a schedule or job - */ -export function useDeleteSchedule() { - const queryClient = useQueryClient() - - return useMutation({ - mutationFn: async ({ - scheduleId, - workspaceId, - }: { - scheduleId: string - workspaceId: string - }) => { - await requestJson(deleteScheduleContract, { - params: { id: scheduleId }, - }) - - return { workspaceId } - }, - onSuccess: () => { - toast.success('Task deleted') - }, - onError: (error) => { - logger.error('Failed to delete schedule', { error }) - toast.error("Couldn't delete task", { description: getErrorMessage(error) }) - }, - onSettled: async (data) => { - if (!data) return - await Promise.all([ - queryClient.invalidateQueries({ queryKey: scheduleKeys.list(data.workspaceId) }), - queryClient.invalidateQueries({ queryKey: scheduleKeys.details() }), - ]) - }, - }) -} - -/** - * Mutation to delete a single occurrence of a recurring task (gcal "this - * event"). The whole series is deleted via {@link useDeleteSchedule} instead. - */ -export function useExcludeOccurrence() { - const queryClient = useQueryClient() - - return useMutation({ - mutationFn: async ({ - scheduleId, - occurrence, - workspaceId, - }: { - scheduleId: string - occurrence: string - workspaceId: string - }) => { - await requestJson(excludeOccurrenceContract, { - params: { id: scheduleId }, - body: { action: 'exclude_occurrence', occurrence }, - }) - - return { workspaceId } - }, - onSuccess: () => { - toast.success('Occurrence removed') - }, - onError: (error) => { - logger.error('Failed to delete occurrence', { error }) - toast.error("Couldn't remove occurrence", { description: getErrorMessage(error) }) - }, - onSettled: async (data) => { - if (!data) return - await Promise.all([ - queryClient.invalidateQueries({ queryKey: scheduleKeys.list(data.workspaceId) }), - queryClient.invalidateQueries({ queryKey: scheduleKeys.details() }), - ]) - }, - }) -} - -/** - * Mutation to update fields on a standalone job schedule - */ -export function useUpdateSchedule() { - const queryClient = useQueryClient() - - return useMutation({ - mutationFn: async ({ - scheduleId, - workspaceId, - ...updates - }: { - scheduleId: string - workspaceId: string - } & Omit) => { - await requestJson(updateScheduleContract, { - params: { id: scheduleId }, - body: { action: 'update', ...updates }, - }) - - return { workspaceId } - }, - onSuccess: () => { - toast.success('Task updated') - }, - onError: (error) => { - logger.error('Failed to update schedule', { error }) - toast.error("Couldn't update task", { description: getErrorMessage(error) }) - }, - onSettled: async (data) => { - if (!data) return - await Promise.all([ - queryClient.invalidateQueries({ queryKey: scheduleKeys.list(data.workspaceId) }), - queryClient.invalidateQueries({ queryKey: scheduleKeys.details() }), - ]) - }, - }) -} - -/** - * Mutation to create a standalone scheduled job - */ -export function useCreateSchedule() { - const queryClient = useQueryClient() - - return useMutation({ - mutationFn: async (body: CreateScheduleBody) => requestJson(createScheduleContract, { body }), - onSuccess: () => { - toast.success('Task scheduled') - }, - onError: (error) => { - logger.error('Failed to create schedule', { error }) - toast.error("Couldn't schedule task", { description: getErrorMessage(error) }) - }, - onSettled: (_data, _error, variables) => - queryClient.invalidateQueries({ queryKey: scheduleKeys.list(variables.workspaceId) }), - }) -} - /** * Mutation to redeploy a workflow (which recreates the schedule) */ diff --git a/apps/sim/hooks/queries/workflow-references.ts b/apps/sim/hooks/queries/workflow-references.ts deleted file mode 100644 index 6ae68da75b4..00000000000 --- a/apps/sim/hooks/queries/workflow-references.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { useQuery } from '@tanstack/react-query' -import { requestJson } from '@/lib/api/client/request' -import { - getWorkflowReferencesContract, - type WorkflowReferencesResponse, -} from '@/lib/api/contracts/workflow-references' - -/** - * Zero — the graph reflects live editor state, and no workflow-edit mutation - * invalidates this key (edits arrive over the socket, not through React Query). - * The modal mounts on demand, so every open refetches; a reopen paints the - * cached tree instantly while the background refetch reconciles it. - */ -export const WORKFLOW_REFERENCES_STALE_TIME = 0 - -export const workflowReferenceKeys = { - all: ['workflow-references'] as const, - details: () => [...workflowReferenceKeys.all, 'detail'] as const, - detail: (workflowId?: string) => [...workflowReferenceKeys.details(), workflowId ?? ''] as const, -} - -async function fetchWorkflowReferences( - workflowId: string, - signal?: AbortSignal -): Promise { - return requestJson(getWorkflowReferencesContract, { - params: { id: workflowId }, - signal, - }) -} - -export function useWorkflowReferences(workflowId?: string) { - return useQuery({ - queryKey: workflowReferenceKeys.detail(workflowId), - queryFn: ({ signal }) => fetchWorkflowReferences(workflowId as string, signal), - enabled: Boolean(workflowId), - staleTime: WORKFLOW_REFERENCES_STALE_TIME, - }) -} diff --git a/apps/sim/hooks/use-workspace-invite-policy.ts b/apps/sim/hooks/use-workspace-invite-policy.ts new file mode 100644 index 00000000000..b3ee90723b2 --- /dev/null +++ b/apps/sim/hooks/use-workspace-invite-policy.ts @@ -0,0 +1,48 @@ +'use client' + +import { useMemo } from 'react' +import { useWorkspacesQuery } from '@/hooks/queries/workspace' +import { usePermissionConfig } from '@/hooks/use-permission-config' + +export interface UseWorkspaceInvitePolicyReturn { + /** + * Why inviting is unavailable, already phrased for whoever is asking — the + * billed user is told to upgrade, everyone else to contact the owner. `null` + * when inviting is available, and also when it is blocked by a switch that has + * no viewer-facing explanation. + */ + inviteDisabledReason: string | null + /** Whether inviting is blocked at all, by any of the sources below. */ + isInvitationsDisabled: boolean +} + +/** + * The workspace's invite gate, as every entry point offering an invite or a + * teammate-management action must read it. + * + * Two independent sources say no, and both have to be consulted: the deployment + * or permission-group switch (`NEXT_PUBLIC_DISABLE_INVITATIONS`, a permission + * group's `disableInvitations`), which is silent about why, and the workspace's + * own plan policy, which supplies {@link UseWorkspaceInvitePolicyReturn.inviteDisabledReason}. + * Reading only one of them is what lets an entry point offer an invite the server + * then refuses. + * + * For callers that are already handed the workspace — the workspace switcher gets + * the whole list as a prop — deriving the same two fields inline is correct; this + * hook is for the ones that would otherwise fetch the list just to ask. + */ +export function useWorkspaceInvitePolicy(workspaceId: string): UseWorkspaceInvitePolicyReturn { + const { isInvitationsDisabled: isInvitationsDisabledByConfig } = usePermissionConfig() + const { data: workspaces } = useWorkspacesQuery() + + const inviteDisabledReason = + workspaces?.find((workspace) => workspace.id === workspaceId)?.inviteDisabledReason ?? null + + return useMemo( + () => ({ + inviteDisabledReason, + isInvitationsDisabled: isInvitationsDisabledByConfig || inviteDisabledReason !== null, + }), + [inviteDisabledReason, isInvitationsDisabledByConfig] + ) +} diff --git a/apps/sim/lib/api/contracts/schedules.ts b/apps/sim/lib/api/contracts/schedules.ts index 3a707e59232..6fd4921f673 100644 --- a/apps/sim/lib/api/contracts/schedules.ts +++ b/apps/sim/lib/api/contracts/schedules.ts @@ -102,38 +102,6 @@ export const workspaceScheduleRowSchema = workflowScheduleRowSchema.extend({ export type WorkspaceScheduleRow = z.output -export const createScheduleBodySchema = z - .object({ - workspaceId: z.string().min(1, 'Workspace ID is required'), - title: z.string().min(1, 'Title is required'), - prompt: z.string().min(1, 'Prompt is required'), - /** Recurring cadence. Omit (with `time` set) for a one-time task. */ - cronExpression: z.string().min(1).optional(), - /** One-time launch instant (ISO 8601). Omit (with `cronExpression` set) for a recurring task. */ - time: z.string().min(1).optional(), - timezone: z.string().optional().default('UTC'), - lifecycle: scheduleLifecycleSchema.optional().default('persistent'), - /** Recurrence end after N runs (gcal "ends after N occurrences"). */ - maxRuns: z.number().int().positive().optional(), - /** Recurrence end on a date (ISO 8601; gcal "ends on date"). */ - endsAt: z.string().optional(), - startDate: z.string().optional(), - contexts: z.array(scheduleContextSchema).optional(), - secretScope: secretMountScopeSchema.optional(), - mountedSecrets: mountedSecretNamesSchema.optional(), - }) - .superRefine((body, ctx) => { - if (!body.cronExpression && !body.time) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - path: ['time'], - message: 'Provide a cron expression for a recurring task or a time for a one-time task', - }) - } - }) - -export type CreateScheduleBody = z.input - export const reactivateScheduleBodySchema = z.object({ action: z.literal('reactivate'), }) @@ -146,41 +114,9 @@ export const disableScheduleBodySchema = z.object({ export type DisableScheduleBody = z.input -export const updateScheduleBodySchema = z.object({ - action: z.literal('update'), - title: z.string().min(1).optional(), - prompt: z.string().min(1).optional(), - cronExpression: z.string().nullable().optional(), - /** One-time launch instant (ISO 8601). Switches a task to one-time when set alongside a null `cronExpression`. */ - time: z.string().min(1).optional(), - timezone: z.string().optional(), - lifecycle: scheduleLifecycleSchema.optional(), - maxRuns: z.number().int().positive().nullable().optional(), - endsAt: z.string().nullable().optional(), - contexts: z.array(scheduleContextSchema).optional(), - secretScope: secretMountScopeSchema.optional(), - mountedSecrets: mountedSecretNamesSchema.optional(), -}) - -export type UpdateScheduleBody = z.input - -/** - * Deletes a single occurrence of a recurring task (gcal "this event"): the - * occurrence's instant is added to the schedule's exclusion list and the next - * run advances past it. Deleting the whole series uses {@link deleteScheduleContract}. - */ -export const excludeOccurrenceBodySchema = z.object({ - action: z.literal('exclude_occurrence'), - occurrence: z.string().min(1, 'Occurrence timestamp is required'), -}) - -export type ExcludeOccurrenceBody = z.input - export const scheduleUpdateSchema = z.discriminatedUnion('action', [ reactivateScheduleBodySchema, disableScheduleBodySchema, - updateScheduleBodySchema, - excludeOccurrenceBodySchema, ]) export type ScheduleUpdate = z.input @@ -227,9 +163,8 @@ export const listWorkspaceSchedulesContract = defineRouteContract({ }) /** - * Single-schedule read by id. Used by the mothership resource viewer so opening - * a scheduled-task artifact does a lightweight by-id fetch instead of pulling - * the entire workspace schedule list (which contended with the chat stream). + * Single-schedule read by id: a lightweight fetch for one workflow schedule + * instead of pulling the whole workspace list. */ export const getScheduleByIdContract = defineRouteContract({ method: 'GET', @@ -244,32 +179,9 @@ export const getScheduleByIdContract = defineRouteContract({ }) /** - * Newly-created job schedules emit a partial summary with the canonical fields - * the route synthesizes server-side; everything else is filled in on - * subsequent reads. + * Re-arms a disabled schedule: the route recomputes `nextRunAt` from the stored + * cron expression and clears the failure counters. */ -export const createScheduleResponseSchema = z.object({ - schedule: z.object({ - id: z.string(), - status: scheduleStatusSchema, - /** Null for one-time tasks, which carry no recurring cadence. */ - cronExpression: z.string().nullable(), - nextRunAt: z.string(), - }), -}) - -export type CreateScheduleResponse = z.output - -export const createScheduleContract = defineRouteContract({ - method: 'POST', - path: '/api/schedules', - body: createScheduleBodySchema, - response: { - mode: 'json', - schema: createScheduleResponseSchema, - }, -}) - export const reactivateScheduleContract = defineRouteContract({ method: 'PUT', path: '/api/schedules/[id]', @@ -281,17 +193,6 @@ export const reactivateScheduleContract = defineRouteContract({ }, }) -export const disableScheduleContract = defineRouteContract({ - method: 'PUT', - path: '/api/schedules/[id]', - params: scheduleIdParamsSchema, - body: disableScheduleBodySchema, - response: { - mode: 'json', - schema: messageResponseSchema, - }, -}) - export const updateScheduleContract = defineRouteContract({ method: 'PUT', path: '/api/schedules/[id]', @@ -303,27 +204,6 @@ export const updateScheduleContract = defineRouteContract({ }, }) -export const excludeOccurrenceContract = defineRouteContract({ - method: 'PUT', - path: '/api/schedules/[id]', - params: scheduleIdParamsSchema, - body: excludeOccurrenceBodySchema, - response: { - mode: 'json', - schema: messageResponseSchema, - }, -}) - -export const deleteScheduleContract = defineRouteContract({ - method: 'DELETE', - path: '/api/schedules/[id]', - params: scheduleIdParamsSchema, - response: { - mode: 'json', - schema: messageResponseSchema, - }, -}) - export const executeSchedulesContract = defineRouteContract({ method: 'GET', path: '/api/schedules/execute', diff --git a/apps/sim/lib/api/contracts/workflow-references.ts b/apps/sim/lib/api/contracts/workflow-references.ts deleted file mode 100644 index c298c4f6bec..00000000000 --- a/apps/sim/lib/api/contracts/workflow-references.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { z } from 'zod' -import { nonEmptyIdSchema } from '@/lib/api/contracts/primitives' -import { defineRouteContract } from '@/lib/api/contracts/types' - -/** - * One node in a workflow reference tree. Recursive, so the Zod schema below needs - * `z.lazy` plus this explicit interface annotation — TypeScript cannot infer a - * self-referential type. Shared by the server graph builder (its return element - * type) and the client hook, keeping both off `z.output<...>`. - */ -export interface ReferenceNode { - /** Referenced workflow id. */ - id: string - /** Referenced workflow name. */ - name: string - /** - * True when this node closes a cycle already on the current path (e.g. the - * root, or `A → B → A`). Cyclic nodes carry no `children`. - */ - cycle: boolean - children: ReferenceNode[] -} - -export const referenceNodeSchema: z.ZodType = z.lazy(() => - z.object({ - id: z.string(), - name: z.string(), - cycle: z.boolean(), - children: z.array(referenceNodeSchema), - }) -) - -export const workflowReferencesParamsSchema = z.object({ - id: nonEmptyIdSchema, -}) - -export const workflowReferencesResponseSchema = z.object({ - /** Workflows that call this workflow (inbound), each recursively expanded. */ - callers: z.array(referenceNodeSchema), - /** Workflows this workflow calls (outbound), each recursively expanded. */ - callees: z.array(referenceNodeSchema), -}) - -export type WorkflowReferencesResponse = z.output - -export const getWorkflowReferencesContract = defineRouteContract({ - method: 'GET', - path: '/api/workflows/[id]/references', - params: workflowReferencesParamsSchema, - response: { - mode: 'json', - schema: workflowReferencesResponseSchema, - }, -}) diff --git a/apps/sim/lib/billing/workspace-permissions.ts b/apps/sim/lib/billing/workspace-permissions.ts index f9b7ff9daa8..41c57c574eb 100644 --- a/apps/sim/lib/billing/workspace-permissions.ts +++ b/apps/sim/lib/billing/workspace-permissions.ts @@ -1,4 +1,5 @@ import type { WorkspaceHostContext, WorkspaceUsageGate } from '@/lib/api/contracts/workspaces' +import { isBillingEnabled } from '@/lib/core/config/env-flags' export type WorkspaceUsageLimitAction = | { type: 'manage-billing'; message: null } @@ -18,6 +19,22 @@ export function canManageWorkspaceBilling( return hostContext.workspace.billedAccountUserId === viewerUserId } +/** + * Returns whether the Billing settings section is reachable for this viewer. + * + * Mirrors the section route's own gate, which sends a deployment running without + * billing back to General and 404s anyone who cannot manage the payer — on an + * organization-hosted workspace that is every member who is not an org admin. + * Menus that link to Billing drop the entry when this is false rather than + * offering a destination the server will refuse. + */ +export function canViewWorkspaceBillingSettings( + hostContext: WorkspaceHostContext, + viewerUserId?: string | null +): boolean { + return isBillingEnabled && canManageWorkspaceBilling(hostContext, viewerUserId) +} + /** * Resolves the workspace-safe action and copy for an exceeded usage gate. * Payer messages are intentionally replaced for viewers who cannot manage the diff --git a/apps/sim/lib/copilot/chat/post.ts b/apps/sim/lib/copilot/chat/post.ts index 161926cb8c3..b581bad7c63 100644 --- a/apps/sim/lib/copilot/chat/post.ts +++ b/apps/sim/lib/copilot/chat/post.ts @@ -94,7 +94,6 @@ const ResourceAttachmentSchema = z.object({ 'filefolder', 'task', 'log', - 'scheduledtask', 'generic', 'browser', // Filtered out client-side rather than sent, but accepted here so a stray @@ -128,7 +127,6 @@ const GENERIC_RESOURCE_TITLE: Record['t filefolder: 'File Folder', task: 'Task', log: 'Log', - scheduledtask: 'Scheduled Task', generic: 'Resource', browser: 'Browser', terminal: 'Terminal', @@ -197,7 +195,6 @@ const ChatContextSchema = z 'file_selection', 'folder', 'filefolder', - 'scheduledtask', 'integration', 'skill', 'mcp', diff --git a/apps/sim/lib/copilot/chat/process-contents.ts b/apps/sim/lib/copilot/chat/process-contents.ts index abd87f1cd8f..b80c7060705 100644 --- a/apps/sim/lib/copilot/chat/process-contents.ts +++ b/apps/sim/lib/copilot/chat/process-contents.ts @@ -1,18 +1,17 @@ import { db, dbReplica } from '@sim/db' -import { knowledgeBase, workflowSchedule } from '@sim/db/schema' +import { knowledgeBase } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { authorizeWorkflowByWorkspacePermission, getActiveWorkflowRecord, } from '@sim/platform-authz/workflow' -import { and, eq, isNull, ne } from 'drizzle-orm' +import { and, eq, isNull } from 'drizzle-orm' import { MAX_TABLE_SELECTION_CONTENT_LENGTH, safeBrowserSelectionUrl, truncateSelectionText, } from '@/lib/copilot/chat/selection-context' import { QueryLogs } from '@/lib/copilot/generated/tool-catalog-v1' -import { normalizeVfsSegment } from '@/lib/copilot/vfs/normalize-segment' import { buildVfsFolderPathMap, canonicalBlockVfsPath, @@ -283,16 +282,6 @@ export async function processContextsServer( path: result.path, } } - if (ctx.kind === 'scheduledtask' && ctx.scheduleId && currentWorkspaceId) { - const result = await resolveScheduledTaskResource(ctx.scheduleId, currentWorkspaceId) - if (!result) return null - return { - type: 'active_resource', - tag: ctx.label ? `@${ctx.label}` : '@', - content: result.content, - path: result.path, - } - } if (ctx.kind === 'docs') { try { const { searchDocumentationServerTool } = await import( @@ -848,9 +837,6 @@ export async function resolveActiveResourceContext( case 'filefolder': { return await resolveFileFolderResource(resourceId, workspaceId) } - case 'scheduledtask': { - return await resolveScheduledTaskResource(resourceId, workspaceId) - } default: return null } @@ -874,38 +860,6 @@ async function resolveTableResource( } } -async function resolveScheduledTaskResource( - scheduleId: string, - workspaceId: string -): Promise { - const [row] = await db - .select({ id: workflowSchedule.id, jobTitle: workflowSchedule.jobTitle }) - .from(workflowSchedule) - .where( - and( - eq(workflowSchedule.id, scheduleId), - eq(workflowSchedule.sourceWorkspaceId, workspaceId), - eq(workflowSchedule.sourceType, 'job'), - isNull(workflowSchedule.archivedAt), - // Mirror the VFS materializer (workspace-vfs `materializeJobs`), which - // excludes completed jobs — otherwise we'd point at a meta.json it never - // wrote and the agent's read would dangle. - ne(workflowSchedule.status, 'completed') - ) - ) - .limit(1) - if (!row) return null - // The VFS materializes jobs at `jobs/{sanitized title}/meta.json` (see - // workspace-vfs `materializeJobs`); emit the same lightweight path pointer so - // the agent reads it via the VFS instead of us inlining the (heavy) row. - return { - type: 'active_resource', - tag: '@active_resource', - content: '', - path: `jobs/${normalizeVfsSegment(row.jobTitle || row.id)}/meta.json`, - } -} - async function resolveFileResource( fileId: string, workspaceId: string diff --git a/apps/sim/lib/copilot/chat/workspace-context.ts b/apps/sim/lib/copilot/chat/workspace-context.ts index faf2b3a2170..4faca5f9d58 100644 --- a/apps/sim/lib/copilot/chat/workspace-context.ts +++ b/apps/sim/lib/copilot/chat/workspace-context.ts @@ -6,17 +6,11 @@ import { mcpServers, userTableDefinitions, workflow, - workflowSchedule, } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' -import { truncate } from '@sim/utils/string' import { and, eq, inArray, isNull } from 'drizzle-orm' -import type { - VfsSnapshotV1, - VfsSnapshotV1Job, - VfsSnapshotV1Workflow, -} from '@/lib/copilot/generated/vfs-snapshot-v1' +import type { VfsSnapshotV1, VfsSnapshotV1Workflow } from '@/lib/copilot/generated/vfs-snapshot-v1' import { normalizeVfsSegment } from '@/lib/copilot/vfs/normalize-segment' import { canonicalWorkflowVfsDir, canonicalWorkspaceFilePath } from '@/lib/copilot/vfs/path-utils' import { @@ -82,15 +76,6 @@ export interface WorkspaceMdData { customBlocks?: Array<{ type: string; name: string; description?: string }> mcpServers?: Array<{ id: string; name: string; url?: string | null; enabled: boolean }> skills?: Array<{ id: string; name: string; description: string }> - jobs?: Array<{ - id: string - title: string | null - prompt: string - cronExpression: string | null - status: string - lifecycle: string - sourceTaskName: string | null - }> } /** @@ -297,22 +282,6 @@ export function buildWorkspaceMd(data: WorkspaceMdData): string { ) } - if (data.jobs && data.jobs.length > 0) { - const lines = [...data.jobs] - .sort((a, b) => stableCompare(a.title || a.id, b.title || b.id) || stableCompare(a.id, b.id)) - .map((j) => { - const displayName = j.title || j.id - let line = `- **${displayName}** (${j.id}) — ${j.status}` - if (j.lifecycle !== 'persistent') line += ` [${j.lifecycle}]` - if (j.cronExpression) line += `, cron: ${j.cronExpression}` - if (j.sourceTaskName) line += `, task: ${j.sourceTaskName}` - const promptPreview = j.prompt.length > 80 ? truncate(j.prompt, 77) : j.prompt - line += `\n ${promptPreview}` - return line - }) - sections.push(`## Jobs (${data.jobs.length})\n${lines.join('\n')}`) - } - return sections.join('\n\n') } @@ -360,7 +329,6 @@ async function buildWorkspaceMdData( customTools, mcpServerRows, skillRows, - jobRows, customBlockSummaries, ] = await Promise.all([ getUsersWithPermissions(workspaceId), @@ -434,25 +402,6 @@ async function buildWorkspaceMdData( listSkillsForUser({ workspaceId, userId, includeBuiltins: false, workspaceAccess }), - db - .select({ - id: workflowSchedule.id, - jobTitle: workflowSchedule.jobTitle, - prompt: workflowSchedule.prompt, - cronExpression: workflowSchedule.cronExpression, - status: workflowSchedule.status, - lifecycle: workflowSchedule.lifecycle, - sourceTaskName: workflowSchedule.sourceTaskName, - }) - .from(workflowSchedule) - .where( - and( - eq(workflowSchedule.sourceWorkspaceId, workspaceId), - eq(workflowSchedule.sourceType, 'job'), - isNull(workflowSchedule.archivedAt) - ) - ), - listCustomBlockSummariesForWorkspace(workspaceId), ]) @@ -536,17 +485,6 @@ async function buildWorkspaceMdData( customBlocks: customBlockSummaries, mcpServers: mcpServerRows, skills: skillRows.map((s) => ({ id: s.id, name: s.name, description: s.description })), - jobs: jobRows - .filter((j) => j.status !== 'completed') - .map((j) => ({ - id: j.id, - title: j.jobTitle, - prompt: j.prompt || '', - cronExpression: j.cronExpression, - status: j.status, - lifecycle: j.lifecycle, - sourceTaskName: j.sourceTaskName, - })), } } catch (err) { logger.error('Failed to build workspace data', { @@ -601,19 +539,6 @@ export function buildVfsSnapshot(data: WorkspaceMdData): VfsSnapshotV1 { ...(wf.isDeployed ? { isDeployed: true } : {}), ...(wf.folderPath ? { folderPath: wf.folderPath } : {}), })) - const jobs: VfsSnapshotV1Job[] = (data.jobs ?? []) - .filter((j) => j.status !== 'completed') - .map((j) => ({ - id: j.id, - ...(j.title ? { title: j.title } : {}), - // Match WORKSPACE.md's preview truncation — full prompts are large, - // volatile-ish, and readable on demand at jobs/{title}/meta.json. - ...(j.prompt ? { prompt: j.prompt.length > 80 ? truncate(j.prompt, 77) : j.prompt } : {}), - ...(j.cronExpression ? { cronExpression: j.cronExpression } : {}), - ...(j.status ? { status: j.status } : {}), - ...(j.lifecycle ? { lifecycle: j.lifecycle } : {}), - ...(j.sourceTaskName ? { sourceTaskName: j.sourceTaskName } : {}), - })) return { ...(data.workspace ? { @@ -675,7 +600,6 @@ export function buildVfsSnapshot(data: WorkspaceMdData): VfsSnapshotV1 { name: s.name, ...(s.description ? { description: s.description } : {}), })), - jobs, } } diff --git a/apps/sim/lib/copilot/generated/tool-catalog-v1.ts b/apps/sim/lib/copilot/generated/tool-catalog-v1.ts index 8200e4f02d1..a61f6a65527 100644 --- a/apps/sim/lib/copilot/generated/tool-catalog-v1.ts +++ b/apps/sim/lib/copilot/generated/tool-catalog-v1.ts @@ -33,7 +33,6 @@ export interface ToolCatalogEntry { | 'browser_wait_for' | 'call_integration_tool' | 'check_deployment_status' - | 'complete_scheduled_task' | 'cp' | 'crawl_website' | 'create_file' @@ -63,7 +62,6 @@ export interface ToolCatalogEntry { | 'get_deployment_log' | 'get_page_contents' | 'get_platform_actions' - | 'get_scheduled_task_logs' | 'get_workflow_data' | 'get_workflow_run_options' | 'glob' @@ -79,7 +77,6 @@ export interface ToolCatalogEntry { | 'manage_credential' | 'manage_custom_tool' | 'manage_mcp_tool' - | 'manage_scheduled_task' | 'manage_skill' | 'materialize_file' | 'media' @@ -102,7 +99,6 @@ export interface ToolCatalogEntry { | 'run_from_block' | 'run_workflow' | 'run_workflow_until_block' - | 'scheduled_task' | 'scrape_page' | 'search' | 'search_documentation' @@ -118,7 +114,6 @@ export interface ToolCatalogEntry { | 'table' | 'terminal' | 'update_deployment_version' - | 'update_scheduled_task_history' | 'update_workspace_mcp_server' | 'user_table' | 'wait' @@ -153,7 +148,6 @@ export interface ToolCatalogEntry { | 'browser_wait_for' | 'call_integration_tool' | 'check_deployment_status' - | 'complete_scheduled_task' | 'cp' | 'crawl_website' | 'create_file' @@ -183,7 +177,6 @@ export interface ToolCatalogEntry { | 'get_deployment_log' | 'get_page_contents' | 'get_platform_actions' - | 'get_scheduled_task_logs' | 'get_workflow_data' | 'get_workflow_run_options' | 'glob' @@ -199,7 +192,6 @@ export interface ToolCatalogEntry { | 'manage_credential' | 'manage_custom_tool' | 'manage_mcp_tool' - | 'manage_scheduled_task' | 'manage_skill' | 'materialize_file' | 'media' @@ -222,7 +214,6 @@ export interface ToolCatalogEntry { | 'run_from_block' | 'run_workflow' | 'run_workflow_until_block' - | 'scheduled_task' | 'scrape_page' | 'search' | 'search_documentation' @@ -238,7 +229,6 @@ export interface ToolCatalogEntry { | 'table' | 'terminal' | 'update_deployment_version' - | 'update_scheduled_task_history' | 'update_workspace_mcp_server' | 'user_table' | 'wait' @@ -258,7 +248,6 @@ export interface ToolCatalogEntry { | 'knowledge' | 'media' | 'run' - | 'scheduled_task' | 'search' | 'table' | 'workflow' @@ -1247,20 +1236,6 @@ export const CheckDeploymentStatus: ToolCatalogEntry = { }, } -export const CompleteScheduledTask: ToolCatalogEntry = { - id: 'complete_scheduled_task', - name: 'complete_scheduled_task', - route: 'sim', - mode: 'async', - parameters: { - type: 'object', - properties: { - jobId: { type: 'string', description: 'The ID of the scheduled task to mark as completed.' }, - }, - required: ['jobId'], - }, -} - export const Cp: ToolCatalogEntry = { id: 'cp', name: 'cp', @@ -2314,7 +2289,7 @@ export const FunctionExecute: ToolCatalogEntry = { code: { type: 'string', description: - 'Code to execute. For JS: raw statements auto-wrapped in async context. For Python: full script. For shell: bash script with pre-installed CLI tools. Request each needed secret with an explicit {{VAR_NAME}} reference.', + 'Code to execute. For JS: raw statements auto-wrapped in async context. For Python: full script. For shell: bash script with pre-installed CLI tools. Use each needed secret as {{VAR_NAME}}; the reference resolves to the value exactly as stored.', }, inputs: { type: 'object', @@ -3030,26 +3005,6 @@ export const GetPlatformActions: ToolCatalogEntry = { parameters: { type: 'object', properties: {} }, } -export const GetScheduledTaskLogs: ToolCatalogEntry = { - id: 'get_scheduled_task_logs', - name: 'get_scheduled_task_logs', - route: 'sim', - mode: 'async', - parameters: { - type: 'object', - properties: { - executionId: { type: 'string', description: 'Optional execution ID for a specific run.' }, - includeDetails: { - type: 'boolean', - description: 'Include tool calls, outputs, and cost details.', - }, - jobId: { type: 'string', description: 'The scheduled task (schedule) ID to get logs for.' }, - limit: { type: 'number', description: 'Max number of entries (default: 3, max: 5)' }, - }, - required: ['jobId'], - }, -} - export const GetWorkflowData: ToolCatalogEntry = { id: 'get_workflow_data', name: 'get_workflow_data', @@ -3501,7 +3456,7 @@ export const ManageCustomTool: ToolCatalogEntry = { operation: { type: 'string', description: - "The operation to perform: 'add', 'edit', 'list', or 'delete'. These verbs are tool-specific — manage_scheduled_task uses create/update instead of add/edit.", + "The operation to perform: 'add', 'edit', 'list', or 'delete'. These verbs are tool-specific — other manage_* tools may use create/update instead of add/edit.", enum: ['add', 'edit', 'delete', 'list'], }, schema: { @@ -3591,7 +3546,7 @@ export const ManageMcpTool: ToolCatalogEntry = { operation: { type: 'string', description: - "The operation to perform: 'add', 'edit', 'list', or 'delete'. These verbs are tool-specific — manage_scheduled_task uses create/update instead of add/edit.", + "The operation to perform: 'add', 'edit', 'list', or 'delete'. These verbs are tool-specific — other manage_* tools may use create/update instead of add/edit.", enum: ['add', 'edit', 'delete', 'list'], }, serverId: { @@ -3605,80 +3560,6 @@ export const ManageMcpTool: ToolCatalogEntry = { requiredPermission: 'write', } -export const ManageScheduledTask: ToolCatalogEntry = { - id: 'manage_scheduled_task', - name: 'manage_scheduled_task', - route: 'sim', - mode: 'async', - parameters: { - type: 'object', - properties: { - args: { - type: 'object', - description: - 'Operation-specific arguments. For create: {title, prompt, cron?, time?, timezone?, lifecycle?, successCondition?, maxRuns?}. For get/delete: {jobId}. For update: {jobId, title?, prompt?, cron?, timezone?, status?, lifecycle?, successCondition?, maxRuns?}. For list: no args needed.', - properties: { - cron: { - type: 'string', - description: - "Cron expression for a recurring scheduled task (e.g. '0 9 * * *'). Provide cron, time, or both — with both, time anchors the recurring task's first fire.", - }, - jobId: { type: 'string', description: 'Scheduled task ID (required for get, update)' }, - jobIds: { - type: 'array', - description: 'Array of scheduled task IDs (for batch delete)', - items: { type: 'string' }, - }, - lifecycle: { - type: 'string', - description: - "'persistent' (default) or 'until_complete'. Until_complete scheduled tasks stop when complete_scheduled_task is called.", - enum: ['persistent', 'until_complete'], - }, - maxRuns: { - type: 'integer', - description: 'Max executions before auto-completing. Safety limit.', - }, - prompt: { - type: 'string', - description: 'The prompt to execute when the scheduled task fires', - }, - status: { - type: 'string', - description: 'Scheduled task status: active, paused', - enum: ['active', 'paused'], - }, - successCondition: { - type: 'string', - description: - 'What must happen for the scheduled task to be considered complete (until_complete lifecycle).', - }, - time: { - type: 'string', - description: - "ISO 8601 datetime. One-time scheduled task -> set time and omit cron. May also anchor a recurring cron task's first-fire time.", - }, - timezone: { - type: 'string', - description: 'IANA timezone (e.g. America/New_York). Defaults to UTC.', - }, - title: { - type: 'string', - description: "Short descriptive title for the scheduled task (e.g. 'Email Poller')", - }, - }, - }, - operation: { - type: 'string', - description: - 'The operation to perform: create, list, get, update, delete. These verbs are tool-specific — the custom-tool/MCP/skill managers use add/edit instead of create/update.', - enum: ['create', 'list', 'get', 'update', 'delete'], - }, - }, - required: ['operation'], - }, -} - export const ManageSkill: ToolCatalogEntry = { id: 'manage_skill', name: 'manage_skill', @@ -3703,7 +3584,7 @@ export const ManageSkill: ToolCatalogEntry = { operation: { type: 'string', description: - "The operation to perform: 'add', 'edit', 'list', or 'delete'. These verbs are tool-specific — manage_scheduled_task uses create/update instead of add/edit.", + "The operation to perform: 'add', 'edit', 'list', or 'delete'. These verbs are tool-specific — other manage_* tools may use create/update instead of add/edit.", enum: ['add', 'edit', 'delete', 'list'], }, skillId: { @@ -3883,7 +3764,7 @@ export const OpenResource: ToolCatalogEntry = { type: { type: 'string', description: 'The resource type.', - enum: ['workflow', 'table', 'knowledgebase', 'file', 'log', 'scheduledtask'], + enum: ['workflow', 'table', 'knowledgebase', 'file', 'log'], }, }, required: ['type'], @@ -4318,7 +4199,7 @@ export const RunCode: ToolCatalogEntry = { code: { type: 'string', description: - 'Code to execute. For JS: raw statements auto-wrapped in async context. For Python: full script. For shell: bash script with pre-installed CLI tools. Request each needed secret with an explicit {{VAR_NAME}} reference.', + 'Code to execute. For JS: raw statements auto-wrapped in async context. For Python: full script. For shell: bash script with pre-installed CLI tools. Use each needed secret as {{VAR_NAME}}; the reference resolves to the value exactly as stored.', }, inputs: { type: 'object', @@ -4528,22 +4409,6 @@ export const RunWorkflowUntilBlock: ToolCatalogEntry = { requiresApproval: true, } -export const ScheduledTask: ToolCatalogEntry = { - id: 'scheduled_task', - name: 'scheduled_task', - route: 'subagent', - mode: 'async', - parameters: { - properties: { - request: { description: 'What scheduled task action is needed.', type: 'string' }, - }, - required: ['request'], - type: 'object', - }, - subagentId: 'scheduled_task', - internal: true, -} - export const ScrapePage: ToolCatalogEntry = { id: 'scrape_page', name: 'scrape_page', @@ -5088,25 +4953,6 @@ export const UpdateDeploymentVersion: ToolCatalogEntry = { requiredPermission: 'write', } -export const UpdateScheduledTaskHistory: ToolCatalogEntry = { - id: 'update_scheduled_task_history', - name: 'update_scheduled_task_history', - route: 'sim', - mode: 'async', - parameters: { - type: 'object', - properties: { - jobId: { type: 'string', description: 'The scheduled task ID.' }, - summary: { - type: 'string', - description: - "A concise summary of what was done this run (e.g., 'Sent follow-up emails to 3 leads: Alice, Bob, Carol').", - }, - }, - required: ['jobId', 'summary'], - }, -} - export const UpdateWorkspaceMcpServer: ToolCatalogEntry = { id: 'update_workspace_mcp_server', name: 'update_workspace_mcp_server', @@ -5764,25 +5610,6 @@ export const ManageMcpToolOperationValues = [ ManageMcpToolOperation.list, ] as const -export const ManageScheduledTaskOperation = { - create: 'create', - list: 'list', - get: 'get', - update: 'update', - delete: 'delete', -} as const - -export type ManageScheduledTaskOperation = - (typeof ManageScheduledTaskOperation)[keyof typeof ManageScheduledTaskOperation] - -export const ManageScheduledTaskOperationValues = [ - ManageScheduledTaskOperation.create, - ManageScheduledTaskOperation.list, - ManageScheduledTaskOperation.get, - ManageScheduledTaskOperation.update, - ManageScheduledTaskOperation.delete, -] as const - export const ManageSkillOperation = { add: 'add', edit: 'edit', @@ -5986,7 +5813,6 @@ export const TOOL_CATALOG: Record = { [BrowserWaitFor.id]: BrowserWaitFor, [CallIntegrationTool.id]: CallIntegrationTool, [CheckDeploymentStatus.id]: CheckDeploymentStatus, - [CompleteScheduledTask.id]: CompleteScheduledTask, [Cp.id]: Cp, [CrawlWebsite.id]: CrawlWebsite, [CreateFile.id]: CreateFile, @@ -6016,7 +5842,6 @@ export const TOOL_CATALOG: Record = { [GetDeploymentLog.id]: GetDeploymentLog, [GetPageContents.id]: GetPageContents, [GetPlatformActions.id]: GetPlatformActions, - [GetScheduledTaskLogs.id]: GetScheduledTaskLogs, [GetWorkflowData.id]: GetWorkflowData, [GetWorkflowRunOptions.id]: GetWorkflowRunOptions, [Glob.id]: Glob, @@ -6032,7 +5857,6 @@ export const TOOL_CATALOG: Record = { [ManageCredential.id]: ManageCredential, [ManageCustomTool.id]: ManageCustomTool, [ManageMcpTool.id]: ManageMcpTool, - [ManageScheduledTask.id]: ManageScheduledTask, [ManageSkill.id]: ManageSkill, [MaterializeFile.id]: MaterializeFile, [Media.id]: Media, @@ -6055,7 +5879,6 @@ export const TOOL_CATALOG: Record = { [RunFromBlock.id]: RunFromBlock, [RunWorkflow.id]: RunWorkflow, [RunWorkflowUntilBlock.id]: RunWorkflowUntilBlock, - [ScheduledTask.id]: ScheduledTask, [ScrapePage.id]: ScrapePage, [Search.id]: Search, [SearchDocumentation.id]: SearchDocumentation, @@ -6071,7 +5894,6 @@ export const TOOL_CATALOG: Record = { [Table.id]: Table, [Terminal.id]: Terminal, [UpdateDeploymentVersion.id]: UpdateDeploymentVersion, - [UpdateScheduledTaskHistory.id]: UpdateScheduledTaskHistory, [UpdateWorkspaceMcpServer.id]: UpdateWorkspaceMcpServer, [UserTable.id]: UserTable, [Wait.id]: Wait, diff --git a/apps/sim/lib/copilot/generated/tool-schemas-v1.ts b/apps/sim/lib/copilot/generated/tool-schemas-v1.ts index 3f981bbaccc..d45a647d6e1 100644 --- a/apps/sim/lib/copilot/generated/tool-schemas-v1.ts +++ b/apps/sim/lib/copilot/generated/tool-schemas-v1.ts @@ -1106,19 +1106,6 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - complete_scheduled_task: { - parameters: { - type: 'object', - properties: { - jobId: { - type: 'string', - description: 'The ID of the scheduled task to mark as completed.', - }, - }, - required: ['jobId'], - }, - resultSchema: undefined, - }, cp: { parameters: { type: 'object', @@ -2211,7 +2198,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { code: { type: 'string', description: - 'Code to execute. For JS: raw statements auto-wrapped in async context. For Python: full script. For shell: bash script with pre-installed CLI tools. Request each needed secret with an explicit {{VAR_NAME}} reference.', + 'Code to execute. For JS: raw statements auto-wrapped in async context. For Python: full script. For shell: bash script with pre-installed CLI tools. Use each needed secret as {{VAR_NAME}}; the reference resolves to the value exactly as stored.', }, inputs: { type: 'object', @@ -2921,31 +2908,6 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - get_scheduled_task_logs: { - parameters: { - type: 'object', - properties: { - executionId: { - type: 'string', - description: 'Optional execution ID for a specific run.', - }, - includeDetails: { - type: 'boolean', - description: 'Include tool calls, outputs, and cost details.', - }, - jobId: { - type: 'string', - description: 'The scheduled task (schedule) ID to get logs for.', - }, - limit: { - type: 'number', - description: 'Max number of entries (default: 3, max: 5)', - }, - }, - required: ['jobId'], - }, - resultSchema: undefined, - }, get_workflow_data: { parameters: { type: 'object', @@ -3376,7 +3338,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { operation: { type: 'string', description: - "The operation to perform: 'add', 'edit', 'list', or 'delete'. These verbs are tool-specific — manage_scheduled_task uses create/update instead of add/edit.", + "The operation to perform: 'add', 'edit', 'list', or 'delete'. These verbs are tool-specific — other manage_* tools may use create/update instead of add/edit.", enum: ['add', 'edit', 'delete', 'list'], }, schema: { @@ -3483,7 +3445,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { operation: { type: 'string', description: - "The operation to perform: 'add', 'edit', 'list', or 'delete'. These verbs are tool-specific — manage_scheduled_task uses create/update instead of add/edit.", + "The operation to perform: 'add', 'edit', 'list', or 'delete'. These verbs are tool-specific — other manage_* tools may use create/update instead of add/edit.", enum: ['add', 'edit', 'delete', 'list'], }, serverId: { @@ -3496,81 +3458,6 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - manage_scheduled_task: { - parameters: { - type: 'object', - properties: { - args: { - type: 'object', - description: - 'Operation-specific arguments. For create: {title, prompt, cron?, time?, timezone?, lifecycle?, successCondition?, maxRuns?}. For get/delete: {jobId}. For update: {jobId, title?, prompt?, cron?, timezone?, status?, lifecycle?, successCondition?, maxRuns?}. For list: no args needed.', - properties: { - cron: { - type: 'string', - description: - "Cron expression for a recurring scheduled task (e.g. '0 9 * * *'). Provide cron, time, or both — with both, time anchors the recurring task's first fire.", - }, - jobId: { - type: 'string', - description: 'Scheduled task ID (required for get, update)', - }, - jobIds: { - type: 'array', - description: 'Array of scheduled task IDs (for batch delete)', - items: { - type: 'string', - }, - }, - lifecycle: { - type: 'string', - description: - "'persistent' (default) or 'until_complete'. Until_complete scheduled tasks stop when complete_scheduled_task is called.", - enum: ['persistent', 'until_complete'], - }, - maxRuns: { - type: 'integer', - description: 'Max executions before auto-completing. Safety limit.', - }, - prompt: { - type: 'string', - description: 'The prompt to execute when the scheduled task fires', - }, - status: { - type: 'string', - description: 'Scheduled task status: active, paused', - enum: ['active', 'paused'], - }, - successCondition: { - type: 'string', - description: - 'What must happen for the scheduled task to be considered complete (until_complete lifecycle).', - }, - time: { - type: 'string', - description: - "ISO 8601 datetime. One-time scheduled task -> set time and omit cron. May also anchor a recurring cron task's first-fire time.", - }, - timezone: { - type: 'string', - description: 'IANA timezone (e.g. America/New_York). Defaults to UTC.', - }, - title: { - type: 'string', - description: "Short descriptive title for the scheduled task (e.g. 'Email Poller')", - }, - }, - }, - operation: { - type: 'string', - description: - 'The operation to perform: create, list, get, update, delete. These verbs are tool-specific — the custom-tool/MCP/skill managers use add/edit instead of create/update.', - enum: ['create', 'list', 'get', 'update', 'delete'], - }, - }, - required: ['operation'], - }, - resultSchema: undefined, - }, manage_skill: { parameters: { type: 'object', @@ -3591,7 +3478,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { operation: { type: 'string', description: - "The operation to perform: 'add', 'edit', 'list', or 'delete'. These verbs are tool-specific — manage_scheduled_task uses create/update instead of add/edit.", + "The operation to perform: 'add', 'edit', 'list', or 'delete'. These verbs are tool-specific — other manage_* tools may use create/update instead of add/edit.", enum: ['add', 'edit', 'delete', 'list'], }, skillId: { @@ -3746,7 +3633,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { type: { type: 'string', description: 'The resource type.', - enum: ['workflow', 'table', 'knowledgebase', 'file', 'log', 'scheduledtask'], + enum: ['workflow', 'table', 'knowledgebase', 'file', 'log'], }, }, required: ['type'], @@ -4185,7 +4072,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { code: { type: 'string', description: - 'Code to execute. For JS: raw statements auto-wrapped in async context. For Python: full script. For shell: bash script with pre-installed CLI tools. Request each needed secret with an explicit {{VAR_NAME}} reference.', + 'Code to execute. For JS: raw statements auto-wrapped in async context. For Python: full script. For shell: bash script with pre-installed CLI tools. Use each needed secret as {{VAR_NAME}}; the reference resolves to the value exactly as stored.', }, inputs: { type: 'object', @@ -4384,19 +4271,6 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - scheduled_task: { - parameters: { - properties: { - request: { - description: 'What scheduled task action is needed.', - type: 'string', - }, - }, - required: ['request'], - type: 'object', - }, - resultSchema: undefined, - }, scrape_page: { parameters: { type: 'object', @@ -4921,24 +4795,6 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - update_scheduled_task_history: { - parameters: { - type: 'object', - properties: { - jobId: { - type: 'string', - description: 'The scheduled task ID.', - }, - summary: { - type: 'string', - description: - "A concise summary of what was done this run (e.g., 'Sent follow-up emails to 3 leads: Alice, Bob, Carol').", - }, - }, - required: ['jobId', 'summary'], - }, - resultSchema: undefined, - }, update_workspace_mcp_server: { parameters: { type: 'object', diff --git a/apps/sim/lib/copilot/generated/vfs-snapshot-v1.ts b/apps/sim/lib/copilot/generated/vfs-snapshot-v1.ts index 37486a75078..62f614b2d22 100644 --- a/apps/sim/lib/copilot/generated/vfs-snapshot-v1.ts +++ b/apps/sim/lib/copilot/generated/vfs-snapshot-v1.ts @@ -10,7 +10,6 @@ export interface VfsSnapshotV1 { envVars?: string[] files?: VfsSnapshotV1File[] integrations?: VfsSnapshotV1Integration[] - jobs?: VfsSnapshotV1Job[] knowledgeBases?: VfsSnapshotV1KnowledgeBase[] mcpServers?: VfsSnapshotV1McpServer[] members?: VfsSnapshotV1Member[] @@ -58,19 +57,6 @@ export interface VfsSnapshotV1Integration { providerId: string role?: string } -/** - * This interface was referenced by `VfsSnapshotV1`'s JSON-Schema - * via the `definition` "VfsSnapshotV1Job". - */ -export interface VfsSnapshotV1Job { - cronExpression?: string - id: string - lifecycle?: string - prompt?: string - sourceTaskName?: string - status?: string - title?: string -} /** * This interface was referenced by `VfsSnapshotV1`'s JSON-Schema * via the `definition` "VfsSnapshotV1KnowledgeBase". diff --git a/apps/sim/lib/copilot/resources/extraction.test.ts b/apps/sim/lib/copilot/resources/extraction.test.ts index 150c8b217b7..65e8d40bc61 100644 --- a/apps/sim/lib/copilot/resources/extraction.test.ts +++ b/apps/sim/lib/copilot/resources/extraction.test.ts @@ -141,43 +141,6 @@ describe('extractResourcesFromToolResult', () => { expect(resources).toEqual([]) }) - - it('auto-opens a scheduledtask resource from manage_scheduled_task create results', () => { - const resources = extractResourcesFromToolResult( - 'manage_scheduled_task', - { operation: 'create', args: { title: 'Daily Report' } }, - { jobId: 'sched_123', title: 'Daily Report', message: 'Job created successfully.' } - ) - - expect(resources).toEqual([{ type: 'scheduledtask', id: 'sched_123', title: 'Daily Report' }]) - }) - - it('auto-opens a scheduledtask resource on update, falling back to the args title', () => { - const resources = extractResourcesFromToolResult( - 'manage_scheduled_task', - { operation: 'update', args: { jobId: 'sched_123', title: 'Renamed Task' } }, - { jobId: 'sched_123', updated: ['title'], message: 'Job updated successfully' } - ) - - expect(resources).toEqual([{ type: 'scheduledtask', id: 'sched_123', title: 'Renamed Task' }]) - }) - - it('does not auto-open for read-only manage_scheduled_task operations', () => { - expect( - extractResourcesFromToolResult( - 'manage_scheduled_task', - { operation: 'list' }, - { jobs: [], count: 0 } - ) - ).toEqual([]) - expect( - extractResourcesFromToolResult( - 'manage_scheduled_task', - { operation: 'get', args: { jobId: 'sched_123' } }, - { id: 'sched_123', title: 'Daily Report' } - ) - ).toEqual([]) - }) }) describe('extractDeletedResourcesFromToolResult', () => { @@ -230,27 +193,4 @@ describe('extractDeletedResourcesFromToolResult', () => { ) ).toEqual([{ type: 'knowledgebase', id: 'kb-1', title: 'Docs' }]) }) - - it('removes scheduledtask resources on manage_scheduled_task delete', () => { - const resources = extractDeletedResourcesFromToolResult( - 'manage_scheduled_task', - { operation: 'delete', args: { jobIds: ['sched_1', 'sched_2'] } }, - { deleted: ['sched_1', 'sched_2'], notFound: [] } - ) - - expect(resources).toEqual([ - { type: 'scheduledtask', id: 'sched_1', title: 'Scheduled Task' }, - { type: 'scheduledtask', id: 'sched_2', title: 'Scheduled Task' }, - ]) - }) - - it('does not remove anything for non-delete manage_scheduled_task ops', () => { - expect( - extractDeletedResourcesFromToolResult( - 'manage_scheduled_task', - { operation: 'update', args: { jobId: 'sched_1' } }, - { jobId: 'sched_1', updated: ['title'] } - ) - ).toEqual([]) - }) }) diff --git a/apps/sim/lib/copilot/resources/extraction.ts b/apps/sim/lib/copilot/resources/extraction.ts index ddd7b4f52db..2a614d944b6 100644 --- a/apps/sim/lib/copilot/resources/extraction.ts +++ b/apps/sim/lib/copilot/resources/extraction.ts @@ -10,7 +10,6 @@ import { GenerateVideo, Knowledge, KnowledgeBase, - ManageScheduledTask, Rm, UserTable, WorkspaceFile, @@ -30,7 +29,6 @@ const RESOURCE_TOOL_NAMES: Set = new Set([ FunctionExecute.id, KnowledgeBase.id, Knowledge.id, - ManageScheduledTask.id, GenerateImage.id, GenerateVideo.id, GenerateAudio.id, @@ -221,19 +219,6 @@ export function extractResourcesFromToolResult( return resources } - case ManageScheduledTask.id: { - // Read-only ops never auto-open; only create/update surface the task. - const op = getOperation(params) - if (op === 'list' || op === 'get') return [] - const jobId = (result.jobId as string) ?? (data.jobId as string) - if (jobId) { - const args = asRecord(params?.args) - const title = (result.title as string) ?? (args.title as string) ?? 'Scheduled Task' - return [{ type: 'scheduledtask', id: jobId, title }] - } - return [] - } - default: return [] } @@ -243,7 +228,6 @@ const DELETE_CAPABLE_TOOL_RESOURCE_TYPE: Record = { [WorkspaceFile.id]: 'file', [UserTable.id]: 'table', [KnowledgeBase.id]: 'knowledgebase', - [ManageScheduledTask.id]: 'scheduledtask', // rm spans categories, so unlike every other entry its resource type comes // from each outcome's kind rather than from this map. The entry exists so // hasDeleteCapability(rm) holds; the rm case below ignores this value. @@ -348,12 +332,6 @@ export function extractDeletedResourcesFromToolResult( return [] } - case ManageScheduledTask.id: { - if (operation !== 'delete') return [] - const deletedIds = Array.isArray(result.deleted) ? (result.deleted as string[]) : [] - return deletedIds.map((id) => ({ type: resourceType, id, title: 'Scheduled Task' })) - } - default: return [] } diff --git a/apps/sim/lib/copilot/resources/types.ts b/apps/sim/lib/copilot/resources/types.ts index 718903b4ccd..6e78ddd5ebd 100644 --- a/apps/sim/lib/copilot/resources/types.ts +++ b/apps/sim/lib/copilot/resources/types.ts @@ -6,7 +6,6 @@ export const MothershipResourceType = { folder: 'folder', filefolder: 'filefolder', task: 'task', - scheduledtask: 'scheduledtask', log: 'log', integration: 'integration', generic: 'generic', @@ -54,7 +53,6 @@ const RESOURCE_POLICY: Record = { folder: { persisted: true }, filefolder: { persisted: true }, task: { persisted: true }, - scheduledtask: { persisted: true }, log: { persisted: true }, integration: { persisted: true }, // A synthetic panel with no addressable entity behind it to reopen. @@ -147,7 +145,6 @@ export const GENERIC_RESOURCE_TITLES = new Set([ 'Workflow', 'Knowledge Base', 'Folder', - 'Scheduled Task', 'Log', ]) @@ -157,5 +154,4 @@ export const VFS_DIR_TO_RESOURCE: Record = { workflows: 'workflow', knowledgebases: 'knowledgebase', folders: 'folder', - jobs: 'scheduledtask', } as const diff --git a/apps/sim/lib/copilot/tool-executor/register-handlers.ts b/apps/sim/lib/copilot/tool-executor/register-handlers.ts index 29e18387f7c..6b434d65892 100644 --- a/apps/sim/lib/copilot/tool-executor/register-handlers.ts +++ b/apps/sim/lib/copilot/tool-executor/register-handlers.ts @@ -1,7 +1,6 @@ import { createLogger } from '@sim/logger' import { CheckDeploymentStatus, - CompleteScheduledTask, Cp as CpTool, CreateWorkflow, CreateWorkspaceMcpServer, @@ -29,7 +28,6 @@ import { ManageCredential, ManageCustomTool, ManageMcpTool, - ManageScheduledTask, ManageSkill, MaterializeFile, Mkdir as MkdirTool, @@ -50,7 +48,6 @@ import { SetBlockEnabled, SetGlobalWorkflowVariables, UpdateDeploymentVersion, - UpdateScheduledTaskHistory, UpdateWorkspaceMcpServer, } from '@/lib/copilot/generated/tool-catalog-v1' import { createServerToolHandler } from '@/lib/copilot/tools/registry/server-tool-adapter' @@ -76,11 +73,6 @@ import { } from '../tools/handlers/deployment/manage' import { executeFunctionExecute } from '../tools/handlers/function-execute' import { executeListIntegrationTools } from '../tools/handlers/integration-tools' -import { - executeCompleteJob, - executeManageJob, - executeUpdateJobHistory, -} from '../tools/handlers/jobs' import { executeManageCredential } from '../tools/handlers/management/manage-credential' import { executeManageCustomTool } from '../tools/handlers/management/manage-custom-tool' import { executeManageMcpTool } from '../tools/handlers/management/manage-mcp-tool' @@ -178,10 +170,6 @@ function buildHandlerMap(): Record { [PromoteToLive.id]: h(executePromoteToLive), [UpdateDeploymentVersion.id]: h(executeUpdateDeploymentVersion), - [ManageScheduledTask.id]: h(executeManageJob), - [CompleteScheduledTask.id]: h(executeCompleteJob), - [UpdateScheduledTaskHistory.id]: h(executeUpdateJobHistory), - [GrepTool.id]: h(executeVfsGrep), [GlobTool.id]: h(executeVfsGlob), [ReadTool.id]: h(executeVfsRead), diff --git a/apps/sim/lib/copilot/tools/client/store-utils.ts b/apps/sim/lib/copilot/tools/client/store-utils.ts index 92bbeb6c572..343c9e2712d 100644 --- a/apps/sim/lib/copilot/tools/client/store-utils.ts +++ b/apps/sim/lib/copilot/tools/client/store-utils.ts @@ -1,6 +1,6 @@ import type { ComponentType } from 'react' import { Loader } from '@sim/emcn' -import { FileText } from 'lucide-react' +import { FileText } from '@sim/emcn/icons' import { Read as ReadTool } from '@/lib/copilot/generated/tool-catalog-v1' import { VFS_DIR_TO_RESOURCE } from '@/lib/copilot/resources/types' import { isToolHiddenInUi } from '@/lib/copilot/tools/client/hidden-tools' diff --git a/apps/sim/lib/copilot/tools/handlers/jobs.ts b/apps/sim/lib/copilot/tools/handlers/jobs.ts deleted file mode 100644 index b9036a7e132..00000000000 --- a/apps/sim/lib/copilot/tools/handlers/jobs.ts +++ /dev/null @@ -1,445 +0,0 @@ -import { db } from '@sim/db' -import { copilotChats, workflowSchedule } from '@sim/db/schema' -import { createLogger } from '@sim/logger' -import { toError } from '@sim/utils/errors' -import { and, eq, isNull } from 'drizzle-orm' -import { z } from 'zod' -import type { ExecutionContext, ToolCallResult } from '@/lib/copilot/request/types' -import { - performCompleteJob, - performCreateJob, - performDeleteJob, - performUpdateJob, -} from '@/lib/workflows/schedules/orchestration' - -const logger = createLogger('JobTools') - -const ACTIVE_JOB_CONDITION = (workspaceId: string) => - and( - eq(workflowSchedule.sourceWorkspaceId, workspaceId), - eq(workflowSchedule.sourceType, 'job'), - isNull(workflowSchedule.archivedAt) - ) - -const JobLifecycleSchema = z.enum(['persistent', 'until_complete']) - -const CreateJobParamsSchema = z - .object({ - title: z.string().optional(), - prompt: z.string().optional(), - cron: z.string().optional(), - time: z.string().optional(), - timezone: z.string().optional(), - lifecycle: JobLifecycleSchema.optional(), - successCondition: z.string().optional(), - maxRuns: z.number().optional(), - }) - .passthrough() - -const ManageJobArgsSchema = z - .object({ - jobId: z.string().optional(), - jobIds: z.array(z.string()).optional(), - title: z.string().optional(), - prompt: z.string().optional(), - cron: z.string().optional(), - time: z.string().optional(), - timezone: z.string().optional(), - status: z.string().optional(), - lifecycle: z.string().optional(), - successCondition: z.string().optional(), - maxRuns: z.number().optional(), - }) - .passthrough() - -const ManageJobParamsSchema = z - .object({ - operation: z.string().optional(), - args: ManageJobArgsSchema.optional(), - }) - .passthrough() - -type CreateJobParams = z.infer -type ManageJobParams = z.infer - -export async function executeCreateJob( - params: Record, - context: ExecutionContext -): Promise { - const parsedParams = CreateJobParamsSchema.safeParse(params) - if (!parsedParams.success) { - return { success: false, error: 'Invalid create job parameters' } - } - - const rawParams: CreateJobParams = parsedParams.data - const timezone = rawParams.timezone || context.userTimezone || 'UTC' - const { title, prompt, cron, time, lifecycle, successCondition, maxRuns } = rawParams - - if (!prompt) { - return { success: false, error: 'prompt is required' } - } - - if (!cron && !time) { - return { success: false, error: 'At least one of cron or time must be provided' } - } - - if (!context.userId || !context.workspaceId) { - return { success: false, error: 'Missing user or workspace context' } - } - - let taskName: string | null = null - if (context.chatId) { - try { - const [chat] = await db - .select({ title: copilotChats.title }) - .from(copilotChats) - .where(eq(copilotChats.id, context.chatId)) - .limit(1) - taskName = chat?.title || null - } catch (err) { - logger.warn('Failed to look up chat title for job', { - chatId: context.chatId, - error: toError(err).message, - }) - } - } - - try { - const result = await performCreateJob({ - workspaceId: context.workspaceId, - userId: context.userId, - title, - prompt, - cronExpression: cron, - time, - timezone, - lifecycle, - successCondition, - maxRuns, - sourceChatId: context.chatId, - sourceTaskName: taskName, - }) - if (!result.success || !result.schedule) { - return { success: false, error: result.error || 'Failed to create job' } - } - - return { - success: true, - output: { - jobId: result.schedule.id, - title: result.schedule.jobTitle, - schedule: result.humanReadable, - nextRunAt: result.schedule.nextRunAt?.toISOString(), - message: `Job created successfully. ${result.humanReadable}`, - }, - } - } catch (err) { - logger.error('Failed to create job', { - error: toError(err).message, - }) - return { success: false, error: 'Failed to create job' } - } -} - -export async function executeManageJob( - params: Record, - context: ExecutionContext -): Promise { - const parsedParams = ManageJobParamsSchema.safeParse(params) - if (!parsedParams.success) { - return { success: false, error: 'Invalid manage job parameters' } - } - - const rawParams: ManageJobParams = parsedParams.data - const { operation, args } = rawParams - - if (!context.userId || !context.workspaceId) { - return { success: false, error: 'Missing user or workspace context' } - } - - switch (operation) { - case 'create': { - return executeCreateJob( - { - title: args?.title, - prompt: args?.prompt, - cron: args?.cron, - time: args?.time, - timezone: args?.timezone, - lifecycle: args?.lifecycle, - successCondition: args?.successCondition, - maxRuns: args?.maxRuns, - } as Record, - context - ) - } - - case 'list': { - try { - const jobs = await db - .select({ - id: workflowSchedule.id, - jobTitle: workflowSchedule.jobTitle, - prompt: workflowSchedule.prompt, - cronExpression: workflowSchedule.cronExpression, - timezone: workflowSchedule.timezone, - status: workflowSchedule.status, - lifecycle: workflowSchedule.lifecycle, - successCondition: workflowSchedule.successCondition, - maxRuns: workflowSchedule.maxRuns, - runCount: workflowSchedule.runCount, - nextRunAt: workflowSchedule.nextRunAt, - lastRanAt: workflowSchedule.lastRanAt, - sourceTaskName: workflowSchedule.sourceTaskName, - createdAt: workflowSchedule.createdAt, - }) - .from(workflowSchedule) - .where(ACTIVE_JOB_CONDITION(context.workspaceId)) - - return { - success: true, - output: { - jobs: jobs.map((j) => ({ - id: j.id, - title: j.jobTitle, - prompt: j.prompt, - cronExpression: j.cronExpression, - timezone: j.timezone, - status: j.status, - lifecycle: j.lifecycle, - successCondition: j.successCondition, - maxRuns: j.maxRuns, - runCount: j.runCount, - nextRunAt: j.nextRunAt?.toISOString(), - lastRanAt: j.lastRanAt?.toISOString(), - sourceTaskName: j.sourceTaskName, - createdAt: j.createdAt.toISOString(), - })), - count: jobs.length, - }, - } - } catch (err) { - logger.error('Failed to list jobs', { - error: toError(err).message, - }) - return { success: false, error: 'Failed to list jobs' } - } - } - - case 'get': { - if (!args?.jobId) { - return { success: false, error: 'jobId is required for get operation' } - } - - try { - const [job] = await db - .select() - .from(workflowSchedule) - .where( - and(eq(workflowSchedule.id, args.jobId), ACTIVE_JOB_CONDITION(context.workspaceId)) - ) - .limit(1) - - if (!job) { - return { success: false, error: `Job not found: ${args.jobId}` } - } - - return { - success: true, - output: { - id: job.id, - title: job.jobTitle, - prompt: job.prompt, - cronExpression: job.cronExpression, - timezone: job.timezone, - status: job.status, - lifecycle: job.lifecycle, - successCondition: job.successCondition, - maxRuns: job.maxRuns, - runCount: job.runCount, - nextRunAt: job.nextRunAt?.toISOString(), - lastRanAt: job.lastRanAt?.toISOString(), - sourceTaskName: job.sourceTaskName, - sourceChatId: job.sourceChatId, - createdAt: job.createdAt.toISOString(), - }, - } - } catch (err) { - logger.error('Failed to get job', { - error: toError(err).message, - }) - return { success: false, error: 'Failed to get job' } - } - } - - case 'update': { - if (!args?.jobId) { - return { success: false, error: 'jobId is required for update operation' } - } - - try { - const result = await performUpdateJob({ - jobId: args.jobId, - workspaceId: context.workspaceId, - userId: context.userId, - title: args.title, - prompt: args.prompt, - cronExpression: args.cron, - time: args.time, - timezone: args.timezone, - status: args.status, - lifecycle: args.lifecycle, - successCondition: args.successCondition, - maxRuns: args.maxRuns, - }) - if (!result.success) { - return { success: false, error: result.error || 'Failed to update job' } - } - - return { - success: true, - output: { - jobId: args.jobId, - updated: result.updatedFields || [], - message: 'Job updated successfully', - }, - } - } catch (err) { - logger.error('Failed to update job', { - error: toError(err).message, - }) - return { success: false, error: 'Failed to update job' } - } - } - - case 'delete': { - const jobIds = args?.jobIds ?? (args?.jobId ? [args.jobId] : []) - if (jobIds.length === 0) { - return { success: false, error: 'jobId or jobIds is required for delete operation' } - } - - try { - const deleted: string[] = [] - const notFound: string[] = [] - - for (const jobId of jobIds) { - const result = await performDeleteJob({ - jobId, - workspaceId: context.workspaceId, - userId: context.userId, - }) - if (!result.success) { - notFound.push(jobId) - continue - } - deleted.push(jobId) - } - - return { - success: deleted.length > 0, - output: { deleted, notFound }, - } - } catch (err) { - logger.error('Failed to delete job', { - error: toError(err).message, - }) - return { success: false, error: 'Failed to delete job' } - } - } - - default: - return { success: false, error: `Unknown operation: ${operation}` } - } -} - -export async function executeCompleteJob( - params: Record, - context: ExecutionContext -): Promise { - const { jobId } = params as { jobId?: string } - - if (!jobId) { - return { success: false, error: 'jobId is required' } - } - - try { - if (!context.workspaceId) { - return { success: false, error: 'Missing workspace context' } - } - - const result = await performCompleteJob({ - jobId, - workspaceId: context.workspaceId, - userId: context.userId, - }) - if (!result.success) { - return { success: false, error: result.error || 'Failed to complete job' } - } - if (result.alreadyCompleted) { - return { - success: true, - output: { jobId, message: 'Job is already completed' }, - } - } - - return { - success: true, - output: { jobId, message: 'Job marked as completed. No further executions will occur.' }, - } - } catch (err) { - logger.error('Failed to complete job', { - error: toError(err).message, - }) - return { success: false, error: 'Failed to complete job' } - } -} - -export async function executeUpdateJobHistory( - params: Record, - context: ExecutionContext -): Promise { - const { jobId, summary } = params as { jobId?: string; summary?: string } - - if (!jobId || !summary) { - return { success: false, error: 'jobId and summary are required' } - } - - if (!context.workspaceId) { - return { success: false, error: 'Missing workspace context' } - } - - try { - const [job] = await db - .select({ - id: workflowSchedule.id, - jobHistory: workflowSchedule.jobHistory, - }) - .from(workflowSchedule) - .where(and(eq(workflowSchedule.id, jobId), ACTIVE_JOB_CONDITION(context.workspaceId))) - .limit(1) - - if (!job) { - return { success: false, error: `Job not found: ${jobId}` } - } - - const existing = (job.jobHistory || []) as Array<{ timestamp: string; summary: string }> - const updated = [...existing, { timestamp: new Date().toISOString(), summary }].slice(-50) - - await db - .update(workflowSchedule) - .set({ jobHistory: updated, updatedAt: new Date() }) - .where(and(eq(workflowSchedule.id, jobId), isNull(workflowSchedule.archivedAt))) - - logger.info('Job history updated', { jobId, entryCount: updated.length }) - - return { - success: true, - output: { jobId, entryCount: updated.length, message: 'History entry recorded.' }, - } - } catch (err) { - logger.error('Failed to update job history', { - error: toError(err).message, - }) - return { success: false, error: 'Failed to update job history' } - } -} diff --git a/apps/sim/lib/copilot/tools/handlers/resources.ts b/apps/sim/lib/copilot/tools/handlers/resources.ts index e7a970f3992..52ea670cf74 100644 --- a/apps/sim/lib/copilot/tools/handlers/resources.ts +++ b/apps/sim/lib/copilot/tools/handlers/resources.ts @@ -1,6 +1,3 @@ -import { db } from '@sim/db' -import { workflowSchedule } from '@sim/db/schema' -import { and, eq, isNull } from 'drizzle-orm' import type { ExecutionContext, ToolCallResult } from '@/lib/copilot/request/types' import { type MothershipResource, MothershipResourceType } from '@/lib/copilot/resources/types' import { canonicalWorkspaceFilePath } from '@/lib/copilot/vfs/path-utils' @@ -86,27 +83,6 @@ async function resolveResource( }) title = `${workflowName} — ${timestamp}` } - if (resourceType === 'scheduledtask') { - if (!item.id) return { error: 'scheduledtask resources require `id`.' } - if (!context.workspaceId) - return { error: 'Opening a scheduled task requires workspace context.' } - const [schedule] = await db - .select({ id: workflowSchedule.id, jobTitle: workflowSchedule.jobTitle }) - .from(workflowSchedule) - .where( - and( - eq(workflowSchedule.id, item.id), - eq(workflowSchedule.sourceWorkspaceId, context.workspaceId), - eq(workflowSchedule.sourceType, 'job'), - isNull(workflowSchedule.archivedAt) - ) - ) - .limit(1) - if (!schedule) return { error: `No scheduled task with id "${item.id}".` } - resourceId = schedule.id - title = schedule.jobTitle || 'Scheduled Task' - } - return { type: resourceType, id: resourceId, title } } diff --git a/apps/sim/lib/copilot/tools/server/jobs/get-job-logs.test.ts b/apps/sim/lib/copilot/tools/server/jobs/get-job-logs.test.ts deleted file mode 100644 index a77b0a606cb..00000000000 --- a/apps/sim/lib/copilot/tools/server/jobs/get-job-logs.test.ts +++ /dev/null @@ -1,131 +0,0 @@ -/** - * @vitest-environment node - */ -import { dbChainMockFns, resetDbChainMock } from '@sim/testing' -import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' - -const { checkWorkspaceAccessMock, materializeExecutionDataForDisplayMock } = vi.hoisted(() => ({ - checkWorkspaceAccessMock: vi.fn(), - materializeExecutionDataForDisplayMock: vi.fn(), -})) - -vi.mock('@/lib/workspaces/permissions/utils', () => ({ - checkWorkspaceAccess: checkWorkspaceAccessMock, -})) - -vi.mock('@/lib/logs/execution/trace-store', () => ({ - materializeExecutionDataForDisplay: materializeExecutionDataForDisplayMock, -})) - -import { getJobLogsServerTool } from './get-job-logs' - -const RAW_SECRET = 'sk-job-log-secret' -const MASKED_SECRET = '{{OPENAI_API_KEY}}' -const CONTEXT = { userId: 'user-1', workspaceId: 'workspace-1' } - -function jobLogRow(overrides: Record = {}) { - return { - id: 'log-1', - executionId: 'execution-1', - status: 'success', - level: 'info', - trigger: 'schedule', - startedAt: new Date('2026-07-31T00:00:00.000Z'), - endedAt: new Date('2026-07-31T00:00:01.000Z'), - totalDurationMs: 1000, - executionData: { - finalOutput: { result: RAW_SECRET }, - traceSpans: [{ id: 'span-1', output: { result: RAW_SECRET } }], - }, - cost: null, - ...overrides, - } -} - -describe('getJobLogsServerTool', () => { - afterAll(resetDbChainMock) - - beforeEach(() => { - vi.clearAllMocks() - resetDbChainMock() - checkWorkspaceAccessMock.mockResolvedValue({ hasAccess: true }) - }) - - it('returns the secret-safe log projection instead of raw successful output', async () => { - const row = jobLogRow() - dbChainMockFns.limit.mockResolvedValueOnce([row]) - materializeExecutionDataForDisplayMock.mockResolvedValueOnce({ - finalOutput: { result: MASKED_SECRET }, - traceSpans: [ - { - id: 'span-1', - name: 'Function 1', - type: 'function', - status: 'success', - duration: 1000, - startTime: '2026-07-31T00:00:00.000Z', - endTime: '2026-07-31T00:00:01.000Z', - output: { result: MASKED_SECRET }, - }, - ], - }) - - const result = await getJobLogsServerTool.execute( - { jobId: 'schedule-1', includeDetails: true }, - CONTEXT - ) - - expect(result).toEqual([ - expect.objectContaining({ - executionId: 'execution-1', - output: { result: MASKED_SECRET }, - }), - ]) - expect(JSON.stringify(result)).not.toContain(RAW_SECRET) - expect(materializeExecutionDataForDisplayMock).toHaveBeenCalledWith(row.executionData, { - workspaceId: 'workspace-1', - workflowId: null, - executionId: 'execution-1', - userId: 'user-1', - }) - }) - - it('does not fall back to raw output or errors when provenance is incomplete', async () => { - dbChainMockFns.limit.mockResolvedValueOnce([ - jobLogRow({ - status: 'error', - executionData: { - finalOutput: { error: RAW_SECRET }, - error: RAW_SECRET, - traceSpans: [{ id: 'span-1', output: { error: RAW_SECRET } }], - }, - }), - ]) - materializeExecutionDataForDisplayMock.mockResolvedValueOnce({ - traceSpans: [ - { - id: 'span-1', - name: 'Function 1', - type: 'function', - status: 'error', - duration: 1000, - startTime: '2026-07-31T00:00:00.000Z', - endTime: '2026-07-31T00:00:01.000Z', - }, - ], - }) - - const result = await getJobLogsServerTool.execute( - { jobId: 'schedule-1', includeDetails: true }, - CONTEXT - ) - - expect(result).toEqual([ - expect.not.objectContaining({ - output: expect.anything(), - error: expect.anything(), - }), - ]) - expect(JSON.stringify(result)).not.toContain(RAW_SECRET) - }) -}) diff --git a/apps/sim/lib/copilot/tools/server/jobs/get-job-logs.ts b/apps/sim/lib/copilot/tools/server/jobs/get-job-logs.ts deleted file mode 100644 index f8c9c60c9bb..00000000000 --- a/apps/sim/lib/copilot/tools/server/jobs/get-job-logs.ts +++ /dev/null @@ -1,242 +0,0 @@ -import { db } from '@sim/db' -import { jobExecutionLogs } from '@sim/db/schema' -import { createLogger } from '@sim/logger' -import { and, desc, eq } from 'drizzle-orm' -import { GetScheduledTaskLogs } from '@/lib/copilot/generated/tool-catalog-v1' -import type { BaseServerTool, ServerToolContext } from '@/lib/copilot/tools/server/base-tool' -import { materializeExecutionDataForDisplay } from '@/lib/logs/execution/trace-store' -import type { TraceSpan } from '@/lib/logs/types' -import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils' - -const logger = createLogger('GetJobLogsServerTool') - -interface GetJobLogsArgs { - jobId: string - executionId?: string - limit?: number - includeDetails?: boolean - workspaceId?: string -} - -interface ToolCallDetail { - name: string - input: unknown - output: unknown - error?: string - duration: number -} - -interface JobLogEntry { - executionId: string - status: string - trigger: string - startedAt: string - endedAt: string | null - durationMs: number | null - error?: string - toolCalls?: ToolCallDetail[] - output?: unknown - cost?: unknown - tokens?: unknown -} - -/** - * Walks the trace-span tree and collects tool invocations from both data shapes: - * - New: `type: 'tool'` spans nested under agent blocks in `children`. - * - Legacy: a `toolCalls` array hanging off the agent span directly (pre-unification). - */ -function collectToolCalls(spans: TraceSpan[] | undefined): ToolCallDetail[] { - if (!spans?.length) return [] - const collected: ToolCallDetail[] = [] - - const visit = (span: TraceSpan) => { - if (span.type === 'tool') { - const output = span.output as { result?: unknown } | undefined - collected.push({ - name: span.name || 'unknown', - input: span.input ?? {}, - output: output?.result ?? span.output, - error: span.status === 'error' ? errorMessageFromSpan(span) : undefined, - duration: span.duration || 0, - }) - return - } - - if (span.toolCalls?.length) { - for (const tc of span.toolCalls) { - collected.push({ - name: tc.name || 'unknown', - input: tc.input ?? {}, - output: tc.output ?? undefined, - error: tc.error || undefined, - duration: tc.duration || 0, - }) - } - } - - if (span.children?.length) { - for (const child of span.children) visit(child) - } - } - - for (const span of spans) visit(span) - return collected -} - -function errorMessageFromSpan(span: TraceSpan): string | undefined { - const out = span.output as { error?: unknown } | undefined - if (typeof out?.error === 'string') return out.error - return undefined -} - -function extractOutputAndError( - executionData: { traceSpans?: TraceSpan[] } & Record -): { - output: unknown - error: string | undefined - toolCalls: ToolCallDetail[] - cost: unknown - tokens: unknown -} { - const traceSpans = executionData?.traceSpans ?? [] - const mainSpan = traceSpans[0] - - const toolCalls = collectToolCalls(traceSpans) - const output = mainSpan?.output || executionData?.finalOutput || undefined - const cost = mainSpan?.cost || executionData?.cost || undefined - const tokens = mainSpan?.tokens || undefined - - const errorMsg = - mainSpan?.status === 'error' - ? mainSpan?.output?.error || executionData?.error - : executionData?.error || undefined - - return { - output, - error: errorMsg - ? typeof errorMsg === 'string' - ? errorMsg - : JSON.stringify(errorMsg) - : undefined, - toolCalls, - cost, - tokens, - } -} - -export const getJobLogsServerTool: BaseServerTool = { - name: GetScheduledTaskLogs.id, - async execute(rawArgs: GetJobLogsArgs, context?: ServerToolContext): Promise { - const withMessageId = (message: string) => - context?.messageId ? `${message} [messageId:${context.messageId}]` : message - - const { - jobId, - executionId, - limit = 3, - includeDetails = false, - workspaceId, - } = rawArgs || ({} as GetJobLogsArgs) - - if (!jobId || typeof jobId !== 'string') { - throw new Error('jobId is required') - } - if (!context?.userId) { - throw new Error('Unauthorized access') - } - - const wsId = workspaceId || context.workspaceId - if (!wsId) { - throw new Error('Workspace context required') - } - const access = await checkWorkspaceAccess(wsId, context.userId) - if (!access.hasAccess) { - throw new Error('Unauthorized workspace access') - } - - const clampedLimit = Math.min(Math.max(1, limit), 5) - - logger.info('Fetching job logs', { - jobId, - executionId, - limit: clampedLimit, - includeDetails, - }) - - const conditions = [ - eq(jobExecutionLogs.scheduleId, jobId), - eq(jobExecutionLogs.workspaceId, wsId), - ] - if (executionId) { - conditions.push(eq(jobExecutionLogs.executionId, executionId)) - } - - const rows = await db - .select({ - id: jobExecutionLogs.id, - executionId: jobExecutionLogs.executionId, - status: jobExecutionLogs.status, - level: jobExecutionLogs.level, - trigger: jobExecutionLogs.trigger, - startedAt: jobExecutionLogs.startedAt, - endedAt: jobExecutionLogs.endedAt, - totalDurationMs: jobExecutionLogs.totalDurationMs, - executionData: jobExecutionLogs.executionData, - cost: jobExecutionLogs.cost, - }) - .from(jobExecutionLogs) - .where(and(...conditions)) - .orderBy(desc(jobExecutionLogs.startedAt)) - .limit(executionId ? 1 : clampedLimit) - - const entries: JobLogEntry[] = await Promise.all( - rows.map(async (row) => { - const executionData = await materializeExecutionDataForDisplay( - row.executionData as Record | null, - { - workspaceId: wsId, - workflowId: null, - executionId: row.executionId, - userId: context.userId, - } - ) - const details = includeDetails ? extractOutputAndError(executionData) : null - - const entry: JobLogEntry = { - executionId: row.executionId, - status: row.status, - trigger: row.trigger, - startedAt: row.startedAt.toISOString(), - endedAt: row.endedAt ? row.endedAt.toISOString() : null, - durationMs: row.totalDurationMs ?? null, - } - - if (details) { - if (details.error) entry.error = details.error - if (details.toolCalls.length > 0) entry.toolCalls = details.toolCalls - if (details.output) entry.output = details.output - if (details.cost) entry.cost = details.cost - if (details.tokens) entry.tokens = details.tokens - } else { - const traceSpans = Array.isArray(executionData.traceSpans) - ? (executionData.traceSpans as TraceSpan[]) - : [] - const errorMsg = executionData.error || traceSpans[0]?.output?.error - if (row.status === 'error' && errorMsg) { - entry.error = typeof errorMsg === 'string' ? errorMsg : JSON.stringify(errorMsg) - } - } - - return entry - }) - ) - - logger.info('Job logs prepared', { - jobId, - count: entries.length, - resultSizeKB: Math.round(JSON.stringify(entries).length / 1024), - }) - - return entries - }, -} diff --git a/apps/sim/lib/copilot/tools/server/router.ts b/apps/sim/lib/copilot/tools/server/router.ts index 8a99814333f..e1067314263 100644 --- a/apps/sim/lib/copilot/tools/server/router.ts +++ b/apps/sim/lib/copilot/tools/server/router.ts @@ -41,7 +41,6 @@ import { shareFileServerTool } from '@/lib/copilot/tools/server/files/share-file import { workspaceFileServerTool } from '@/lib/copilot/tools/server/files/workspace-file' import { validateGeneratedToolPayload } from '@/lib/copilot/tools/server/generated-schema' import { generateImageServerTool } from '@/lib/copilot/tools/server/image/generate-image' -import { getJobLogsServerTool } from '@/lib/copilot/tools/server/jobs/get-job-logs' import { knowledgeBaseServerTool } from '@/lib/copilot/tools/server/knowledge/knowledge-base' import { searchKnowledgeBaseServerTool } from '@/lib/copilot/tools/server/knowledge/search-knowledge-base' import { ffmpegServerTool } from '@/lib/copilot/tools/server/media/ffmpeg' @@ -154,7 +153,6 @@ const baseServerToolRegistry: Record = { [getTriggerBlocksServerTool.name]: getTriggerBlocksServerTool, [editWorkflowServerTool.name]: editWorkflowServerTool, [queryLogsServerTool.name]: queryLogsServerTool, - [getJobLogsServerTool.name]: getJobLogsServerTool, [searchDocumentationServerTool.name]: searchDocumentationServerTool, [searchOnlineServerTool.name]: searchOnlineServerTool, [setEnvironmentVariablesServerTool.name]: setEnvironmentVariablesServerTool, diff --git a/apps/sim/lib/copilot/tools/tool-display.test.ts b/apps/sim/lib/copilot/tools/tool-display.test.ts index feaa674f753..a519f326339 100644 --- a/apps/sim/lib/copilot/tools/tool-display.test.ts +++ b/apps/sim/lib/copilot/tools/tool-display.test.ts @@ -56,7 +56,7 @@ function toolPropertyEnum(entry: ToolCatalogEntry, property: string): unknown[] describe('humanizeToolName', () => { it('title-cases snake_case names', () => { - expect(humanizeToolName('manage_scheduled_task')).toBe('Manage Scheduled Task') + expect(humanizeToolName('manage_custom_tool')).toBe('Manage Custom Tool') }) it('title-cases kebab-case names', () => { @@ -308,11 +308,6 @@ describe('getToolDisplayTitle for managed resources', () => { ], ['manage_mcp_tool', { operation: 'edit', config: { name: 'Linear' } }, 'Updating Linear'], ['manage_skill', { operation: 'delete', name: 'sales-research' }, 'Deleting sales-research'], - [ - 'manage_scheduled_task', - { operation: 'create', args: { title: 'Morning Digest' } }, - 'Creating Morning Digest', - ], [ 'manage_credential', { @@ -326,8 +321,6 @@ describe('getToolDisplayTitle for managed resources', () => { ['manage_custom_tool', { operation: 'list' }, 'Viewing custom tools'], ['manage_mcp_tool', { operation: 'list' }, 'Viewing MCP servers'], ['manage_skill', { operation: 'list' }, 'Viewing skills'], - ['manage_scheduled_task', { operation: 'get' }, 'Reading scheduled task'], - ['manage_scheduled_task', { operation: 'list' }, 'Viewing scheduled tasks'], ])('uses verb + resource name for %s', (toolName, args, expected) => { expect(getToolDisplayTitle(toolName, args)).toBe(expected) }) @@ -414,9 +407,6 @@ describe('getToolDisplayTitle for operation-driven tools', () => { expect(getToolDisplayTitle('restore_resource', { type: 'knowledgebase' })).toBe( 'Restoring knowledge base' ) - expect(getToolDisplayTitle('open_resource', { resources: [{ type: 'scheduledtask' }] })).toBe( - 'Opening scheduled task' - ) }) it('includes deployment versions when available', () => { diff --git a/apps/sim/lib/copilot/tools/tool-display.ts b/apps/sim/lib/copilot/tools/tool-display.ts index 091d289a5b1..c8623c9b237 100644 --- a/apps/sim/lib/copilot/tools/tool-display.ts +++ b/apps/sim/lib/copilot/tools/tool-display.ts @@ -62,7 +62,6 @@ function deploymentTitle(args: ToolArgs, deploymentType: string): string { function resourceTypeLabel(type: string): string { const labels: Record = { knowledgebase: 'knowledge base', - scheduledtask: 'scheduled task', file_folder: 'file folder', log: 'logs', } @@ -434,7 +433,6 @@ const TOOL_TITLES: Record = { generate_audio: 'Generating audio', ffmpeg: 'Processing media', check_deployment_status: 'Checking deployment status', - complete_scheduled_task: 'Completing scheduled task', create_file: 'Creating file', create_file_folder: 'Creating folder', create_workspace_mcp_server: 'Creating MCP server', @@ -452,7 +450,6 @@ const TOOL_TITLES: Record = { get_deployed_workflow_state: 'Getting deployed workflow', get_deployment_log: 'Getting deployment logs', get_platform_actions: 'Getting platform actions', - get_scheduled_task_logs: 'Getting scheduled task logs', get_workflow_data: 'Getting workflow data', get_workflow_run_options: 'Getting run options', list_file_folders: 'Listing folders', @@ -479,7 +476,6 @@ const TOOL_TITLES: Record = { set_environment_variables: 'Setting environment variables', set_global_workflow_variables: 'Setting workflow variables', update_deployment_version: 'Updating deployment', - update_scheduled_task_history: 'Updating task history', update_workspace_mcp_server: 'Updating MCP server', // Browser agent tools without an argument-aware title. browser_go_back: 'Going back', @@ -502,7 +498,6 @@ const TOOL_TITLES: Record = { auth: 'Auth Agent', knowledge: 'Knowledge Agent', table: 'Table Agent', - scheduled_task: 'Scheduled Task Agent', agent: 'Tools Agent', research: 'Research Agent', scout: 'Scout Agent', @@ -892,17 +887,6 @@ export function getToolDisplayTitle(name: string, args?: Record list: { verb: 'Viewing', resource: 'skills' }, }) } - case 'manage_scheduled_task': { - const target = - firstStringArg(args, 'title', 'taskName', 'name') || nestedStringArg(args, 'args', 'title') - return namedOperationTitle(args, target, 'Scheduled task action', { - create: { verb: 'Creating', resource: 'scheduled task' }, - get: { verb: 'Reading', resource: 'scheduled task' }, - update: { verb: 'Updating', resource: 'scheduled task' }, - delete: { verb: 'Deleting', resource: 'scheduled task' }, - list: { verb: 'Viewing', resource: 'scheduled tasks' }, - }) - } case 'manage_credential': { const operation = stringArg(args, 'operation') if (operation === 'rename') { diff --git a/apps/sim/lib/copilot/vfs/serializers.ts b/apps/sim/lib/copilot/vfs/serializers.ts index 5dd5959a86e..53109f4062b 100644 --- a/apps/sim/lib/copilot/vfs/serializers.ts +++ b/apps/sim/lib/copilot/vfs/serializers.ts @@ -1102,49 +1102,6 @@ export function serializeTriggerOverview( return lines.join('\n') } -/** - * Serialize job metadata for VFS jobs/{id}/meta.json - */ -export function serializeJobMeta(job: { - id: string - title: string | null - prompt: string - cronExpression: string | null - timezone: string | null - status: string - lifecycle: string - successCondition: string | null - maxRuns: number | null - runCount: number - nextRunAt: Date | null - lastRanAt: Date | null - sourceTaskName: string | null - sourceChatId: string | null - createdAt: Date -}): string { - return JSON.stringify( - { - id: job.id, - title: job.title || undefined, - prompt: job.prompt, - cronExpression: job.cronExpression || undefined, - timezone: job.timezone || 'UTC', - status: job.status, - lifecycle: job.lifecycle, - successCondition: job.successCondition || undefined, - maxRuns: job.maxRuns ?? undefined, - runCount: job.runCount, - nextRunAt: job.nextRunAt?.toISOString(), - lastRanAt: job.lastRanAt?.toISOString(), - sourceTaskName: job.sourceTaskName || undefined, - sourceChatId: job.sourceChatId || undefined, - createdAt: job.createdAt.toISOString(), - }, - null, - 2 - ) -} - export function serializeTaskSession(task: { id: string title: string diff --git a/apps/sim/lib/copilot/vfs/workspace-vfs.ts b/apps/sim/lib/copilot/vfs/workspace-vfs.ts index 2be11810418..8e112a553ca 100644 --- a/apps/sim/lib/copilot/vfs/workspace-vfs.ts +++ b/apps/sim/lib/copilot/vfs/workspace-vfs.ts @@ -6,7 +6,6 @@ import { customTools as customToolsTable, document, folder as folderTable, - jobExecutionLogs, knowledgeBaseTagDefinitions, knowledgeConnector, mcpServers as mcpServersTable, @@ -15,11 +14,10 @@ import { workflowExecutionLogs, workflowMcpServer, workflowMcpTool, - workflowSchedule, } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' -import { and, desc, eq, inArray, isNotNull, isNull, ne, or, sql } from 'drizzle-orm' +import { and, desc, eq, inArray, isNotNull, isNull, or, sql } from 'drizzle-orm' import { listApiKeys } from '@/lib/api-key/service' import { buildWorkspaceContextMd, @@ -72,7 +70,6 @@ import { serializeEnvironmentVariables, serializeFileMeta, serializeIntegrationSchema, - serializeJobMeta, serializeKBMeta, serializeMcpServer, serializeRecentExecutions, @@ -452,9 +449,6 @@ function getStaticComponentFiles(): Map { * files/{name} (workspace file leaf; dynamic content on read) * files/{path}/{name}/style (dynamic — style extraction for .docx/.pptx/.pdf) * files/{path}/{name}/compiled-check (dynamic — compile generated source / validate diagrams, returns {ok,error?}) - * jobs/{title}/meta.json - * jobs/{title}/history.json - * jobs/{title}/executions.json * tasks/{title}/session.md * tasks/{title}/chat.json * custom-tools/{name}.json @@ -683,7 +677,6 @@ export class WorkspaceVFS { customBlocksSummary, mcpServersSummary, skillsSummary, - jobsSummary, wsRow, members, ] = await Promise.all([ @@ -696,7 +689,6 @@ export class WorkspaceVFS { timed('custom_blocks', this.materializeCustomBlocks(workspaceId)), timed('mcp_servers', this.materializeMcpServers(workspaceId)), timed('skills', this.materializeSkills(workspaceId)), - timed('jobs', this.materializeJobs(workspaceId)), timed('workspace_row', getWorkspaceWithOwner(workspaceId)), timed('members', getUsersWithPermissions(workspaceId)), // Writes tasks/ files only — WORKSPACE.md has no Tasks section @@ -718,7 +710,6 @@ export class WorkspaceVFS { customBlocks: customBlocksSummary, mcpServers: mcpServersSummary, skills: skillsSummary, - jobs: jobsSummary, } this.files.set('WORKSPACE.md', buildWorkspaceMd(workspaceMdData)) @@ -2090,114 +2081,6 @@ export class WorkspaceVFS { } } - /** - * Materialize scheduled jobs using the workflowSchedule table. - * Returns a summary for WORKSPACE.md generation. - */ - private async materializeJobs( - workspaceId: string - ): Promise> { - try { - const jobRows = await db - .select({ - id: workflowSchedule.id, - jobTitle: workflowSchedule.jobTitle, - prompt: workflowSchedule.prompt, - cronExpression: workflowSchedule.cronExpression, - timezone: workflowSchedule.timezone, - status: workflowSchedule.status, - lifecycle: workflowSchedule.lifecycle, - successCondition: workflowSchedule.successCondition, - maxRuns: workflowSchedule.maxRuns, - runCount: workflowSchedule.runCount, - nextRunAt: workflowSchedule.nextRunAt, - lastRanAt: workflowSchedule.lastRanAt, - sourceTaskName: workflowSchedule.sourceTaskName, - sourceChatId: workflowSchedule.sourceChatId, - jobHistory: workflowSchedule.jobHistory, - createdAt: workflowSchedule.createdAt, - }) - .from(workflowSchedule) - .where( - and( - eq(workflowSchedule.sourceWorkspaceId, workspaceId), - eq(workflowSchedule.sourceType, 'job'), - isNull(workflowSchedule.archivedAt), - ne(workflowSchedule.status, 'completed') - ) - ) - - for (const job of jobRows) { - const safeName = sanitizeName(job.jobTitle || job.id) - this.files.set( - `jobs/${safeName}/meta.json`, - serializeJobMeta({ - id: job.id, - title: job.jobTitle, - prompt: job.prompt || '', - cronExpression: job.cronExpression, - timezone: job.timezone, - status: job.status, - lifecycle: job.lifecycle, - successCondition: job.successCondition, - maxRuns: job.maxRuns, - runCount: job.runCount, - nextRunAt: job.nextRunAt, - lastRanAt: job.lastRanAt, - sourceTaskName: job.sourceTaskName, - sourceChatId: job.sourceChatId, - createdAt: job.createdAt, - }) - ) - - const history = job.jobHistory as Array<{ timestamp: string; summary: string }> | null - if (history && history.length > 0) { - this.files.set(`jobs/${safeName}/history.json`, JSON.stringify(history, null, 2)) - } - - // executions.json is lazy, advertised only when the job has run (cheap - // signal: lastRanAt) — no per-job query on a read/glob. - if (job.lastRanAt) { - this.registerLazy(`jobs/${safeName}/executions.json`, async () => { - const execRows = await db - .select({ - id: jobExecutionLogs.id, - executionId: jobExecutionLogs.executionId, - status: jobExecutionLogs.status, - trigger: jobExecutionLogs.trigger, - startedAt: jobExecutionLogs.startedAt, - endedAt: jobExecutionLogs.endedAt, - totalDurationMs: jobExecutionLogs.totalDurationMs, - }) - .from(jobExecutionLogs) - .where(eq(jobExecutionLogs.scheduleId, job.id)) - .orderBy(desc(jobExecutionLogs.startedAt)) - .limit(5) - return execRows.length > 0 ? serializeRecentExecutions(execRows) : null - }) - } - } - - return jobRows - .filter((j) => j.status !== 'completed') - .map((j) => ({ - id: j.id, - title: j.jobTitle, - prompt: j.prompt || '', - cronExpression: j.cronExpression, - status: j.status, - lifecycle: j.lifecycle, - sourceTaskName: j.sourceTaskName, - })) - } catch (err) { - logger.warn('Failed to materialize jobs', { - workspaceId, - error: toError(err).message, - }) - return [] - } - } - private async materializeRecentlyDeleted(workspaceId: string, userId: string): Promise { try { const [ diff --git a/apps/sim/lib/mothership/inbox/response.ts b/apps/sim/lib/mothership/inbox/response.ts index fbd04227574..9f76951cf8c 100644 --- a/apps/sim/lib/mothership/inbox/response.ts +++ b/apps/sim/lib/mothership/inbox/response.ts @@ -70,7 +70,7 @@ const emailStyles = { fontSize: '15px', lineHeight: '25px', color: '#1a1a1a', - fontWeight: 430, + fontWeight: 400, }, content: { margin: 0, @@ -105,7 +105,7 @@ const markdownStyles = { lineHeight: '25px', color: '#1a1a1a', fontFamily: FONT_FAMILY, - fontWeight: 430, + fontWeight: 400, }, h1: { fontWeight: 600, diff --git a/apps/sim/lib/posthog/events.ts b/apps/sim/lib/posthog/events.ts index aa520209e87..48462b7be1b 100644 --- a/apps/sim/lib/posthog/events.ts +++ b/apps/sim/lib/posthog/events.ts @@ -596,11 +596,6 @@ export interface PostHogEventMap { connected_provider_count: number } - suggested_actions_shuffled: { - workspace_id: string - connected_provider_count: number - } - suggested_actions_toggled: { workspace_id: string expanded: boolean @@ -672,14 +667,6 @@ export interface PostHogEventMap { workspace_id: string } - scheduled_task_created: { - workspace_id: string - } - - scheduled_task_deleted: { - workspace_id: string - } - workspace_logo_uploaded: { workspace_id: string file_name: string diff --git a/apps/sim/lib/workflows/references/operations.test.ts b/apps/sim/lib/workflows/references/operations.test.ts deleted file mode 100644 index 02ad33e67c9..00000000000 --- a/apps/sim/lib/workflows/references/operations.test.ts +++ /dev/null @@ -1,309 +0,0 @@ -/** - * @vitest-environment node - */ -import { describe, expect, it } from 'vitest' -import { - type CustomBlockLink, - type ReferenceBlockRow, - resolveWorkflowReferences, - type WorkflowNode, -} from '@/lib/workflows/references/operations' - -const workflows: WorkflowNode[] = [ - { id: 'a', name: 'A' }, - { id: 'b', name: 'B' }, - { id: 'c', name: 'C' }, - { id: 'd', name: 'D' }, -] - -function workflowBlock( - parentId: string, - childId: string, - mode: 'basic' | 'manual' = 'basic', - type: 'workflow' | 'workflow_input' = 'workflow' -): ReferenceBlockRow { - return { - parentId, - type, - childFromSelector: mode === 'basic' ? childId : null, - childFromManual: mode === 'manual' ? childId : null, - canonicalModes: null, - toolInputValues: null, - } -} - -describe('resolveWorkflowReferences', () => { - it('resolves direct callers and callees', () => { - const blocks = [workflowBlock('a', 'b'), workflowBlock('a', 'c')] - const { callers, callees } = resolveWorkflowReferences('a', workflows, blocks, []) - - expect(callers).toEqual([]) - expect(callees.map((n) => n.id)).toEqual(['b', 'c']) - - const bResult = resolveWorkflowReferences('b', workflows, blocks, []) - expect(bResult.callers.map((n) => n.id)).toEqual(['a']) - expect(bResult.callees).toEqual([]) - }) - - it('resolves references made through workflow_input blocks', () => { - const blocks = [ - workflowBlock('a', 'b', 'basic', 'workflow_input'), - workflowBlock('b', 'c', 'basic', 'workflow_input'), - ] - const { callees } = resolveWorkflowReferences('a', workflows, blocks, []) - expect(callees.map((n) => n.id)).toEqual(['b']) - expect(callees[0].children.map((n) => n.id)).toEqual(['c']) - - const cResult = resolveWorkflowReferences('c', workflows, blocks, []) - expect(cResult.callers.map((n) => n.id)).toEqual(['b']) - expect(cResult.callers[0].children.map((n) => n.id)).toEqual(['a']) - }) - - it('resolves the advanced-mode manualWorkflowId value', () => { - const blocks = [workflowBlock('a', 'b', 'manual')] - const { callees } = resolveWorkflowReferences('a', workflows, blocks, []) - expect(callees.map((n) => n.id)).toEqual(['b']) - }) - - it('uses the active mode, not a retained inactive value', () => { - // Advanced mode active (canonicalModes override), but a stale basic value - // (`b`) lingers. Must resolve to the advanced value (`c`), not the stale basic. - const blocks: ReferenceBlockRow[] = [ - { - parentId: 'a', - type: 'workflow', - childFromSelector: 'b', - childFromManual: 'c', - canonicalModes: { workflowId: 'advanced' }, - toolInputValues: null, - }, - ] - const { callees } = resolveWorkflowReferences('a', workflows, blocks, []) - expect(callees.map((n) => n.id)).toEqual(['c']) - }) - - it('marks cycles as leaves and stops recursing', () => { - const blocks = [workflowBlock('a', 'b'), workflowBlock('b', 'a')] - const { callees } = resolveWorkflowReferences('a', workflows, blocks, []) - - expect(callees).toHaveLength(1) - expect(callees[0]).toMatchObject({ id: 'b', cycle: false }) - expect(callees[0].children).toHaveLength(1) - expect(callees[0].children[0]).toMatchObject({ id: 'a', cycle: true, children: [] }) - }) - - it('shows a self-reference as a cycle leaf', () => { - // A → A: the reference is real and belongs in the cycle-safe viewer. - const blocks = [workflowBlock('a', 'a')] - const { callers, callees } = resolveWorkflowReferences('a', workflows, blocks, []) - expect(callees).toEqual([{ id: 'a', name: 'A', cycle: true, children: [] }]) - expect(callers).toEqual([{ id: 'a', name: 'A', cycle: true, children: [] }]) - }) - - it('bounds converging paths (diamond) instead of re-expanding', () => { - // A → B, A → C, B → D, C → D, D → E. D reconverges; it must appear under both - // B and C but expand its subtree (E) only under the first-visited branch. - const workflowsWithE = [...workflows, { id: 'e', name: 'E' }] - const blocks = [ - workflowBlock('a', 'b'), - workflowBlock('a', 'c'), - workflowBlock('b', 'd'), - workflowBlock('c', 'd'), - workflowBlock('d', 'e'), - ] - const { callees } = resolveWorkflowReferences('a', workflowsWithE, blocks, []) - const b = callees.find((n) => n.id === 'b') - const c = callees.find((n) => n.id === 'c') - // D expands under the first-visited branch (B) and is a collapsed leaf under C. - expect(b?.children).toEqual([ - { - id: 'd', - name: 'D', - cycle: false, - children: [{ id: 'e', name: 'E', cycle: false, children: [] }], - }, - ]) - expect(c?.children).toEqual([{ id: 'd', name: 'D', cycle: false, children: [] }]) - }) - - it('truncates expansion at the depth ceiling', () => { - // A linear chain longer than MAX_REFERENCE_DEPTH (25): w0 → w1 → … → w29. - const chain = Array.from({ length: 30 }, (_, i) => ({ id: `w${i}`, name: `W${i}` })) - const blocks = Array.from({ length: 29 }, (_, i) => workflowBlock(`w${i}`, `w${i + 1}`)) - const { callees } = resolveWorkflowReferences('w0', chain, blocks, []) - let depth = 0 - let node = callees[0] - while (node) { - depth += 1 - node = node.children[0] - } - expect(depth).toBe(25) - }) - - it('re-expands a depth-truncated node when a shallower path reaches it', () => { - // Root fans out to a 25-deep chain (visited first by name sort: "A…") whose - // tail X gets truncated at the ceiling, and a direct edge (via "Z") to X. - // The shallow path must still show X's child Y instead of a collapsed leaf. - const nodes = [ - { id: 'root', name: 'Root' }, - ...Array.from({ length: 24 }, (_, i) => ({ - id: `a${i}`, - name: `A${String(i).padStart(2, '0')}`, - })), - { id: 'x', name: 'X' }, - { id: 'y', name: 'Y' }, - { id: 'z', name: 'Z' }, - ] - const blocks = [ - workflowBlock('root', 'a0'), - ...Array.from({ length: 23 }, (_, i) => workflowBlock(`a${i}`, `a${i + 1}`)), - workflowBlock('a23', 'x'), - workflowBlock('root', 'z'), - workflowBlock('z', 'x'), - workflowBlock('x', 'y'), - ] - const { callees } = resolveWorkflowReferences('root', nodes, blocks, []) - const z = callees.find((n) => n.id === 'z') - const xUnderZ = z?.children.find((n) => n.id === 'x') - expect(xUnderZ?.children.map((n) => n.id)).toEqual(['y']) - }) - - it('drops dangling / out-of-workspace child ids', () => { - const blocks = [workflowBlock('a', 'missing')] - const { callees } = resolveWorkflowReferences('a', workflows, blocks, []) - expect(callees).toEqual([]) - }) - - it('resolves references made through custom blocks', () => { - const blocks: ReferenceBlockRow[] = [ - { - parentId: 'd', - type: 'custom_block_x', - childFromSelector: null, - childFromManual: null, - canonicalModes: null, - toolInputValues: null, - }, - ] - const customBlocks: CustomBlockLink[] = [{ type: 'custom_block_x', workflowId: 'c' }] - - const cResult = resolveWorkflowReferences('c', workflows, blocks, customBlocks) - expect(cResult.callers.map((n) => n.id)).toEqual(['d']) - - const dResult = resolveWorkflowReferences('d', workflows, blocks, customBlocks) - expect(dResult.callees.map((n) => n.id)).toEqual(['c']) - }) - - it('ignores custom blocks with no bound source in scope', () => { - const blocks: ReferenceBlockRow[] = [ - { - parentId: 'd', - type: 'custom_block_unknown', - childFromSelector: null, - childFromManual: null, - canonicalModes: null, - toolInputValues: null, - }, - ] - const { callees } = resolveWorkflowReferences('d', workflows, blocks, []) - expect(callees).toEqual([]) - }) - - it('returns empty trees when the workflow is not a workspace node', () => { - const blocks = [workflowBlock('a', 'b')] - const result = resolveWorkflowReferences('unknown', workflows, blocks, []) - expect(result).toEqual({ callers: [], callees: [] }) - }) - - it('sorts children by name', () => { - // Names: B, C — insert in reverse to prove sorting. - const blocks = [workflowBlock('a', 'c'), workflowBlock('a', 'b')] - const { callees } = resolveWorkflowReferences('a', workflows, blocks, []) - expect(callees.map((n) => n.name)).toEqual(['B', 'C']) - }) - - it('resolves workflow tools inside tool-input sub-blocks', () => { - // Agent block on A carrying a workflow_input tool that calls B; a non-workflow - // tool and a malformed entry must be ignored. - const blocks: ReferenceBlockRow[] = [ - { - parentId: 'a', - type: 'agent', - childFromSelector: null, - childFromManual: null, - canonicalModes: null, - toolInputValues: [ - [ - { type: 'workflow_input', params: { workflowId: 'b' } }, - { type: 'function', params: {} }, - { type: 'workflow_input' }, - ], - ], - }, - ] - const { callees } = resolveWorkflowReferences('a', workflows, blocks, []) - expect(callees.map((n) => n.id)).toEqual(['b']) - - const bResult = resolveWorkflowReferences('b', workflows, blocks, []) - expect(bResult.callers.map((n) => n.id)).toEqual(['a']) - }) - - it('resolves a workflow tool to its active advanced-mode value', () => { - // Tool 0 is toggled to advanced via the index-scoped canonicalModes key; the - // stale basic selector (`b`) must not mask the live manual value (`c`). - const blocks: ReferenceBlockRow[] = [ - { - parentId: 'a', - type: 'agent', - childFromSelector: null, - childFromManual: null, - canonicalModes: { '0:workflowId': 'advanced' }, - toolInputValues: [ - [{ type: 'workflow_input', params: { workflowId: 'b', manualWorkflowId: 'c' } }], - ], - }, - ] - const { callees } = resolveWorkflowReferences('a', workflows, blocks, []) - expect(callees.map((n) => n.id)).toEqual(['c']) - }) - - it('resolves legacy workflow-typed tools and isolates index-scoped modes per tool', () => { - // Tool 0 is a legacy `workflow`-typed entry (still rendered/executed by the - // editor); tool 1 is advanced-mode via its own index-scoped key. Tool 0 must - // stay basic (`b`) — tool 1's override must not bleed into it. - const blocks: ReferenceBlockRow[] = [ - { - parentId: 'a', - type: 'agent', - childFromSelector: null, - childFromManual: null, - canonicalModes: { '1:workflowId': 'advanced' }, - toolInputValues: [ - [ - { type: 'workflow', params: { workflowId: 'b' } }, - { type: 'workflow_input', params: { workflowId: 'c', manualWorkflowId: 'd' } }, - ], - ], - }, - ] - const { callees } = resolveWorkflowReferences('a', workflows, blocks, []) - expect(callees.map((n) => n.id)).toEqual(['b', 'd']) - }) - - it('resolves workflow tools from a JSON-stringified tool-input value', () => { - const blocks: ReferenceBlockRow[] = [ - { - parentId: 'a', - type: 'agent', - childFromSelector: null, - childFromManual: null, - canonicalModes: null, - toolInputValues: [ - JSON.stringify([{ type: 'workflow_input', params: { workflowId: 'c' } }]), - ], - }, - ] - const { callees } = resolveWorkflowReferences('a', workflows, blocks, []) - expect(callees.map((n) => n.id)).toEqual(['c']) - }) -}) diff --git a/apps/sim/lib/workflows/references/operations.ts b/apps/sim/lib/workflows/references/operations.ts deleted file mode 100644 index 768539b286d..00000000000 --- a/apps/sim/lib/workflows/references/operations.ts +++ /dev/null @@ -1,327 +0,0 @@ -import { db } from '@sim/db' -import { workflow, workflowBlocks } from '@sim/db/schema' -import { and, eq, inArray, isNull, or, sql } from 'drizzle-orm' -import type { ReferenceNode } from '@/lib/api/contracts/workflow-references' -import { MAX_CALL_CHAIN_DEPTH } from '@/lib/execution/call-chain' -import { getCustomBlockRowsForWorkspace } from '@/lib/workflows/custom-blocks/operations' -import { coerceObjectArray, isRecord } from '@/lib/workflows/persistence/remap-internal-ids' -import { - type CanonicalGroup, - type CanonicalModeOverrides, - resolveActiveCanonicalValue, - scopeCanonicalModesForTool, -} from '@/lib/workflows/subblocks/visibility' -import { CUSTOM_BLOCK_TYPE_PREFIX } from '@/blocks/custom/build-config' -import { BlockType, isWorkflowBlockType } from '@/executor/constants' - -/** - * Depth ceiling for a reference tree — the runtime call-chain bound; a display - * tree never needs to show more than the executor allows. The per-path visited - * set is the real cycle guard; this is a belt-and-suspenders bound on - * pathological graphs. - */ -const MAX_REFERENCE_DEPTH = MAX_CALL_CHAIN_DEPTH - -/** - * The `workflowId` canonical pair on a workflow / workflow_input block: the basic - * `workflowId` selector and the advanced `manualWorkflowId` input. Used with - * {@link resolveActiveCanonicalValue} so the reference resolves to the value of the - * block's ACTIVE mode — never a dormant mode's retained (stale) value. - * (`remapWorkflowReferencesInSubBlocks` in - * `@/lib/workflows/persistence/remap-internal-ids` builds the same pair from - * positional keys — keep the two in sync if the pair ever changes.) - */ -const WORKFLOW_ID_CANONICAL_GROUP: CanonicalGroup = { - canonicalId: 'workflowId', - basicId: 'workflowId', - advancedIds: ['manualWorkflowId'], -} - -/** `custom_block_` with LIKE wildcards (`_`) escaped, for the SQL prefix match. */ -const CUSTOM_BLOCK_LIKE_PREFIX = CUSTOM_BLOCK_TYPE_PREFIX.replace(/[\\%_]/g, '\\$&') - -/** A workspace-local, non-archived workflow node. */ -export interface WorkflowNode { - id: string - name: string -} - -/** A placed block that may reference another workflow (raw, pre-resolution). */ -export interface ReferenceBlockRow { - parentId: string - type: string - /** `workflowId` sub-block value (basic mode), if a workflow block. */ - childFromSelector: string | null - /** `manualWorkflowId` sub-block value (advanced mode), if a workflow block. */ - childFromManual: string | null - /** - * The block's `data.canonicalModes` override (basic/advanced per canonical id), - * used to pick the active `workflowId` value. Absent for most blocks. - */ - canonicalModes: CanonicalModeOverrides | null - /** - * Aggregated `tool-input` sub-block values on this block (agent-style tool - * lists). Each entry is one sub-block's raw value — an array of tool objects, - * or that array JSON-stringified. `workflow_input` tools carry the callee id - * in `params.workflowId` (basic) or `params.manualWorkflowId` (advanced). - * Null when the block has no tool-input sub-blocks. - */ - toolInputValues: unknown[] | null -} - -/** A custom-block type slug bound to its source workflow. */ -export interface CustomBlockLink { - type: string - workflowId: string -} - -/** - * The workspace-wide reference graph derived from live editor state: every - * workflow node's name, plus forward (callee) and reverse (caller) adjacency. - */ -interface ReferenceGraph { - nameById: Map - forward: Map> - reverse: Map> -} - -/** - * Callee workflow ids referenced by a block's tool-input values: workflow tools - * (`workflow_input`, plus legacy stored entries typed `workflow`) resolved to - * their ACTIVE canonical member — the basic `params.workflowId` selector or the - * advanced `params.manualWorkflowId` input, per the tool's index-scoped - * `canonicalModes` override ({@link scopeCanonicalModesForTool}) — mirroring how - * execution picks the live value. - */ -function toolInputCallees( - toolInputValues: unknown[] | null, - canonicalModes: CanonicalModeOverrides | null -): string[] { - if (!toolInputValues) return [] - const callees: string[] = [] - for (const value of toolInputValues) { - const { array } = coerceObjectArray(value) - if (!array) continue - array.forEach((tool, toolIndex) => { - if ( - !isRecord(tool) || - typeof tool.type !== 'string' || - !isWorkflowBlockType(tool.type) || - !isRecord(tool.params) - ) { - return - } - const scoped = scopeCanonicalModesForTool(canonicalModes ?? undefined, toolIndex, tool.type) - const active = resolveActiveCanonicalValue( - WORKFLOW_ID_CANONICAL_GROUP, - { - workflowId: typeof tool.params.workflowId === 'string' ? tool.params.workflowId : null, - manualWorkflowId: - typeof tool.params.manualWorkflowId === 'string' ? tool.params.manualWorkflowId : null, - }, - scoped - ) - if (typeof active === 'string' && active) callees.push(active) - }) - } - return callees -} - -/** - * Build the directed reference graph from raw workspace rows. Pure (no I/O) so it - * can be unit-tested directly. Resolves three call-edge shapes: - * - direct **workflow blocks** (`workflow` and `workflow_input`), whose child id - * is the `workflowId` (basic) or `manualWorkflowId` (advanced) sub-block value; - * - **custom blocks** (`custom_block_`), whose type slug maps to a bound - * source workflow via `customBlocks`; and - * - **workflow tools** — `workflow_input` entries inside a block's `tool-input` - * sub-blocks (an agent invoking another workflow as a tool). - * - * Non-call reference shapes (the logs block's `workflowSelector` monitor list and - * the workspace-event trigger's `workflowIds`; see - * `remapWorkflowReferencesInSubBlocks`) are deliberately excluded — the viewer - * shows call relationships. - * - * The workflow-block child is the value of the block's ACTIVE mode - * ({@link resolveActiveCanonicalValue}), so a dormant basic/advanced value can't - * mask the live one. Edges are scoped to workspace-local, non-archived workflows; - * empty values and ids outside `workflows` are dropped. A workflow that calls - * itself is kept — the tree builder renders it as a `cycle` leaf. - */ -function buildReferenceGraph( - workflows: WorkflowNode[], - blocks: ReferenceBlockRow[], - customBlocks: CustomBlockLink[] -): ReferenceGraph { - const nameById = new Map() - for (const node of workflows) nameById.set(node.id, node.name) - - const sourceByCustomType = new Map() - for (const link of customBlocks) sourceByCustomType.set(link.type, link.workflowId) - - const forward = new Map>() - const reverse = new Map>() - - const addEdge = (parentId: string, childId: string) => { - if (!childId) return - if (!nameById.has(parentId) || !nameById.has(childId)) return - let callees = forward.get(parentId) - if (!callees) forward.set(parentId, (callees = new Set())) - callees.add(childId) - let callers = reverse.get(childId) - if (!callers) reverse.set(childId, (callers = new Set())) - callers.add(parentId) - } - - for (const block of blocks) { - if (isWorkflowBlockType(block.type)) { - const active = resolveActiveCanonicalValue( - WORKFLOW_ID_CANONICAL_GROUP, - { workflowId: block.childFromSelector, manualWorkflowId: block.childFromManual }, - block.canonicalModes ?? undefined - ) - if (typeof active === 'string' && active) addEdge(block.parentId, active) - } else { - const sourceId = sourceByCustomType.get(block.type) - if (sourceId) addEdge(block.parentId, sourceId) - } - for (const calleeId of toolInputCallees(block.toolInputValues, block.canonicalModes)) { - addEdge(block.parentId, calleeId) - } - } - - return { nameById, forward, reverse } -} - -/** - * Expand a direction of the graph into a tree rooted at `rootId`. `adjacency` is - * either the forward (callees) or reverse (callers) map. - * - * A node already on the current DFS path is emitted as a `cycle: true` leaf and - * not re-expanded, so `A → B → A` (and a self-call `A → A`) terminates. A node - * already fully expanded elsewhere in this tree (reachable via another acyclic - * path — a diamond) is emitted once more as a plain leaf without re-expanding its - * subtree: the edge stays visible, but a densely reconverging graph can't blow up - * exponentially. Children are sorted by name for stable rendering. - */ -function buildTree( - rootId: string, - adjacency: Map>, - nameById: Map -): ReferenceNode[] { - const path = new Set([rootId]) - const expanded = new Set() - const nameOf = (id: string) => nameById.get(id) as string - - const expand = (id: string, depth: number): ReferenceNode[] => { - if (depth >= MAX_REFERENCE_DEPTH) return [] - const neighbors = adjacency.get(id) - if (!neighbors || neighbors.size === 0) return [] - - const sorted = [...neighbors].sort((a, b) => nameOf(a).localeCompare(nameOf(b))) - - const nodes: ReferenceNode[] = [] - for (const childId of sorted) { - const name = nameOf(childId) - if (path.has(childId)) { - nodes.push({ id: childId, name, cycle: true, children: [] }) - continue - } - if (expanded.has(childId)) { - nodes.push({ id: childId, name, cycle: false, children: [] }) - continue - } - expanded.add(childId) - path.add(childId) - nodes.push({ id: childId, name, cycle: false, children: expand(childId, depth + 1) }) - path.delete(childId) - // A depth-capped expansion is incomplete — allow a shallower path to retry - // it in full instead of collapsing to a leaf. Each retry starts strictly - // shallower, so this stays bounded. - if (depth + 1 >= MAX_REFERENCE_DEPTH) expanded.delete(childId) - } - return nodes - } - - return expand(rootId, 0) -} - -/** - * Resolve the reference trees for one workflow from raw workspace rows. Pure so it - * can be unit-tested without a database. Returns empty arrays when the workflow is - * not a workspace-local node or has no references. - */ -export function resolveWorkflowReferences( - workflowId: string, - workflows: WorkflowNode[], - blocks: ReferenceBlockRow[], - customBlocks: CustomBlockLink[] -): { callers: ReferenceNode[]; callees: ReferenceNode[] } { - const { nameById, forward, reverse } = buildReferenceGraph(workflows, blocks, customBlocks) - - if (!nameById.has(workflowId)) { - return { callers: [], callees: [] } - } - - return { - callers: buildTree(workflowId, reverse, nameById), - callees: buildTree(workflowId, forward, nameById), - } -} - -/** - * Resolve the reference trees for one workflow: `callers` (workflows that call - * it, inbound) and `callees` (workflows it calls, outbound), read from the live - * `workflowBlocks` (draft) table — the state the sidebar and editor show. The - * root itself is not a node; each array holds its direct references, recursively - * expanded. - */ -export async function getWorkflowReferences( - workspaceId: string, - workflowId: string -): Promise<{ callers: ReferenceNode[]; callees: ReferenceNode[] }> { - const hasToolInput = sql`EXISTS ( - SELECT 1 FROM jsonb_each(${workflowBlocks.subBlocks}) AS kv - WHERE kv.value ->> 'type' = 'tool-input' - )` - - const [workflowRows, blockRows, customBlockRows] = await Promise.all([ - db - .select({ id: workflow.id, name: workflow.name }) - .from(workflow) - .where(and(eq(workflow.workspaceId, workspaceId), isNull(workflow.archivedAt))), - db - .select({ - parentId: workflowBlocks.workflowId, - type: workflowBlocks.type, - childFromSelector: sql< - string | null - >`${workflowBlocks.subBlocks} -> 'workflowId' ->> 'value'`, - childFromManual: sql< - string | null - >`${workflowBlocks.subBlocks} -> 'manualWorkflowId' ->> 'value'`, - canonicalModes: sql`${workflowBlocks.data} -> 'canonicalModes'`, - toolInputValues: sql`( - SELECT jsonb_agg(kv.value -> 'value') - FROM jsonb_each(${workflowBlocks.subBlocks}) AS kv - WHERE kv.value ->> 'type' = 'tool-input' - )`, - }) - .from(workflowBlocks) - .innerJoin(workflow, eq(workflow.id, workflowBlocks.workflowId)) - .where( - and( - eq(workflow.workspaceId, workspaceId), - isNull(workflow.archivedAt), - or( - inArray(workflowBlocks.type, [BlockType.WORKFLOW, BlockType.WORKFLOW_INPUT]), - sql`${workflowBlocks.type} LIKE ${`${CUSTOM_BLOCK_LIKE_PREFIX}%`} ESCAPE '\\'`, - hasToolInput - ) - ) - ), - getCustomBlockRowsForWorkspace(workspaceId), - ]) - - return resolveWorkflowReferences(workflowId, workflowRows, blockRows, customBlockRows) -} diff --git a/apps/sim/lib/workflows/schedules/disable-notifications.test.ts b/apps/sim/lib/workflows/schedules/disable-notifications.test.ts index 020105c8ccb..156087025af 100644 --- a/apps/sim/lib/workflows/schedules/disable-notifications.test.ts +++ b/apps/sim/lib/workflows/schedules/disable-notifications.test.ts @@ -106,33 +106,6 @@ describe('notifyScheduleAutoDisabled', () => { ) }) - it('resolves a job schedule through sourceUserId and links to scheduled tasks', async () => { - queueTableRows(schemaMock.workflowSchedule, [ - { - ...WORKFLOW_SCHEDULE_ROW, - sourceType: 'job', - jobTitle: 'Weekly report', - sourceUserId: 'job-owner', - sourceWorkspaceId: 'ws-9', - workflowName: null, - workflowUserId: null, - workflowWorkspaceId: null, - }, - ]) - queueTableRows(schemaMock.user, [CREATOR]) - - await notifyScheduleAutoDisabled({ scheduleId: 's-1', reason: 'authentication_error' }) - - expect(renderMock).toHaveBeenCalledWith( - expect.objectContaining({ - kind: 'job', - resourceName: 'Weekly report', - reason: 'authentication_error', - manageLink: 'https://app.sim.ai/workspace/ws-9/scheduled-tasks', - }) - ) - }) - it('falls back to the creator alone when the workflow has no workspace', async () => { queueTableRows(schemaMock.workflowSchedule, [ { ...WORKFLOW_SCHEDULE_ROW, workflowWorkspaceId: null }, diff --git a/apps/sim/lib/workflows/schedules/disable-notifications.ts b/apps/sim/lib/workflows/schedules/disable-notifications.ts index 2bebe8bfd80..833f54a4026 100644 --- a/apps/sim/lib/workflows/schedules/disable-notifications.ts +++ b/apps/sim/lib/workflows/schedules/disable-notifications.ts @@ -38,11 +38,7 @@ export async function notifyScheduleAutoDisabled(params: { try { const rows = await db .select({ - sourceType: workflowSchedule.sourceType, - jobTitle: workflowSchedule.jobTitle, failedCount: workflowSchedule.failedCount, - sourceUserId: workflowSchedule.sourceUserId, - sourceWorkspaceId: workflowSchedule.sourceWorkspaceId, workflowId: workflowSchedule.workflowId, workflowName: workflow.name, workflowUserId: workflow.userId, @@ -59,11 +55,9 @@ export async function notifyScheduleAutoDisabled(params: { return } - const isJob = row.sourceType === 'job' - const kind = isJob ? 'job' : 'workflow' - const ownerUserId = isJob ? row.sourceUserId : row.workflowUserId - const workspaceId = isJob ? row.sourceWorkspaceId : row.workflowWorkspaceId - const resourceName = (isJob ? row.jobTitle : row.workflowName) ?? undefined + const ownerUserId = row.workflowUserId + const workspaceId = row.workflowWorkspaceId + const resourceName = row.workflowName ?? undefined const recipients = await resolveRecipients(ownerUserId, workspaceId) if (recipients.length === 0) { @@ -75,14 +69,13 @@ export async function notifyScheduleAutoDisabled(params: { return } - const manageLink = buildManageLink(workspaceId, isJob ? null : row.workflowId) + const manageLink = buildManageLink(workspaceId, row.workflowId) const subject = getEmailSubject('schedule-disabled') for (const recipient of recipients) { try { const html = await renderScheduleDisabledEmail({ recipientName: recipient.name ?? undefined, - kind, resourceName, reason, failedCount: row.failedCount, @@ -169,7 +162,6 @@ function buildManageLink( workspaceId: string | null, workflowId: string | null ): string | undefined { - if (!workspaceId) return undefined - const base = `${getBaseUrl()}/workspace/${workspaceId}` - return workflowId ? `${base}/w/${workflowId}` : `${base}/scheduled-tasks` + if (!workspaceId || !workflowId) return undefined + return `${getBaseUrl()}/workspace/${workspaceId}/w/${workflowId}` } diff --git a/apps/sim/lib/workflows/schedules/orchestration.test.ts b/apps/sim/lib/workflows/schedules/orchestration.test.ts deleted file mode 100644 index 771cdd5840c..00000000000 --- a/apps/sim/lib/workflows/schedules/orchestration.test.ts +++ /dev/null @@ -1,133 +0,0 @@ -/** - * @vitest-environment node - */ -import { - dbChainMock, - dbChainMockFns, - queueTableRows, - resetDbChainMock, - schemaMock, -} from '@sim/testing' -import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' - -const { mockRecordAudit, mockCaptureServerEvent } = vi.hoisted(() => ({ - mockRecordAudit: vi.fn(), - mockCaptureServerEvent: vi.fn(), -})) - -vi.mock('@sim/audit', () => ({ - AuditAction: { SCHEDULE_UPDATED: 'SCHEDULE_UPDATED' }, - AuditResourceType: { SCHEDULE: 'SCHEDULE' }, - recordAudit: mockRecordAudit, -})) - -vi.mock('@sim/db', () => ({ ...dbChainMock, ...schemaMock })) - -vi.mock('@/lib/posthog/server', () => ({ - captureServerEvent: mockCaptureServerEvent, -})) - -import { performUpdateJob } from '@/lib/workflows/schedules/orchestration' - -const BASE_JOB = { - id: 'job-1', - sourceWorkspaceId: 'workspace-1', - sourceUserId: 'user-1', - sourceType: 'job', - archivedAt: null, - timezone: 'UTC', - cronExpression: null, - jobTitle: 'Nightly task', - status: 'disabled', - secretScope: 'all', - mountedSecrets: [], -} - -describe('performUpdateJob', () => { - beforeEach(() => { - vi.clearAllMocks() - resetDbChainMock() - }) - - afterAll(() => { - resetDbChainMock() - }) - - it('does not schedule a next run when editing time on a disabled job', async () => { - queueTableRows(schemaMock.workflowSchedule, [{ ...BASE_JOB, status: 'disabled' }]) - - const result = await performUpdateJob({ - jobId: 'job-1', - workspaceId: 'workspace-1', - userId: 'user-1', - time: '2099-01-01T09:00:00Z', - }) - - expect(result.success).toBe(true) - expect(dbChainMockFns.set).toHaveBeenCalledTimes(1) - expect(dbChainMockFns.set.mock.calls[0][0]).not.toHaveProperty('nextRunAt') - }) - - it('schedules the next run when editing time on an active job', async () => { - queueTableRows(schemaMock.workflowSchedule, [{ ...BASE_JOB, status: 'active' }]) - - const result = await performUpdateJob({ - jobId: 'job-1', - workspaceId: 'workspace-1', - userId: 'user-1', - time: '2099-01-01T09:00:00Z', - }) - - expect(result.success).toBe(true) - expect(dbChainMockFns.set).toHaveBeenCalledTimes(1) - expect(dbChainMockFns.set.mock.calls[0][0]).toMatchObject({ - nextRunAt: new Date('2099-01-01T09:00:00Z'), - }) - }) - - it('denies task content edits from a non-creator without writing', async () => { - queueTableRows(schemaMock.workflowSchedule, [BASE_JOB]) - - const result = await performUpdateJob({ - jobId: 'job-1', - workspaceId: 'workspace-1', - userId: 'workspace-writer', - prompt: 'Changed prompt', - }) - - expect(result).toMatchObject({ success: false, errorCode: 'forbidden' }) - expect(dbChainMockFns.set).not.toHaveBeenCalled() - }) - - it('allows a non-creator to pause a task', async () => { - queueTableRows(schemaMock.workflowSchedule, [{ ...BASE_JOB, status: 'active' }]) - - const result = await performUpdateJob({ - jobId: 'job-1', - workspaceId: 'workspace-1', - userId: 'workspace-writer', - status: 'paused', - }) - - expect(result.success).toBe(true) - expect(dbChainMockFns.set.mock.calls[0][0]).toMatchObject({ status: 'disabled' }) - }) - - it('persists a canonical selected secret policy for the creator', async () => { - queueTableRows(schemaMock.workflowSchedule, [BASE_JOB]) - - const result = await performUpdateJob({ - jobId: 'job-1', - workspaceId: 'workspace-1', - userId: 'user-1', - secretScope: 'selected', - mountedSecrets: [' B ', 'A', 'B'], - }) - - expect(result.success).toBe(true) - expect(dbChainMockFns.set.mock.calls[0][0]).toMatchObject({ - secretScope: 'selected', - mountedSecrets: ['B', 'A'], - }) - }) -}) diff --git a/apps/sim/lib/workflows/schedules/orchestration.ts b/apps/sim/lib/workflows/schedules/orchestration.ts deleted file mode 100644 index 28a49eec405..00000000000 --- a/apps/sim/lib/workflows/schedules/orchestration.ts +++ /dev/null @@ -1,600 +0,0 @@ -import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' -import { db, workflowSchedule } from '@sim/db' -import { createLogger } from '@sim/logger' -import { toError } from '@sim/utils/errors' -import { generateId } from '@sim/utils/id' -import { and, eq, isNull } from 'drizzle-orm' -import type { NextRequest } from 'next/server' -import type { ScheduleContext } from '@/lib/api/contracts/schedules' -import { - normalizeSecretMountPolicy, - type SecretMountScope, -} from '@/lib/copilot/secret-mount-policy' -import { captureServerEvent } from '@/lib/posthog/server' -import { - computeNextRunAt, - parseCronToHumanReadable, - validateCronExpression, -} from '@/lib/workflows/schedules/utils' - -const logger = createLogger('ScheduleOrchestration') - -type ScheduleErrorCode = 'not_found' | 'forbidden' | 'validation' | 'internal' - -interface ActorMetadata { - actorName?: string | null - actorEmail?: string | null - request?: NextRequest -} - -export interface PerformCreateJobParams extends ActorMetadata { - workspaceId: string - userId: string - title?: string | null - prompt: string - cronExpression?: string | null - time?: string | null - timezone: string - lifecycle?: 'persistent' | 'until_complete' - successCondition?: string | null - maxRuns?: number | null - startDate?: string | null - /** Recurrence end on a date (ISO 8601); the schedule completes once its next run would fall after this. */ - endsAt?: string | null - /** `@`-mentioned resources / `/`-invoked skills captured with the prompt. */ - contexts?: ScheduleContext[] | null - secretScope?: SecretMountScope - mountedSecrets?: string[] - sourceChatId?: string | null - sourceTaskName?: string | null -} - -export interface PerformScheduleResult { - success: boolean - error?: string - errorCode?: ScheduleErrorCode - schedule?: typeof workflowSchedule.$inferSelect - humanReadable?: string - updatedFields?: string[] - alreadyCompleted?: boolean -} - -export interface PerformUpdateJobParams extends ActorMetadata { - jobId: string - workspaceId: string - userId: string - title?: string - prompt?: string - cronExpression?: string | null - time?: string | null - timezone?: string - status?: string - lifecycle?: string - successCondition?: string | null - maxRuns?: number | null - endsAt?: string | null - contexts?: ScheduleContext[] | null - secretScope?: SecretMountScope - mountedSecrets?: string[] -} - -export interface PerformExcludeOccurrenceParams extends ActorMetadata { - jobId: string - workspaceId: string - userId: string - /** The exact occurrence instant to skip (ISO 8601), as produced by the recurrence. */ - occurrence: string -} - -export interface PerformDeleteJobParams extends ActorMetadata { - jobId: string - workspaceId: string - userId: string -} - -export interface PerformCompleteJobParams extends ActorMetadata { - jobId: string - workspaceId: string - userId: string -} - -const activeJobCondition = (jobId: string, workspaceId: string) => - and( - eq(workflowSchedule.id, jobId), - eq(workflowSchedule.sourceWorkspaceId, workspaceId), - eq(workflowSchedule.sourceType, 'job'), - isNull(workflowSchedule.archivedAt) - ) - -function parseOneTimeRun(time: string, timezone: string): Date | null { - let timeStr = time - const hasOffset = /[Zz]|[+-]\d{2}(:\d{2})?$/.test(timeStr) - if (!hasOffset && timezone !== 'UTC') { - try { - const formatter = new Intl.DateTimeFormat('en-US', { - timeZone: timezone, - timeZoneName: 'shortOffset', - }) - const parts = formatter.formatToParts(new Date()) - const offsetPart = parts.find((part) => part.type === 'timeZoneName') - const match = offsetPart?.value.match(/GMT([+-]\d{1,2}(?::\d{2})?)/) - if (match) { - const [rawHours, rawMinutes = '00'] = match[1].split(':') - const sign = rawHours.startsWith('-') ? '-' : '+' - const hour = Number(rawHours.replace(/^[+-]/, '')) - if (Number.isFinite(hour)) { - const offset = `${sign}${String(hour).padStart(2, '0')}:${rawMinutes.padStart(2, '0')}` - timeStr = `${timeStr}${offset}` - } - } - } catch {} - } - - const parsed = new Date(timeStr) - return Number.isNaN(parsed.getTime()) ? null : parsed -} - -export async function performCreateJob( - params: PerformCreateJobParams -): Promise { - if (!params.prompt.trim()) { - return { success: false, error: 'prompt is required', errorCode: 'validation' } - } - - const cronExpression = params.cronExpression || null - if (!cronExpression && !params.time) { - return { - success: false, - error: 'At least one of cronExpression or time must be provided', - errorCode: 'validation', - } - } - - let endsAt: Date | null = null - if (params.endsAt) { - const parsedEndsAt = new Date(params.endsAt) - if (Number.isNaN(parsedEndsAt.getTime())) { - return { - success: false, - error: `Invalid endsAt value: ${params.endsAt}`, - errorCode: 'validation', - } - } - endsAt = parsedEndsAt - } - - let nextRunAt: Date | null = null - if (cronExpression) { - const validation = validateCronExpression(cronExpression, params.timezone) - if (!validation.isValid) { - return { - success: false, - error: validation.error || 'Invalid cron expression', - errorCode: 'validation', - } - } - nextRunAt = computeNextRunAt({ cronExpression, timezone: params.timezone, endsAt }) - } - - if (params.time) { - const parsed = parseOneTimeRun(params.time, params.timezone) - if (!parsed) { - return { - success: false, - error: `Invalid time value: ${params.time}`, - errorCode: 'validation', - } - } - if (!cronExpression || parsed > new Date()) nextRunAt = parsed - } - - if (params.startDate) { - const start = new Date(params.startDate) - if (start > new Date()) nextRunAt = start - } - - if (!nextRunAt) { - return { success: false, error: 'Could not determine next run time', errorCode: 'validation' } - } - - try { - const id = generateId() - const now = new Date() - const secretMountPolicy = normalizeSecretMountPolicy(params) - await db.insert(workflowSchedule).values({ - id, - workflowId: null, - cronExpression, - triggerType: 'schedule', - sourceType: 'job', - status: 'active', - timezone: params.timezone, - nextRunAt, - createdAt: now, - updatedAt: now, - failedCount: 0, - jobTitle: params.title?.trim() || null, - prompt: params.prompt.trim(), - lifecycle: params.lifecycle || 'persistent', - successCondition: params.successCondition || null, - maxRuns: params.maxRuns ?? null, - runCount: 0, - contexts: params.contexts ?? null, - excludedDates: null, - endsAt, - sourceChatId: params.sourceChatId || null, - sourceTaskName: params.sourceTaskName || null, - sourceUserId: params.userId, - sourceWorkspaceId: params.workspaceId, - secretScope: secretMountPolicy.secretScope, - mountedSecrets: secretMountPolicy.mountedSecrets, - }) - - const [schedule] = await db - .select() - .from(workflowSchedule) - .where(eq(workflowSchedule.id, id)) - .limit(1) - - const humanReadable = cronExpression - ? parseCronToHumanReadable(cronExpression, params.timezone) - : `Once at ${nextRunAt.toISOString()}` - - recordAudit({ - workspaceId: params.workspaceId, - actorId: params.userId, - actorName: params.actorName ?? undefined, - actorEmail: params.actorEmail ?? undefined, - action: AuditAction.SCHEDULE_CREATED, - resourceType: AuditResourceType.SCHEDULE, - resourceId: id, - resourceName: params.title?.trim() || undefined, - description: `Created job schedule "${params.title?.trim() || id}"`, - metadata: { - cronExpression, - timezone: params.timezone, - lifecycle: params.lifecycle || 'persistent', - maxRuns: params.maxRuns ?? null, - }, - request: params.request, - }) - - captureServerEvent( - params.userId, - 'scheduled_task_created', - { workspace_id: params.workspaceId }, - { groups: { workspace: params.workspaceId } } - ) - - if (schedule?.workflowId) { - captureServerEvent( - params.userId, - 'workflow_schedule_created', - { workflow_id: schedule.workflowId, workspace_id: params.workspaceId }, - { groups: { workspace: params.workspaceId } } - ) - } - - return { success: true, schedule, humanReadable } - } catch (error) { - logger.error('Failed to create job', { error: toError(error).message }) - return { success: false, error: 'Failed to create job', errorCode: 'internal' } - } -} - -export async function performUpdateJob( - params: PerformUpdateJobParams -): Promise { - try { - const [job] = await db - .select() - .from(workflowSchedule) - .where(activeJobCondition(params.jobId, params.workspaceId)) - .limit(1) - - if (!job) - return { success: false, error: `Job not found: ${params.jobId}`, errorCode: 'not_found' } - - const hasCreatorOnlyUpdate = - params.title !== undefined || - params.prompt !== undefined || - params.cronExpression !== undefined || - params.time !== undefined || - params.timezone !== undefined || - params.lifecycle !== undefined || - params.successCondition !== undefined || - params.maxRuns !== undefined || - params.endsAt !== undefined || - params.contexts !== undefined || - params.secretScope !== undefined || - params.mountedSecrets !== undefined - if (hasCreatorOnlyUpdate && job.sourceUserId !== params.userId) { - return { - success: false, - error: 'Only the task creator can edit this task', - errorCode: 'forbidden', - } - } - - const updates: Partial = { updatedAt: new Date() } - if (params.title !== undefined) updates.jobTitle = params.title.trim() - if (params.prompt !== undefined) updates.prompt = params.prompt.trim() - if (params.timezone !== undefined) updates.timezone = params.timezone - if (params.status !== undefined) { - if (!['active', 'paused', 'disabled'].includes(params.status)) { - return { - success: false, - error: 'status must be "active" or "paused"', - errorCode: 'validation', - } - } - updates.status = params.status === 'paused' ? 'disabled' : params.status - } - if (params.lifecycle !== undefined) { - if (params.lifecycle !== 'persistent' && params.lifecycle !== 'until_complete') { - return { - success: false, - error: 'lifecycle must be "persistent" or "until_complete"', - errorCode: 'validation', - } - } - updates.lifecycle = params.lifecycle - if (params.lifecycle === 'persistent') updates.maxRuns = null - } - if (params.successCondition !== undefined) updates.successCondition = params.successCondition - if (params.maxRuns !== undefined) updates.maxRuns = params.maxRuns - if (params.contexts !== undefined) updates.contexts = params.contexts - if (params.secretScope !== undefined || params.mountedSecrets !== undefined) { - const secretMountPolicy = normalizeSecretMountPolicy({ - secretScope: params.secretScope ?? job.secretScope, - mountedSecrets: params.mountedSecrets ?? job.mountedSecrets, - }) - updates.secretScope = secretMountPolicy.secretScope - updates.mountedSecrets = secretMountPolicy.mountedSecrets - } - const effectiveStatus = updates.status ?? job.status - - let endsAt: Date | null = job.endsAt - if (params.endsAt !== undefined) { - if (params.endsAt === null) { - endsAt = null - } else { - const parsedEndsAt = new Date(params.endsAt) - if (Number.isNaN(parsedEndsAt.getTime())) { - return { - success: false, - error: `Invalid endsAt value: ${params.endsAt}`, - errorCode: 'validation', - } - } - endsAt = parsedEndsAt - } - updates.endsAt = endsAt - } - - if (params.cronExpression !== undefined && params.cronExpression !== null) { - const timezone = params.timezone || job.timezone || 'UTC' - const validation = validateCronExpression(params.cronExpression, timezone) - if (!validation.isValid) { - return { - success: false, - error: validation.error || 'Invalid cron expression', - errorCode: 'validation', - } - } - updates.cronExpression = params.cronExpression - if (effectiveStatus === 'active') { - updates.nextRunAt = computeNextRunAt({ - cronExpression: params.cronExpression, - timezone, - excludedDates: job.excludedDates, - endsAt, - }) - } - } else if (params.cronExpression === null) { - updates.cronExpression = null - } - if (params.time !== undefined && params.time !== null) { - const timezone = params.timezone || job.timezone || 'UTC' - const parsed = parseOneTimeRun(params.time, timezone) - if (!parsed) { - return { - success: false, - error: `Invalid time value: ${params.time}`, - errorCode: 'validation', - } - } - const cronExpression = - params.cronExpression !== undefined ? params.cronExpression : job.cronExpression - if (effectiveStatus === 'active' && (!cronExpression || parsed > new Date())) { - updates.nextRunAt = parsed - } - } - - const updatedFields = Object.keys(updates).filter((key) => key !== 'updatedAt') - - await db - .update(workflowSchedule) - .set(updates) - .where(and(eq(workflowSchedule.id, params.jobId), isNull(workflowSchedule.archivedAt))) - - recordAudit({ - workspaceId: params.workspaceId, - actorId: params.userId, - actorName: params.actorName ?? undefined, - actorEmail: params.actorEmail ?? undefined, - action: AuditAction.SCHEDULE_UPDATED, - resourceType: AuditResourceType.SCHEDULE, - resourceId: params.jobId, - resourceName: job.jobTitle ?? undefined, - description: `Updated job schedule "${job.jobTitle ?? params.jobId}"`, - metadata: { operation: 'update', updatedFields }, - request: params.request, - }) - - return { success: true, updatedFields } - } catch (error) { - logger.error('Failed to update job', { error: toError(error).message }) - return { success: false, error: 'Failed to update job', errorCode: 'internal' } - } -} - -export async function performExcludeOccurrence( - params: PerformExcludeOccurrenceParams -): Promise { - const occurrence = new Date(params.occurrence) - if (Number.isNaN(occurrence.getTime())) { - return { - success: false, - error: `Invalid occurrence value: ${params.occurrence}`, - errorCode: 'validation', - } - } - - try { - const [job] = await db - .select() - .from(workflowSchedule) - .where(activeJobCondition(params.jobId, params.workspaceId)) - .limit(1) - - if (!job) - return { success: false, error: `Job not found: ${params.jobId}`, errorCode: 'not_found' } - if (!job.cronExpression) { - return { - success: false, - error: 'Only recurring tasks have individual occurrences to delete', - errorCode: 'validation', - } - } - - const occurrenceIso = occurrence.toISOString() - const excludedDates = Array.from(new Set([...(job.excludedDates ?? []), occurrenceIso])) - - const updates: Partial = { - excludedDates, - updatedAt: new Date(), - } - - if (job.nextRunAt && job.nextRunAt.getTime() === occurrence.getTime()) { - const nextRunAt = computeNextRunAt({ - cronExpression: job.cronExpression, - timezone: job.timezone || 'UTC', - from: occurrence, - excludedDates, - endsAt: job.endsAt, - }) - updates.nextRunAt = nextRunAt - if (!nextRunAt) updates.status = 'completed' - } - - await db - .update(workflowSchedule) - .set(updates) - .where(and(eq(workflowSchedule.id, params.jobId), isNull(workflowSchedule.archivedAt))) - - recordAudit({ - workspaceId: params.workspaceId, - actorId: params.userId, - actorName: params.actorName ?? undefined, - actorEmail: params.actorEmail ?? undefined, - action: AuditAction.SCHEDULE_UPDATED, - resourceType: AuditResourceType.SCHEDULE, - resourceId: params.jobId, - resourceName: job.jobTitle ?? undefined, - description: `Deleted one occurrence of job "${job.jobTitle ?? params.jobId}"`, - metadata: { operation: 'exclude_occurrence', occurrence: occurrenceIso }, - request: params.request, - }) - - return { success: true } - } catch (error) { - logger.error('Failed to exclude occurrence', { error: toError(error).message }) - return { success: false, error: 'Failed to delete occurrence', errorCode: 'internal' } - } -} - -export async function performDeleteJob( - params: PerformDeleteJobParams -): Promise { - const [job] = await db - .select() - .from(workflowSchedule) - .where(activeJobCondition(params.jobId, params.workspaceId)) - .limit(1) - - if (!job) - return { success: false, error: `Job not found: ${params.jobId}`, errorCode: 'not_found' } - - await db.delete(workflowSchedule).where(eq(workflowSchedule.id, params.jobId)) - recordAudit({ - workspaceId: params.workspaceId, - actorId: params.userId, - actorName: params.actorName ?? undefined, - actorEmail: params.actorEmail ?? undefined, - action: AuditAction.SCHEDULE_DELETED, - resourceType: AuditResourceType.SCHEDULE, - resourceId: params.jobId, - resourceName: job.jobTitle ?? undefined, - description: `Deleted job "${job.jobTitle ?? params.jobId}"`, - metadata: { - sourceType: job.sourceType, - cronExpression: job.cronExpression, - timezone: job.timezone, - }, - request: params.request, - }) - - captureServerEvent( - params.userId, - 'scheduled_task_deleted', - { workspace_id: params.workspaceId }, - { groups: { workspace: params.workspaceId } } - ) - - if (job.workflowId) { - captureServerEvent( - params.userId, - 'workflow_schedule_deleted', - { workflow_id: job.workflowId, workspace_id: params.workspaceId }, - { groups: { workspace: params.workspaceId } } - ) - } - - return { success: true, schedule: job } -} - -export async function performCompleteJob( - params: PerformCompleteJobParams -): Promise { - const [job] = await db - .select() - .from(workflowSchedule) - .where(activeJobCondition(params.jobId, params.workspaceId)) - .limit(1) - - if (!job) - return { success: false, error: `Job not found: ${params.jobId}`, errorCode: 'not_found' } - if (job.status === 'completed') return { success: true, schedule: job, alreadyCompleted: true } - - const [updatedJob] = await db - .update(workflowSchedule) - .set({ status: 'completed', nextRunAt: null, updatedAt: new Date() }) - .where(and(eq(workflowSchedule.id, params.jobId), isNull(workflowSchedule.archivedAt))) - .returning() - - recordAudit({ - workspaceId: params.workspaceId, - actorId: params.userId, - actorName: params.actorName ?? undefined, - actorEmail: params.actorEmail ?? undefined, - action: AuditAction.SCHEDULE_UPDATED, - resourceType: AuditResourceType.SCHEDULE, - resourceId: params.jobId, - description: 'Completed job', - metadata: { operation: 'complete' }, - request: params.request, - }) - - return { success: true, schedule: updatedJob, alreadyCompleted: false } -} diff --git a/apps/sim/lib/workflows/schedules/utils.ts b/apps/sim/lib/workflows/schedules/utils.ts index 53f7bf01ab4..ab30c80bb27 100644 --- a/apps/sim/lib/workflows/schedules/utils.ts +++ b/apps/sim/lib/workflows/schedules/utils.ts @@ -53,45 +53,6 @@ export function validateCronExpression( /** Upper bound on how many excluded occurrences the next-run search skips before giving up. */ const MAX_OCCURRENCE_SKIP = 1000 -/** - * Computes the next run instant for a recurring schedule, skipping occurrences - * the user deleted individually and stopping at the recurrence end boundary. - * Returns `null` when the recurrence has no remaining run (past `endsAt`, or - * every candidate within the search bound is excluded). - * - * Excluded occurrences are matched by exact instant, so callers must record the - * cron-produced occurrence time (not a rounded value) when excluding. - */ -export function computeNextRunAt(params: { - cronExpression: string - timezone?: string - from?: Date - excludedDates?: string[] | null - endsAt?: Date | null -}): Date | null { - const { cronExpression, timezone, from, excludedDates, endsAt } = params - let cron: Cron - try { - cron = new Cron(cronExpression, timezone ? { timezone } : undefined) - } catch { - return null - } - - const excluded = new Set( - (excludedDates ?? []).map((iso) => new Date(iso).getTime()).filter((ms) => !Number.isNaN(ms)) - ) - - let cursor = from ?? new Date() - for (let i = 0; i < MAX_OCCURRENCE_SKIP; i++) { - const next = cron.nextRun(cursor) - if (!next) return null - if (endsAt && next.getTime() > endsAt.getTime()) return null - if (!excluded.has(next.getTime())) return next - cursor = next - } - return null -} - interface SubBlockValue { value: string } diff --git a/apps/sim/lib/workspaces/lifecycle.test.ts b/apps/sim/lib/workspaces/lifecycle.test.ts index 2102e84ae97..e6a4efc0e50 100644 --- a/apps/sim/lib/workspaces/lifecycle.test.ts +++ b/apps/sim/lib/workspaces/lifecycle.test.ts @@ -77,7 +77,7 @@ describe('workspace lifecycle', () => { expect(mockArchiveWorkflowsForWorkspace).toHaveBeenCalledWith('workspace-1', { requestId: 'req-1', }) - expect(tx.update).toHaveBeenCalledTimes(10) + expect(tx.update).toHaveBeenCalledTimes(9) expect(tx.delete).toHaveBeenCalledTimes(1) }) diff --git a/apps/sim/lib/workspaces/lifecycle.ts b/apps/sim/lib/workspaces/lifecycle.ts index 59162b42682..27b6514e410 100644 --- a/apps/sim/lib/workspaces/lifecycle.ts +++ b/apps/sim/lib/workspaces/lifecycle.ts @@ -9,7 +9,6 @@ import { mcpServers, userTableDefinitions, workflowMcpServer, - workflowSchedule, workspace, workspaceFiles, } from '@sim/db/schema' @@ -145,23 +144,6 @@ export async function archiveWorkspace( }) .where(and(eq(mcpServers.workspaceId, workspaceId), isNull(mcpServers.deletedAt))) - await tx - .update(workflowSchedule) - .set({ - archivedAt: now, - updatedAt: now, - status: 'disabled', - nextRunAt: null, - lastQueuedAt: null, - }) - .where( - and( - eq(workflowSchedule.sourceWorkspaceId, workspaceId), - eq(workflowSchedule.sourceType, 'job'), - isNull(workflowSchedule.archivedAt) - ) - ) - await tx .update(workspace) .set({ diff --git a/apps/sim/next.config.ts b/apps/sim/next.config.ts index 1ecee9fd736..172bf16d220 100644 --- a/apps/sim/next.config.ts +++ b/apps/sim/next.config.ts @@ -33,7 +33,6 @@ const LANDING_ROUTES = [ 'models', 'pricing', 'privacy', - 'scheduled-tasks', 'solutions', 'tables', 'terms', @@ -476,6 +475,15 @@ const nextConfig: NextConfig = { } ) + // The scheduled-tasks marketing page is retired with the feature. The URL is + // indexed, so send it to the surface that still carries scheduled execution + // (the workflow Schedule trigger) instead of letting it 404. + redirects.push({ + source: '/scheduled-tasks', + destination: '/workflows', + permanent: true, + }) + /** * The marketing Academy course/lesson pages were removed; content is * consolidated into the docs site instead. Old course/lesson slugs have diff --git a/apps/sim/package.json b/apps/sim/package.json index 8f6a6b96078..a2faac62d1f 100644 --- a/apps/sim/package.json +++ b/apps/sim/package.json @@ -185,7 +185,6 @@ "jszip": "3.10.1", "lib0": "0.2.117", "lru-cache": "11.3.6", - "lucide-react": "^0.511.0", "mammoth": "^1.9.0", "mermaid": "11.15.0", "micromatch": "4.0.8", diff --git a/apps/sim/public/static/readme-scheduled-tasks.png b/apps/sim/public/static/readme-scheduled-tasks.png deleted file mode 100644 index 57eaa363874..00000000000 Binary files a/apps/sim/public/static/readme-scheduled-tasks.png and /dev/null differ diff --git a/apps/sim/stores/constants.ts b/apps/sim/stores/constants.ts index c44e4e89181..f04814333e0 100644 --- a/apps/sim/stores/constants.ts +++ b/apps/sim/stores/constants.ts @@ -23,8 +23,8 @@ export const CONTENT_WINDOW_GAP = 8 /** Sidebar width constraints */ export const SIDEBAR_WIDTH = { - DEFAULT: 248, - MIN: 248, + DEFAULT: 238, + MIN: 238, /** Width when sidebar is collapsed to icon-only mode */ COLLAPSED: 51, /** Maximum is 30% of viewport, enforced dynamically */ diff --git a/apps/sim/stores/modals/search/store.ts b/apps/sim/stores/modals/search/store.ts index 7084b591044..f21d516ddc4 100644 --- a/apps/sim/stores/modals/search/store.ts +++ b/apps/sim/stores/modals/search/store.ts @@ -1,4 +1,4 @@ -import { RepeatIcon, SplitIcon } from 'lucide-react' +import { Repeat, Split } from '@sim/emcn/icons' import { create } from 'zustand' import { devtools } from 'zustand/middleware' import { getToolOperationsIndex } from '@/lib/search/tool-operations' @@ -128,14 +128,14 @@ export const useSearchModalStore = create()( { id: 'loop', name: 'Loop', - icon: RepeatIcon, + icon: Repeat, bgColor: '#2FB3FF', type: 'loop', }, { id: 'parallel', name: 'Parallel', - icon: SplitIcon, + icon: Split, bgColor: '#FEE12B', type: 'parallel', }, diff --git a/apps/sim/stores/panel/types.ts b/apps/sim/stores/panel/types.ts index b026fe028e0..42c4a8cfd54 100644 --- a/apps/sim/stores/panel/types.ts +++ b/apps/sim/stores/panel/types.ts @@ -77,7 +77,6 @@ export type ChatContext = } | { kind: 'folder'; folderId: string; label: string } | { kind: 'filefolder'; fileFolderId: string; label: string } - | { kind: 'scheduledtask'; scheduleId: string; label: string } | { kind: 'docs'; label: string } /** * A tab in the desktop browser or terminal panel, dragged into the input to diff --git a/apps/sim/tailwind.config.ts b/apps/sim/tailwind.config.ts index 76d8940f930..576f1528e06 100644 --- a/apps/sim/tailwind.config.ts +++ b/apps/sim/tailwind.config.ts @@ -66,6 +66,16 @@ export default { }, spacing: { '4.5': '18px', + /** + * Hairline scale key. Overriding `spacing` rather than `width`/`height` + * keeps every derived scale in agreement — a `w-px` line and the + * `-right-px` offset that positions it resolve to the same value, which + * a width-only override desynchronizes by half a device pixel. + */ + px: 'var(--border-width)', + }, + borderWidth: { + DEFAULT: 'var(--border-width)', }, colors: { background: 'hsl(var(--background))', @@ -98,7 +108,13 @@ export default { DEFAULT: 'hsl(var(--destructive))', foreground: 'hsl(var(--destructive-foreground))', }, - border: 'hsl(var(--border))', + /** + * Neutral border colors are plain hex, not the HSL triplets the shadcn + * keys below use — `hsl(var(--border))` would be invalid CSS and get + * dropped, leaving the utility to fall through to the `*` border-color + * rule in globals by accident. + */ + border: 'var(--border)', input: 'hsl(var(--input))', ring: 'hsl(var(--ring))', chart: { @@ -122,11 +138,6 @@ export default { 950: '#0a0a0a', }, }, - fontWeight: { - base: 'var(--font-weight-base)', - medium: 'var(--font-weight-medium)', - semibold: 'var(--font-weight-semibold)', - }, borderRadius: { xs: '2px', sm: 'calc(var(--radius) - 4px)', @@ -140,8 +151,18 @@ export default { kbd: 'var(--shadow-kbd)', 'kbd-sm': 'var(--shadow-kbd-sm)', card: 'var(--shadow-card)', + ambient: 'var(--shadow-ambient)', }, dropShadow: {}, + maxWidth: { + /** + * The home/chat reading column. The heading, input, suggested actions, + * transcript, and its skeleton must all share one value or the footer + * input visibly misaligns with the messages above it — so they read this + * key rather than repeating a literal. + */ + chat: '44rem', + }, transitionProperty: { width: 'width', left: 'left', diff --git a/bun.lock b/bun.lock index 6085287fa66..efcbfdcc57a 100644 --- a/bun.lock +++ b/bun.lock @@ -75,7 +75,6 @@ "fumadocs-mdx": "14.3.2", "fumadocs-openapi": "10.8.1", "fumadocs-ui": "16.8.5", - "lucide-react": "^0.511.0", "next": "16.2.12", "next-themes": "^0.4.6", "react": "19.2.4", @@ -291,7 +290,6 @@ "jszip": "3.10.1", "lib0": "0.2.117", "lru-cache": "11.3.6", - "lucide-react": "^0.511.0", "mammoth": "^1.9.0", "mermaid": "11.15.0", "micromatch": "4.0.8", @@ -489,7 +487,6 @@ "class-variance-authority": "^0.7.1", "framer-motion": "^12.5.0", "input-otp": "^1.4.2", - "lucide-react": "^0.511.0", "next": "16.2.12", "prismjs": "^1.30.0", "react": "19.2.4", @@ -514,7 +511,6 @@ "class-variance-authority": "^0.7.1", "framer-motion": "^12.5.0", "input-otp": "^1.4.2", - "lucide-react": ">=0.479.0", "next": ">=15", "prismjs": "^1.30.0", "react": "^19", @@ -663,7 +659,6 @@ "@sim/tsconfig": "workspace:*", "@sim/utils": "workspace:*", "@types/react": "^19", - "lucide-react": "^0.511.0", "react": "19.2.4", "reactflow": "^11.11.4", "remark-breaks": "^4.0.0", @@ -673,7 +668,6 @@ "peerDependencies": { "@sim/emcn": "workspace:*", "@sim/utils": "workspace:*", - "lucide-react": ">=0.479.0", "react": "^19", "reactflow": "^11.11.4", "remark-breaks": "^4.0.0", diff --git a/packages/emcn/package.json b/packages/emcn/package.json index 6089d9afbe7..55455c98a66 100644 --- a/packages/emcn/package.json +++ b/packages/emcn/package.json @@ -56,7 +56,6 @@ "class-variance-authority": "^0.7.1", "framer-motion": "^12.5.0", "input-otp": "^1.4.2", - "lucide-react": ">=0.479.0", "next": ">=15", "prismjs": "^1.30.0", "react": "^19", @@ -83,7 +82,6 @@ "class-variance-authority": "^0.7.1", "framer-motion": "^12.5.0", "input-otp": "^1.4.2", - "lucide-react": "^0.511.0", "next": "16.2.12", "prismjs": "^1.30.0", "react": "19.2.4", diff --git a/packages/emcn/src/AGENTS.md b/packages/emcn/src/AGENTS.md index ebbfb1ad49a..8f8497c5913 100644 --- a/packages/emcn/src/AGENTS.md +++ b/packages/emcn/src/AGENTS.md @@ -1,6 +1,6 @@ # EMCN Components Scope -These rules apply to `apps/sim/components/emcn/**`. +These rules apply to `packages/emcn/**`. - Import from `@sim/emcn`, never from subpaths except CSS files. - Use Radix UI primitives for accessibility where applicable. diff --git a/packages/emcn/src/components/button/button.tsx b/packages/emcn/src/components/button/button.tsx index 80eebd30722..e1916bda1e4 100644 --- a/packages/emcn/src/components/button/button.tsx +++ b/packages/emcn/src/components/button/button.tsx @@ -2,6 +2,24 @@ import { type ButtonHTMLAttributes, forwardRef } from 'react' import { cva, type VariantProps } from 'class-variance-authority' import { cn } from '../../lib/cn' +/** + * `size='icon'` is the square 20px icon-only button — a chip field's trailing + * affordance, a toast dismiss, a section-header action. It drops the text padding + * the `sm`/`md` sizes carry and tightens the radius to `rounded-sm` (4px), which + * reads correctly at this size where the base 5px does not. The box is deliberately + * larger than every glyph it holds; that margin IS the button's padding, since the + * glyph is sized at the call site rather than here. + * + * Glyphs also draw one step thinner than the 1.55 the icon set ships, so a lone + * icon reads as a secondary affordance rather than a piece of UI text. CSS wins + * over the SVG's own `stroke-width` attribute, so this reaches every stroked icon + * without touching the icon components — including the few that ship at 2. + * + * Compose it with `quiet` (the usual choice) or `ghost` (where the surrounding + * surface owns the hover) — those pairings also pick up the muted icon color. + * + * @example + */ const buttonVariants = cva( 'inline-flex items-center justify-center font-medium transition-colors disabled:pointer-events-none disabled:opacity-70 outline-none focus:outline-none focus-visible:outline-none rounded-[5px]', { @@ -30,8 +48,19 @@ const buttonVariants = cva( size: { sm: 'px-1.5 py-1 text-[length:11px]', md: 'px-2 py-1.5 text-[length:12px]', + icon: 'size-[20px] rounded-sm p-0 [&_svg]:[stroke-width:1.25]', }, }, + compoundVariants: [ + /** + * A lone glyph is icon content, not text, so the neutral icon buttons paint + * with `--text-icon-muted` rather than the variant's text color. Scoped to + * the neutral variants: the filled ones (`primary`, `destructive`, …) carry + * inverse text that must keep winning over their own surface. + */ + { size: 'icon', variant: 'quiet', className: 'text-[var(--text-icon-muted)]' }, + { size: 'icon', variant: 'ghost', className: 'text-[var(--text-icon-muted)]' }, + ], defaultVariants: { variant: 'default', size: 'md', diff --git a/packages/emcn/src/components/calendar/calendar-day-cell.tsx b/packages/emcn/src/components/calendar/calendar-day-cell.tsx index 33f070af65c..e3c49796c15 100644 --- a/packages/emcn/src/components/calendar/calendar-day-cell.tsx +++ b/packages/emcn/src/components/calendar/calendar-day-cell.tsx @@ -45,7 +45,6 @@ export const CalendarDayCell = forwardRef Time - +
)} @@ -547,9 +539,9 @@ function RangeCalendarView({ {showTime && (
- + to - +
)} diff --git a/packages/emcn/src/components/checkbox/checkbox.tsx b/packages/emcn/src/components/checkbox/checkbox.tsx index 92c5dee6f90..ba721515e96 100644 --- a/packages/emcn/src/components/checkbox/checkbox.tsx +++ b/packages/emcn/src/components/checkbox/checkbox.tsx @@ -3,7 +3,7 @@ import * as React from 'react' import * as CheckboxPrimitive from '@radix-ui/react-checkbox' import { cva, type VariantProps } from 'class-variance-authority' -import { Check, Minus } from 'lucide-react' +import { Check, Minus } from '../../icons' import { cn } from '../../lib/cn' /** diff --git a/packages/emcn/src/components/chip-copy-input/chip-copy-input.tsx b/packages/emcn/src/components/chip-copy-input/chip-copy-input.tsx index cee80bf7e5c..75111802089 100644 --- a/packages/emcn/src/components/chip-copy-input/chip-copy-input.tsx +++ b/packages/emcn/src/components/chip-copy-input/chip-copy-input.tsx @@ -52,7 +52,7 @@ export const ChipCopyInput = React.forwardRef copy(value)} aria-label={copyLabel} > diff --git a/packages/emcn/src/components/chip-date-picker/chip-date-picker.tsx b/packages/emcn/src/components/chip-date-picker/chip-date-picker.tsx index 42d83aa5452..b131ccee387 100644 --- a/packages/emcn/src/components/chip-date-picker/chip-date-picker.tsx +++ b/packages/emcn/src/components/chip-date-picker/chip-date-picker.tsx @@ -29,8 +29,6 @@ interface ChipDatePickerBaseProps { disabled?: boolean /** Stretch the trigger to fill its container (mirrors `Chip`'s `fullWidth`). */ fullWidth?: boolean - /** Removes the default `mx-0.5` cluster margin (mirrors `Chip`'s `flush`). */ - flush?: boolean /** Forwarded class for the trigger button. */ className?: string } @@ -87,7 +85,6 @@ const ChipDatePicker = forwardRef( align = 'start', disabled, fullWidth, - flush, className, } = props @@ -108,8 +105,8 @@ const ChipDatePicker = forwardRef( disabled={disabled} className={cn( variant === 'ghost' - ? chipVariants({ fullWidth, flush }) - : cn(chipVariants({ variant: 'filled', fullWidth, flush }), TRIGGER_BORDER_CLASS), + ? chipVariants({ fullWidth }) + : cn(chipVariants({ variant: 'filled', fullWidth }), TRIGGER_BORDER_CLASS), className )} > diff --git a/packages/emcn/src/components/chip-dropdown/chip-dropdown.tsx b/packages/emcn/src/components/chip-dropdown/chip-dropdown.tsx index 67c4b7a8b02..6173e1ed96d 100644 --- a/packages/emcn/src/components/chip-dropdown/chip-dropdown.tsx +++ b/packages/emcn/src/components/chip-dropdown/chip-dropdown.tsx @@ -164,7 +164,6 @@ type ChipDropdownProps = ChipDropdownSingleProps | ChipDropdownMultiProps * searchable * searchPlaceholder='Search members...' * fullWidth - * flush * /> */ const ChipDropdown = forwardRef( @@ -181,7 +180,6 @@ const ChipDropdown = forwardRef( variant = 'filled', active, fullWidth, - flush, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, id, @@ -308,7 +306,7 @@ const ChipDropdown = forwardRef( aria-label={ariaLabel} aria-labelledby={ariaLabelledBy} className={cn( - chipVariants({ variant, active, fullWidth, flush }), + chipVariants({ variant, active, fullWidth }), hasTriggerBorder && TRIGGER_BORDER_CLASS, className )} diff --git a/packages/emcn/src/components/chip-input/chip-input.tsx b/packages/emcn/src/components/chip-input/chip-input.tsx index 9cb72304fe3..1266a9aa5b9 100644 --- a/packages/emcn/src/components/chip-input/chip-input.tsx +++ b/packages/emcn/src/components/chip-input/chip-input.tsx @@ -32,7 +32,7 @@ import { chipFieldSurfaceClass, chipFieldTextClass } from '../chip/chip-chrome' type ChipInputIcon = React.ComponentType<{ className?: string }> export interface ChipInputProps extends Omit, 'size'> { - /** Leading icon component (e.g. lucide `Search`). Rendered at 14px in `--text-icon`, with the chip's 1.5 gap. */ + /** Leading icon component (e.g. `Search` from `@sim/emcn/icons`). Rendered at 14px in `--text-icon`, with the chip's 1.5 gap. */ icon?: ChipInputIcon /** Trailing content rendered after the input (e.g. reveal / copy buttons). */ endAdornment?: React.ReactNode diff --git a/packages/emcn/src/components/chip-modal/chip-modal.tsx b/packages/emcn/src/components/chip-modal/chip-modal.tsx index 9a64ae85ae9..e0d2da89f4b 100644 --- a/packages/emcn/src/components/chip-modal/chip-modal.tsx +++ b/packages/emcn/src/components/chip-modal/chip-modal.tsx @@ -39,8 +39,7 @@ 'use client' import * as React from 'react' -import { X } from 'lucide-react' -import { Loader } from '../../icons' +import { Loader, X } from '../../icons' import { cn } from '../../lib/cn' import { Button } from '../button/button' import { Chip, type ChipProps } from '../chip/chip' @@ -887,10 +886,10 @@ export interface ChipModalFooterAction { * Escape hatch for the left-docked footer cluster: renders the given node in * place of a declarative action Chip. Reserve it for chip-chrome controls * (`ChipDatePicker`, `ChipTimePicker`, `ChipDropdown`, ...) so the footer - * stays visually canonical — pass `flush` to the control so it sits on the - * cluster's `gap-2` rhythm like the footer's own Chips. The primary action - * stays declarative by design; only `secondaryActions` accepts custom - * controls. + * stays visually canonical — the cluster's `gap-2` alone sets the rhythm, as + * it does for the footer's own Chips, so the control must carry no outer + * margin. The primary action stays declarative by design; only + * `secondaryActions` accepts custom controls. */ export interface ChipModalFooterCustomAction { /** Chip-chrome control rendered verbatim in the slot. */ @@ -989,7 +988,7 @@ function ChipModalFooterShell({ function renderFooterSlotAction(action: ChipModalFooterSlotAction): React.ReactNode { if ('custom' in action) return action.custom return ( - + {action.label} ) @@ -1048,7 +1047,6 @@ function ChipModalFooter({ const primaryChip = ( {hideCancel ? null : ( - + Cancel )} @@ -1307,12 +1305,11 @@ function ChipConfirmModal({ {children} - + {dismissLabel} diff --git a/packages/emcn/src/components/chip-select/chip-select.tsx b/packages/emcn/src/components/chip-select/chip-select.tsx index 05565b47034..fd528fe6913 100644 --- a/packages/emcn/src/components/chip-select/chip-select.tsx +++ b/packages/emcn/src/components/chip-select/chip-select.tsx @@ -227,7 +227,7 @@ export function ChipSelect({ disabled={disabled} aria-label={ariaLabel} className={cn( - chipVariants({ variant: 'filled', flush: true, fullWidth }), + chipVariants({ variant: 'filled', fullWidth }), TRIGGER_BORDER_CLASS, fullWidth ? 'w-full justify-between' : 'w-fit max-w-[240px]', className diff --git a/packages/emcn/src/components/chip-switch/chip-switch.tsx b/packages/emcn/src/components/chip-switch/chip-switch.tsx index cf0cf535de2..dcf287749b2 100644 --- a/packages/emcn/src/components/chip-switch/chip-switch.tsx +++ b/packages/emcn/src/components/chip-switch/chip-switch.tsx @@ -80,7 +80,6 @@ export function ChipSwitch({ className={cn( chipVariants({ variant: isActive ? 'border-shadow' : 'default', - flush: true, }), 'justify-center', isActive diff --git a/packages/emcn/src/components/chip-tag/chip-tag.tsx b/packages/emcn/src/components/chip-tag/chip-tag.tsx index e69ae59c429..c19fbba8277 100644 --- a/packages/emcn/src/components/chip-tag/chip-tag.tsx +++ b/packages/emcn/src/components/chip-tag/chip-tag.tsx @@ -37,10 +37,10 @@ const chipTagVariants = cva( mono: 'h-5 gap-[3px] px-1 bg-[var(--surface-5)] text-[var(--text-primary)] dark:bg-[var(--surface-4)]', field: 'h-5 gap-[3px] px-1 bg-[var(--surface-6)] text-[var(--text-primary)] dark:bg-[var(--surface-3)]', - gray: 'h-5 gap-[3px] px-1 bg-[var(--surface-5)] text-[var(--text-secondary)] shadow-[inset_0_0_0_1px_var(--border-1)]', + gray: 'h-5 gap-[3px] px-1 border border-[var(--border-1)] bg-[var(--surface-5)] text-[var(--text-secondary)]', solid: 'h-5 gap-[3px] px-1 bg-[var(--text-secondary)] text-[var(--text-inverse)]', invite: - 'h-5 gap-1.5 px-1 bg-[var(--surface-5)] text-[var(--text-body)] shadow-[inset_0_0_0_1px_var(--border-1)] dark:bg-[var(--surface-4)]', + 'h-5 gap-1.5 px-1 border border-[var(--border-1)] bg-[var(--surface-5)] text-[var(--text-body)] dark:bg-[var(--surface-4)]', }, invalid: { true: '', false: '' }, }, @@ -48,7 +48,7 @@ const chipTagVariants = cva( { variant: 'invite', invalid: true, - className: 'bg-[var(--badge-error-bg)] text-[var(--text-error)] shadow-none', + className: 'bg-[var(--badge-error-bg)] text-[var(--text-error)] border-transparent', }, ], defaultVariants: { variant: 'mono', invalid: false }, diff --git a/packages/emcn/src/components/chip-time-picker/chip-time-picker.tsx b/packages/emcn/src/components/chip-time-picker/chip-time-picker.tsx index 83d2d25964a..6bebb61d419 100644 --- a/packages/emcn/src/components/chip-time-picker/chip-time-picker.tsx +++ b/packages/emcn/src/components/chip-time-picker/chip-time-picker.tsx @@ -52,8 +52,6 @@ export interface ChipTimePickerProps { disabled?: boolean /** Stretch the field to fill its container (mirrors `Chip`'s `fullWidth`). */ fullWidth?: boolean - /** Removes the default `mx-0.5` cluster margin (mirrors `Chip`'s `flush`). */ - flush?: boolean /** Layout/sizing only — width overrides, margins. The chrome is owned by the chip field. */ className?: string } @@ -66,11 +64,11 @@ export interface ChipTimePickerProps { * doesn't parse reverts to the last committed time. * * @example - * + * */ const ChipTimePicker = React.forwardRef( function ChipTimePicker( - { value, onChange, placeholder = '10:00 AM', disabled, fullWidth, flush, className }, + { value, onChange, placeholder = '10:00 AM', disabled, fullWidth, className }, ref ) { const [text, setText] = React.useState(() => formatTimeLabel(value)) @@ -124,7 +122,7 @@ const ChipTimePicker = React.forwardRef( aria-label='Time' autoComplete='off' spellCheck={false} - className={cn(fullWidth ? 'w-full' : 'w-[88px]', flush ? 'mx-0' : 'mx-0.5', className)} + className={cn(fullWidth ? 'w-full' : 'w-[88px]', className)} /> ) } diff --git a/packages/emcn/src/components/chip/chip.tsx b/packages/emcn/src/components/chip/chip.tsx index fb868dd8b18..fb87f1684e9 100644 --- a/packages/emcn/src/components/chip/chip.tsx +++ b/packages/emcn/src/components/chip/chip.tsx @@ -36,8 +36,12 @@ import { * `border` (the `border-shadow` shadow ring on a transparent surface — an outline drawn purely via box-shadow, * no CSS border, no fill). * `active` renders the default/filled chip in its selected state — `--surface-active` at rest, one surface darker - * (`--surface-6`) on hover. `fullWidth` swaps `inline-flex` for block-level `flex`. `flush` removes the default - * `mx-0.5` cluster margin — use when a single chip sits in its own layout slot (grid/table cell). + * (`--surface-6`) on hover. `fullWidth` swaps `inline-flex` for block-level `flex`. + * + * The chip carries NO outer margin — spacing between chips belongs to the parent, as a `gap`. It used to ship a + * default `mx-0.5` "cluster margin" with a `flush` prop to switch it off, which meant a chip's visual box was not + * its layout box: changing the space between two chips took an edit in two places, and a collapsing container + * could never close past the margins. Do not reintroduce it. * * The default/filled hover lives in `active`-keyed compound variants (not the base variant string) so the * rest/hover classes are mutually exclusive — a chip renders exactly ONE `hover-hover:bg-*`. This keeps raw @@ -62,7 +66,6 @@ const chipVariants = cva( }, active: { true: '', false: '' }, fullWidth: { true: 'flex', false: 'inline-flex' }, - flush: { true: 'mx-0', false: 'mx-0.5' }, }, compoundVariants: [ { @@ -86,7 +89,7 @@ const chipVariants = cva( className: 'bg-[var(--surface-active)] hover-hover:bg-[var(--surface-6)]', }, ], - defaultVariants: { variant: 'default', active: false, fullWidth: false, flush: false }, + defaultVariants: { variant: 'default', active: false, fullWidth: false }, } ) @@ -142,14 +145,14 @@ interface ChipProps * @example {balance} */ const Chip = forwardRef(function Chip( - { className, variant, active, fullWidth, flush, leftIcon, rightIcon, children, type, ...props }, + { className, variant, active, fullWidth, leftIcon, rightIcon, children, type, ...props }, ref ) { return (
diff --git a/packages/emcn/src/components/tag-input/tag-input.tsx b/packages/emcn/src/components/tag-input/tag-input.tsx index 36f6de7f1d6..6cee52f3be3 100644 --- a/packages/emcn/src/components/tag-input/tag-input.tsx +++ b/packages/emcn/src/components/tag-input/tag-input.tsx @@ -39,7 +39,7 @@ import * as React from 'react' import { cva, type VariantProps } from 'class-variance-authority' -import { Paperclip, Plus, X } from 'lucide-react' +import { Paperclip, Plus, X } from '../../icons' import { cn } from '../../lib/cn' import { handleKeyboardActivation } from '../../lib/keyboard' import { ChipTag, chipTagVariants } from '../chip-tag/chip-tag' @@ -99,7 +99,7 @@ export interface FileInputOptions { /** Accepted file types (default: '.csv,.txt,text/csv,text/plain') */ accept?: string /** Icon component to render (default: Paperclip) */ - icon?: React.ComponentType<{ className?: string; strokeWidth?: number }> + icon?: React.ComponentType<{ className?: string }> /** Extract values from file content. Each extracted value will be passed to onAdd. */ extractValues?: (text: string) => string[] /** Tooltip text for the file input button */ @@ -492,7 +492,7 @@ const TagInput = React.forwardRef( className='-m-1.5 absolute right-2 bottom-[9px] p-1.5 text-[var(--text-tertiary)] transition-colors hover-hover:text-[var(--text-secondary)]' aria-label={fileInputOptions?.tooltip ?? 'Upload file'} > - + diff --git a/packages/emcn/src/components/time-picker/time-picker.tsx b/packages/emcn/src/components/time-picker/time-picker.tsx index f12462ba9ce..8b6ccb0bfaa 100644 --- a/packages/emcn/src/components/time-picker/time-picker.tsx +++ b/packages/emcn/src/components/time-picker/time-picker.tsx @@ -27,7 +27,7 @@ import * as React from 'react' import { cva, type VariantProps } from 'class-variance-authority' -import { ChevronDown } from 'lucide-react' +import { ChevronDown } from '../../icons' import { cn } from '../../lib/cn' import { Popover, PopoverAnchor, PopoverContent } from '../popover/popover' diff --git a/packages/emcn/src/components/toast/toast.tsx b/packages/emcn/src/components/toast/toast.tsx index 5f8cb5a8a86..4798688cfc0 100644 --- a/packages/emcn/src/components/toast/toast.tsx +++ b/packages/emcn/src/components/toast/toast.tsx @@ -339,7 +339,7 @@ function ToastItem({ toast: t, geometry, reduceMotion, onDismiss, onMeasure }: T onClick={dismiss} aria-label='Dismiss notification' title='Dismiss' - className='size-[18px] rounded-sm p-0' + size='icon' > @@ -358,7 +358,6 @@ function ToastItem({ toast: t, geometry, reduceMotion, onDismiss, onMeasure }: T {t.action ? ( { t.action!.onClick() dismiss() diff --git a/packages/emcn/src/icons/user-plus.tsx b/packages/emcn/src/icons/arrow-left-right.tsx similarity index 56% rename from packages/emcn/src/icons/user-plus.tsx rename to packages/emcn/src/icons/arrow-left-right.tsx index d2c6a30ea71..b29836ab80b 100644 --- a/packages/emcn/src/icons/user-plus.tsx +++ b/packages/emcn/src/icons/arrow-left-right.tsx @@ -1,10 +1,10 @@ import type { SVGProps } from 'react' /** - * UserPlus icon component — person silhouette with a plus sign in the bottom-right + * ArrowLeftRight icon component - stacked left and right arrows for swap toggles * @param props - SVG properties including className, fill, etc. */ -export function UserPlus(props: SVGProps) { +export function ArrowLeftRight(props: SVGProps) { return ( ) { aria-hidden='true' {...props} > - - - - + + + + ) } diff --git a/packages/emcn/src/icons/arrow-up-left.tsx b/packages/emcn/src/icons/arrow-up-left.tsx new file mode 100644 index 00000000000..3410ab3fb89 --- /dev/null +++ b/packages/emcn/src/icons/arrow-up-left.tsx @@ -0,0 +1,26 @@ +import type { SVGProps } from 'react' + +/** + * ArrowUpLeft icon component - diagonal arrow pointing to the upper-left corner + * @param props - SVG properties including className, fill, etc. + */ +export function ArrowUpLeft(props: SVGProps) { + return ( + + ) +} diff --git a/packages/emcn/src/icons/ban.tsx b/packages/emcn/src/icons/ban.tsx new file mode 100644 index 00000000000..42886de1c0d --- /dev/null +++ b/packages/emcn/src/icons/ban.tsx @@ -0,0 +1,26 @@ +import type { SVGProps } from 'react' + +/** + * Ban icon component - prohibition sign, a circle crossed by one diagonal bar + * @param props - SVG properties including className, fill, etc. + */ +export function Ban(props: SVGProps) { + return ( + + ) +} diff --git a/packages/emcn/src/icons/bold.tsx b/packages/emcn/src/icons/bold.tsx new file mode 100644 index 00000000000..bbc6754a3d8 --- /dev/null +++ b/packages/emcn/src/icons/bold.tsx @@ -0,0 +1,26 @@ +import type { SVGProps } from 'react' + +/** + * Bold icon component - capital B with a vertical stem and two stacked bowls + * @param props - SVG properties including className, fill, etc. + */ +export function Bold(props: SVGProps) { + return ( + + ) +} diff --git a/packages/emcn/src/icons/box.tsx b/packages/emcn/src/icons/box.tsx new file mode 100644 index 00000000000..32cab06e83c --- /dev/null +++ b/packages/emcn/src/icons/box.tsx @@ -0,0 +1,27 @@ +import type { SVGProps } from 'react' + +/** + * Box icon component - isometric 3D cube + * @param props - SVG properties including className, fill, etc. + */ +export function Box(props: SVGProps) { + return ( + + ) +} diff --git a/packages/emcn/src/icons/brain-circuit.tsx b/packages/emcn/src/icons/brain-circuit.tsx new file mode 100644 index 00000000000..aa0430d194e --- /dev/null +++ b/packages/emcn/src/icons/brain-circuit.tsx @@ -0,0 +1,28 @@ +import type { SVGProps } from 'react' + +/** + * BrainCircuit icon component - half brain with circuit traces branching to node dots + * @param props - SVG properties including className, fill, etc. + */ +export function BrainCircuit(props: SVGProps) { + return ( + + ) +} diff --git a/packages/emcn/src/icons/brain.tsx b/packages/emcn/src/icons/brain.tsx new file mode 100644 index 00000000000..98bbcee58ad --- /dev/null +++ b/packages/emcn/src/icons/brain.tsx @@ -0,0 +1,27 @@ +import type { SVGProps } from 'react' + +/** + * Brain icon component - two mirrored lobed halves joined by a central seam + * @param props - SVG properties including className, fill, etc. + */ +export function Brain(props: SVGProps) { + return ( + + ) +} diff --git a/packages/emcn/src/icons/bubble-chat-delay.tsx b/packages/emcn/src/icons/bubble-chat-delay.tsx index 490cf2b7f83..4ac022f6be7 100644 --- a/packages/emcn/src/icons/bubble-chat-delay.tsx +++ b/packages/emcn/src/icons/bubble-chat-delay.tsx @@ -25,14 +25,14 @@ export function BubbleChatDelay({ size = 24, width, height, ...props }: IconProp stroke='currentColor' strokeLinecap='round' strokeLinejoin='round' - strokeWidth='1.5' + strokeWidth='1.55' /> ) diff --git a/packages/emcn/src/icons/building.tsx b/packages/emcn/src/icons/building.tsx new file mode 100644 index 00000000000..25551bf52ac --- /dev/null +++ b/packages/emcn/src/icons/building.tsx @@ -0,0 +1,27 @@ +import type { SVGProps } from 'react' + +/** + * Building icon component - office tower with windows and a shorter annex + * @param props - SVG properties including className, fill, etc. + */ +export function Building(props: SVGProps) { + return ( + + ) +} diff --git a/packages/emcn/src/icons/camera.tsx b/packages/emcn/src/icons/camera.tsx new file mode 100644 index 00000000000..461520ce560 --- /dev/null +++ b/packages/emcn/src/icons/camera.tsx @@ -0,0 +1,27 @@ +import type { SVGProps } from 'react' + +/** + * Camera icon component - camera body with a raised viewfinder bump and a centered lens + * @param props - SVG properties including className, fill, etc. + */ +export function Camera(props: SVGProps) { + return ( + + ) +} diff --git a/packages/emcn/src/icons/chevron-up.tsx b/packages/emcn/src/icons/chevron-up.tsx new file mode 100644 index 00000000000..ae77e46b4ba --- /dev/null +++ b/packages/emcn/src/icons/chevron-up.tsx @@ -0,0 +1,25 @@ +import type { SVGProps } from 'react' + +/** + * ChevronUp icon component - single chevron pointing up + * @param props - SVG properties including className, fill, etc. + */ +export function ChevronUp(props: SVGProps) { + return ( + + ) +} diff --git a/packages/emcn/src/icons/chevrons-down-up.tsx b/packages/emcn/src/icons/chevrons-down-up.tsx new file mode 100644 index 00000000000..e6817a32ebb --- /dev/null +++ b/packages/emcn/src/icons/chevrons-down-up.tsx @@ -0,0 +1,26 @@ +import type { SVGProps } from 'react' + +/** + * ChevronsDownUp icon component - stacked chevrons pointing toward each other + * @param props - SVG properties including className, fill, etc. + */ +export function ChevronsDownUp(props: SVGProps) { + return ( + + ) +} diff --git a/packages/emcn/src/icons/chevrons-up-down.tsx b/packages/emcn/src/icons/chevrons-up-down.tsx new file mode 100644 index 00000000000..62d52512cc5 --- /dev/null +++ b/packages/emcn/src/icons/chevrons-up-down.tsx @@ -0,0 +1,26 @@ +import type { SVGProps } from 'react' + +/** + * ChevronsUpDown icon component - stacked chevrons pointing away from each other + * @param props - SVG properties including className, fill, etc. + */ +export function ChevronsUpDown(props: SVGProps) { + return ( + + ) +} diff --git a/packages/emcn/src/icons/circle-pause.tsx b/packages/emcn/src/icons/circle-pause.tsx new file mode 100644 index 00000000000..0d5915446ce --- /dev/null +++ b/packages/emcn/src/icons/circle-pause.tsx @@ -0,0 +1,27 @@ +import type { SVGProps } from 'react' + +/** + * CirclePause icon component - circle enclosing two vertical bars, used for waiting states + * @param props - SVG properties including className, fill, etc. + */ +export function CirclePause(props: SVGProps) { + return ( + + ) +} diff --git a/packages/emcn/src/icons/circle-x.tsx b/packages/emcn/src/icons/circle-x.tsx new file mode 100644 index 00000000000..73a83c8f307 --- /dev/null +++ b/packages/emcn/src/icons/circle-x.tsx @@ -0,0 +1,27 @@ +import type { SVGProps } from 'react' + +/** + * CircleX icon component - circle enclosing an X, used for error and failed states + * @param props - SVG properties including className, fill, etc. + */ +export function CircleX(props: SVGProps) { + return ( + + ) +} diff --git a/packages/emcn/src/icons/circle.tsx b/packages/emcn/src/icons/circle.tsx new file mode 100644 index 00000000000..910c0803309 --- /dev/null +++ b/packages/emcn/src/icons/circle.tsx @@ -0,0 +1,25 @@ +import type { SVGProps } from 'react' + +/** + * Circle icon component - plain unfilled ring, fills when given `fill-current` + * @param props - SVG properties including className, fill, etc. + */ +export function Circle(props: SVGProps) { + return ( + + ) +} diff --git a/packages/emcn/src/icons/clipboard.tsx b/packages/emcn/src/icons/clipboard.tsx index 1bf26d7af58..9683e9a445a 100644 --- a/packages/emcn/src/icons/clipboard.tsx +++ b/packages/emcn/src/icons/clipboard.tsx @@ -12,7 +12,7 @@ export function Clipboard(props: SVGProps) { viewBox='-1 -2 24 24' fill='none' stroke='currentColor' - strokeWidth='1.75' + strokeWidth='1.55' strokeLinecap='round' strokeLinejoin='round' xmlns='http://www.w3.org/2000/svg' diff --git a/packages/emcn/src/icons/code.tsx b/packages/emcn/src/icons/code.tsx new file mode 100644 index 00000000000..23ca3105d31 --- /dev/null +++ b/packages/emcn/src/icons/code.tsx @@ -0,0 +1,26 @@ +import type { SVGProps } from 'react' + +/** + * Code icon component - two angle brackets pointing away from a shared centre + * @param props - SVG properties including className, fill, etc. + */ +export function Code(props: SVGProps) { + return ( + + ) +} diff --git a/packages/emcn/src/icons/compass.tsx b/packages/emcn/src/icons/compass.tsx new file mode 100644 index 00000000000..81ddda7ae3a --- /dev/null +++ b/packages/emcn/src/icons/compass.tsx @@ -0,0 +1,27 @@ +import type { SVGProps } from 'react' + +/** + * Compass icon component - dial with a two-tone needle pointing north-east + * @param props - SVG properties including className, fill, etc. + */ +export function Compass(props: SVGProps) { + return ( + + ) +} diff --git a/packages/emcn/src/icons/credit.tsx b/packages/emcn/src/icons/credit.tsx index 5e5a4e0c43b..98b0c3ee25c 100644 --- a/packages/emcn/src/icons/credit.tsx +++ b/packages/emcn/src/icons/credit.tsx @@ -1,7 +1,7 @@ import type { SVGProps } from 'react' /** - * Credit icon component - circular token with inner ring + * Credit icon component - coin with an inner ring * @param props - SVG properties including className, fill, etc. */ export function Credit(props: SVGProps) { @@ -19,8 +19,8 @@ export function Credit(props: SVGProps) { aria-hidden='true' {...props} > - - + + ) } diff --git a/packages/emcn/src/icons/database.tsx b/packages/emcn/src/icons/database.tsx index d0a221cdeb2..415e8ada1a5 100644 --- a/packages/emcn/src/icons/database.tsx +++ b/packages/emcn/src/icons/database.tsx @@ -1,7 +1,7 @@ import type { SVGProps } from 'react' /** - * Database icon component - cylinder with horizontal dividers + * Database icon component - cylinder with a single tier divider * @param props - SVG properties including className, fill, etc. */ export function Database(props: SVGProps) { @@ -19,9 +19,9 @@ export function Database(props: SVGProps) { aria-hidden='true' {...props} > - - - + + + ) } diff --git a/packages/emcn/src/icons/eye-off.tsx b/packages/emcn/src/icons/eye-off.tsx index 62cc6922ae8..1196edad71c 100644 --- a/packages/emcn/src/icons/eye-off.tsx +++ b/packages/emcn/src/icons/eye-off.tsx @@ -17,7 +17,7 @@ export function EyeOff(props: SVGProps) { viewBox='0 0 24 24' fill='none' stroke='currentColor' - strokeWidth='2' + strokeWidth='1.55' strokeLinecap='round' strokeLinejoin='round' xmlns='http://www.w3.org/2000/svg' diff --git a/packages/emcn/src/icons/file-text.tsx b/packages/emcn/src/icons/file-text.tsx new file mode 100644 index 00000000000..c47d022e608 --- /dev/null +++ b/packages/emcn/src/icons/file-text.tsx @@ -0,0 +1,27 @@ +import type { SVGProps } from 'react' + +/** + * FileText icon component - document with folded corner and text lines + * @param props - SVG properties including className, fill, etc. + */ +export function FileText(props: SVGProps) { + return ( + + ) +} diff --git a/packages/emcn/src/icons/files.tsx b/packages/emcn/src/icons/files.tsx index c7ed29bfdbb..ac2226b6f3a 100644 --- a/packages/emcn/src/icons/files.tsx +++ b/packages/emcn/src/icons/files.tsx @@ -1,7 +1,7 @@ import type { SVGProps } from 'react' /** - * Files icon component - stacked documents with folded corner on the front doc + * Files icon component - a document with a folded corner over a second sheet * @param props - SVG properties including className, fill, etc. */ export function Files(props: SVGProps) { @@ -19,9 +19,9 @@ export function Files(props: SVGProps) { aria-hidden='true' {...props} > - - - + + + ) } diff --git a/packages/emcn/src/icons/folder-input.tsx b/packages/emcn/src/icons/folder-input.tsx index 3a47e4e9b1c..022eecc6cf7 100644 --- a/packages/emcn/src/icons/folder-input.tsx +++ b/packages/emcn/src/icons/folder-input.tsx @@ -14,7 +14,7 @@ export function FolderInput(props: SVGProps) { diff --git a/packages/emcn/src/icons/folder-open.tsx b/packages/emcn/src/icons/folder-open.tsx new file mode 100644 index 00000000000..66ad48cb6b5 --- /dev/null +++ b/packages/emcn/src/icons/folder-open.tsx @@ -0,0 +1,26 @@ +import type { SVGProps } from 'react' + +/** + * FolderOpen icon component - folder with its front panel tilted open + * @param props - SVG properties including className, fill, etc. + */ +export function FolderOpen(props: SVGProps) { + return ( + + ) +} diff --git a/packages/emcn/src/icons/form-input.tsx b/packages/emcn/src/icons/form-input.tsx new file mode 100644 index 00000000000..fd3ada471ad --- /dev/null +++ b/packages/emcn/src/icons/form-input.tsx @@ -0,0 +1,27 @@ +import type { SVGProps } from 'react' + +/** + * FormInput icon component - input field with a text caret and placeholder dashes + * @param props - SVG properties including className, fill, etc. + */ +export function FormInput(props: SVGProps) { + return ( + + ) +} diff --git a/packages/emcn/src/icons/globe.tsx b/packages/emcn/src/icons/globe.tsx new file mode 100644 index 00000000000..0e6a5fc99c2 --- /dev/null +++ b/packages/emcn/src/icons/globe.tsx @@ -0,0 +1,27 @@ +import type { SVGProps } from 'react' + +/** + * Globe icon component - circle with a meridian ellipse and two latitude lines + * @param props - SVG properties including className, fill, etc. + */ +export function Globe(props: SVGProps) { + return ( + + ) +} diff --git a/packages/emcn/src/icons/heading1.tsx b/packages/emcn/src/icons/heading1.tsx new file mode 100644 index 00000000000..f5542145d86 --- /dev/null +++ b/packages/emcn/src/icons/heading1.tsx @@ -0,0 +1,28 @@ +import type { SVGProps } from 'react' + +/** + * Heading1 icon component - capital H with a numeral 1 set to its right + * @param props - SVG properties including className, fill, etc. + */ +export function Heading1(props: SVGProps) { + return ( + + ) +} diff --git a/packages/emcn/src/icons/heading2.tsx b/packages/emcn/src/icons/heading2.tsx new file mode 100644 index 00000000000..d3766c61b83 --- /dev/null +++ b/packages/emcn/src/icons/heading2.tsx @@ -0,0 +1,28 @@ +import type { SVGProps } from 'react' + +/** + * Heading2 icon component - capital H with a numeral 2 set to its right + * @param props - SVG properties including className, fill, etc. + */ +export function Heading2(props: SVGProps) { + return ( + + ) +} diff --git a/packages/emcn/src/icons/heading3.tsx b/packages/emcn/src/icons/heading3.tsx new file mode 100644 index 00000000000..bc1e7aec949 --- /dev/null +++ b/packages/emcn/src/icons/heading3.tsx @@ -0,0 +1,28 @@ +import type { SVGProps } from 'react' + +/** + * Heading3 icon component - capital H with a numeral 3 set to its right + * @param props - SVG properties including className, fill, etc. + */ +export function Heading3(props: SVGProps) { + return ( + + ) +} diff --git a/packages/emcn/src/icons/help-circle.tsx b/packages/emcn/src/icons/help-circle.tsx index 8ea21b8f8cd..4ed81d38d0c 100644 --- a/packages/emcn/src/icons/help-circle.tsx +++ b/packages/emcn/src/icons/help-circle.tsx @@ -9,7 +9,7 @@ export function HelpCircle(props: SVGProps) { ) { aria-hidden='true' {...props} > - - - + + + ) } diff --git a/packages/emcn/src/icons/highlighter.tsx b/packages/emcn/src/icons/highlighter.tsx new file mode 100644 index 00000000000..3b14d9e5776 --- /dev/null +++ b/packages/emcn/src/icons/highlighter.tsx @@ -0,0 +1,27 @@ +import type { SVGProps } from 'react' + +/** + * Highlighter icon component - chisel-tip marker angled up-right above a highlighted bar + * @param props - SVG properties including className, fill, etc. + */ +export function Highlighter(props: SVGProps) { + return ( + + ) +} diff --git a/packages/emcn/src/icons/index.ts b/packages/emcn/src/icons/index.ts index b6017814ca8..6c8d3c14159 100644 --- a/packages/emcn/src/icons/index.ts +++ b/packages/emcn/src/icons/index.ts @@ -1,29 +1,46 @@ export { ArrowDown } from './arrow-down' export { ArrowLeft } from './arrow-left' +export { ArrowLeftRight } from './arrow-left-right' export { ArrowRight } from './arrow-right' export { ArrowUp } from './arrow-up' export { ArrowUpDown } from './arrow-up-down' +export { ArrowUpLeft } from './arrow-up-left' export { Asterisk } from './asterisk' +export { Ban } from './ban' export { Bell } from './bell' export { Blimp } from './blimp' +export { Bold } from './bold' export { BookOpen } from './book-open' +export { Box } from './box' +export { Brain } from './brain' +export { BrainCircuit } from './brain-circuit' export { BubbleChatClose } from './bubble-chat-close' export { BubbleChatDelay } from './bubble-chat-delay' export { BubbleChatPreview } from './bubble-chat-preview' export { Bug } from './bug' +export { Building } from './building' export { Calendar } from './calendar' +export { Camera } from './camera' export { Check } from './check' export { ChevronDown } from './chevron-down' export { ChevronLeft } from './chevron-left' export { ChevronRight } from './chevron-right' +export { ChevronUp } from './chevron-up' +export { ChevronsDownUp } from './chevrons-down-up' +export { ChevronsUpDown } from './chevrons-up-down' +export { Circle } from './circle' export { CircleAlert } from './circle-alert' export { CircleCheck } from './circle-check' export { CircleInfo } from './circle-info' +export { CirclePause } from './circle-pause' +export { CircleX } from './circle-x' export { Clipboard } from './clipboard' export { ClipboardList } from './clipboard-list' export { Clock } from './clock' +export { Code } from './code' export { Columns2 } from './columns2' export { Columns3 } from './columns3' +export { Compass } from './compass' export { Connections } from './connections' export { Credit } from './credit' export { Cursor } from './cursor' @@ -36,40 +53,62 @@ export { Expand } from './expand' export { Eye } from './eye' export { EyeOff } from './eye-off' export { File } from './file' +export { FileText } from './file-text' export { FileX } from './file-x' export { Files } from './files' export { Fingerprint } from './fingerprint' export { Folder } from './folder' export { FolderCode } from './folder-code' export { FolderInput } from './folder-input' +export { FolderOpen } from './folder-open' export { FolderPlus } from './folder-plus' +export { FormInput } from './form-input' +export { Globe } from './globe' export { Hammer } from './hammer' export { Hand } from './hand' +export { Heading1 } from './heading1' +export { Heading2 } from './heading2' +export { Heading3 } from './heading3' export { HelpCircle } from './help-circle' export { HexSimple } from './hex-simple' +export { Highlighter } from './highlighter' export { Home } from './home' export { ImageUp } from './image-up' +export { InfinityIcon, InfinityIcon as Infinity } from './infinity' export { Integration } from './integration' +export { Italic } from './italic' export { Key } from './key' export { KeySquare } from './key-square' export { Layout } from './layout' export { Library } from './library' export { Link } from './link' +export { List } from './list' +export { ListChecks } from './list-checks' export { ListFilter } from './list-filter' +export { ListOrdered } from './list-ordered' export { Loader } from './loader' export { Lock } from './lock' export { LogIn } from './log-in' export { LogOut } from './log-out' export { Mail } from './mail' export { ManageWorkspace } from './manage-workspace' +export { Menu } from './menu' export { Mic } from './mic' +export { MicOff } from './mic-off' +export { Minus } from './minus' +export { Moon } from './moon' export { MoreHorizontal } from './more-horizontal' +export { MoreVertical } from './more-vertical' +export { Music } from './music' export { NoWrap } from './no-wrap' export { Palette } from './palette' export { PanelLeft } from './panel-left' +export { PanelRight } from './panel-right' export { Paperclip } from './paperclip' export { Pause } from './pause' export { Pencil } from './pencil' +export { Phone } from './phone' +export { Pilcrow } from './pilcrow' export { PillsRing } from './pills-ring' export { Pin } from './pin' export { PinOff } from './pin-off' @@ -77,31 +116,38 @@ export { Play, PlayOutline } from './play' export { Plus } from './plus' export { Redo } from './redo' export { RefreshCw } from './refresh-cw' +export { Repeat } from './repeat' export { Rocket } from './rocket' export { Rows3 } from './rows3' +export { Rss } from './rss' +export { Scissors } from './scissors' export { Search } from './search' export { SelectAll } from './select-all' export { Send } from './send' +export { SendToBack } from './send-to-back' export { Server } from './server' export { Settings } from './settings' +export { Share } from './share' export { ShieldCheck } from './shield-check' export { Shuffle } from './shuffle' export { Sim } from './sim' export { Slash } from './slash' +export { Sparkles } from './sparkles' export { Split } from './split' export { Sprout } from './sprout' export { Square } from './square' export { SquareArrowUpRight } from './square-arrow-up-right' +export { Strikethrough } from './strikethrough' +export { Sun } from './sun' export { Table } from './table' export { TableX } from './table-x' export { TagIcon } from './tag' export { Task } from './task' export { TerminalWindow } from './terminal-window' +export { TextQuote } from './text-quote' export { ThumbsDown } from './thumbs-down' export { ThumbsUp } from './thumbs-up' export { Trash } from './trash' -export { TrashOutline } from './trash-outline' -export { Trash2 } from './trash2' export { TriangleAlert } from './triangle-alert' export { TypeBoolean } from './type-boolean' export { TypeCurrency } from './type-currency' @@ -109,16 +155,19 @@ export { TypeJson } from './type-json' export { TypeNumber } from './type-number' export { TypeText } from './type-text' export { Undo } from './undo' +export { Unlink } from './unlink' export { Unlock } from './unlock' export { Upload } from './upload' export { User } from './user' -export { UserPlus } from './user-plus' export { Users } from './users' +export { Wand } from './wand' +export { Webhook } from './webhook' export { Wordmark } from './wordmark' export { Workflow } from './workflow' export { WorkflowX } from './workflow-x' export { Wrap } from './wrap' export { Wrench } from './wrench' export { X } from './x' +export { Zap } from './zap' export { ZoomIn } from './zoom-in' export { ZoomOut } from './zoom-out' diff --git a/packages/emcn/src/icons/infinity.tsx b/packages/emcn/src/icons/infinity.tsx new file mode 100644 index 00000000000..8a0bd923eff --- /dev/null +++ b/packages/emcn/src/icons/infinity.tsx @@ -0,0 +1,25 @@ +import type { SVGProps } from 'react' + +/** + * Infinity icon component - lemniscate drawn as one continuous stroke + * @param props - SVG properties including className, fill, etc. + */ +export function InfinityIcon(props: SVGProps) { + return ( + + ) +} diff --git a/packages/emcn/src/icons/italic.tsx b/packages/emcn/src/icons/italic.tsx new file mode 100644 index 00000000000..abc0855a821 --- /dev/null +++ b/packages/emcn/src/icons/italic.tsx @@ -0,0 +1,27 @@ +import type { SVGProps } from 'react' + +/** + * Italic icon component - slanted stroke with short serifs at its top and bottom + * @param props - SVG properties including className, fill, etc. + */ +export function Italic(props: SVGProps) { + return ( + + ) +} diff --git a/packages/emcn/src/icons/library.tsx b/packages/emcn/src/icons/library.tsx index 1bd3d0b8428..13eab8d6d84 100644 --- a/packages/emcn/src/icons/library.tsx +++ b/packages/emcn/src/icons/library.tsx @@ -1,7 +1,7 @@ import type { SVGProps } from 'react' /** - * Library icon component - displays stacked books/documents + * Library icon component - stacked log entries, each a short marker beside its line * @param props - SVG properties including className, fill, etc. */ export function Library(props: SVGProps) { @@ -19,10 +19,12 @@ export function Library(props: SVGProps) { aria-hidden='true' {...props} > - - - - + + + + + + ) } diff --git a/packages/emcn/src/icons/list-checks.tsx b/packages/emcn/src/icons/list-checks.tsx new file mode 100644 index 00000000000..264a2602d19 --- /dev/null +++ b/packages/emcn/src/icons/list-checks.tsx @@ -0,0 +1,26 @@ +import type { SVGProps } from 'react' + +/** + * ListChecks icon component - three lines, each preceded by a checkmark + * @param props - SVG properties including className, fill, etc. + */ +export function ListChecks(props: SVGProps) { + return ( + + ) +} diff --git a/packages/emcn/src/icons/list-ordered.tsx b/packages/emcn/src/icons/list-ordered.tsx new file mode 100644 index 00000000000..32e8728efee --- /dev/null +++ b/packages/emcn/src/icons/list-ordered.tsx @@ -0,0 +1,28 @@ +import type { SVGProps } from 'react' + +/** + * ListOrdered icon component - three lines, each preceded by the numerals 1, 2 and 3 + * @param props - SVG properties including className, fill, etc. + */ +export function ListOrdered(props: SVGProps) { + return ( + + ) +} diff --git a/packages/emcn/src/icons/list.tsx b/packages/emcn/src/icons/list.tsx new file mode 100644 index 00000000000..77bf53df303 --- /dev/null +++ b/packages/emcn/src/icons/list.tsx @@ -0,0 +1,28 @@ +import type { SVGProps } from 'react' + +/** + * List icon component - three lines, each preceded by a round bullet dot + * @param props - SVG properties including className, fill, etc. + */ +export function List(props: SVGProps) { + return ( + + ) +} diff --git a/packages/emcn/src/icons/menu.tsx b/packages/emcn/src/icons/menu.tsx new file mode 100644 index 00000000000..fc2c5cdf47e --- /dev/null +++ b/packages/emcn/src/icons/menu.tsx @@ -0,0 +1,27 @@ +import type { SVGProps } from 'react' + +/** + * Menu icon component - three equal-length horizontal bars (hamburger) + * @param props - SVG properties including className, fill, etc. + */ +export function Menu(props: SVGProps) { + return ( + + ) +} diff --git a/packages/emcn/src/icons/mic-off.tsx b/packages/emcn/src/icons/mic-off.tsx new file mode 100644 index 00000000000..a434155ad7d --- /dev/null +++ b/packages/emcn/src/icons/mic-off.tsx @@ -0,0 +1,28 @@ +import type { SVGProps } from 'react' + +/** + * MicOff icon component - microphone with diagonal strike-through for the muted state + * @param props - SVG properties including className, fill, etc. + */ +export function MicOff(props: SVGProps) { + return ( + + ) +} diff --git a/packages/emcn/src/icons/minus.tsx b/packages/emcn/src/icons/minus.tsx new file mode 100644 index 00000000000..ddab0bbaf84 --- /dev/null +++ b/packages/emcn/src/icons/minus.tsx @@ -0,0 +1,25 @@ +import type { SVGProps } from 'react' + +/** + * Minus icon component - single horizontal bar matching the horizontal stroke of Plus + * @param props - SVG properties including className, fill, etc. + */ +export function Minus(props: SVGProps) { + return ( + + ) +} diff --git a/packages/emcn/src/icons/moon.tsx b/packages/emcn/src/icons/moon.tsx new file mode 100644 index 00000000000..aa36f144530 --- /dev/null +++ b/packages/emcn/src/icons/moon.tsx @@ -0,0 +1,25 @@ +import type { SVGProps } from 'react' + +/** + * Moon icon component - crescent moon for dark mode + * @param props - SVG properties including className, fill, etc. + */ +export function Moon(props: SVGProps) { + return ( + + ) +} diff --git a/packages/emcn/src/icons/more-vertical.tsx b/packages/emcn/src/icons/more-vertical.tsx new file mode 100644 index 00000000000..1d8335716ff --- /dev/null +++ b/packages/emcn/src/icons/more-vertical.tsx @@ -0,0 +1,27 @@ +import type { SVGProps } from 'react' + +/** + * MoreVertical icon component - three evenly spaced vertical dots + * @param props - SVG properties including className, fill, etc. + */ +export function MoreVertical(props: SVGProps) { + return ( + + ) +} diff --git a/packages/emcn/src/icons/music.tsx b/packages/emcn/src/icons/music.tsx new file mode 100644 index 00000000000..b746acc7407 --- /dev/null +++ b/packages/emcn/src/icons/music.tsx @@ -0,0 +1,27 @@ +import type { SVGProps } from 'react' + +/** + * Music icon component - two eighth notes joined by a single beam + * @param props - SVG properties including className, fill, etc. + */ +export function Music(props: SVGProps) { + return ( + + ) +} diff --git a/packages/emcn/src/icons/panel-right.tsx b/packages/emcn/src/icons/panel-right.tsx new file mode 100644 index 00000000000..b862215be85 --- /dev/null +++ b/packages/emcn/src/icons/panel-right.tsx @@ -0,0 +1,26 @@ +import type { SVGProps } from 'react' + +/** + * PanelRight icon component - sidebar panel docked to the right edge + * @param props - SVG properties including className, fill, etc. + */ +export function PanelRight(props: SVGProps) { + return ( + + ) +} diff --git a/packages/emcn/src/icons/phone.tsx b/packages/emcn/src/icons/phone.tsx new file mode 100644 index 00000000000..f190ecf2ca6 --- /dev/null +++ b/packages/emcn/src/icons/phone.tsx @@ -0,0 +1,25 @@ +import type { SVGProps } from 'react' + +/** + * Phone icon component - angled handset outline, centred so a 135 degree rotation reads as hang up + * @param props - SVG properties including className, fill, etc. + */ +export function Phone(props: SVGProps) { + return ( + + ) +} diff --git a/packages/emcn/src/icons/pilcrow.tsx b/packages/emcn/src/icons/pilcrow.tsx new file mode 100644 index 00000000000..44bd955bb76 --- /dev/null +++ b/packages/emcn/src/icons/pilcrow.tsx @@ -0,0 +1,27 @@ +import type { SVGProps } from 'react' + +/** + * Pilcrow icon component - paragraph mark with a closed bowl and two descending stems + * @param props - SVG properties including className, fill, etc. + */ +export function Pilcrow(props: SVGProps) { + return ( + + ) +} diff --git a/packages/emcn/src/icons/pin-off.tsx b/packages/emcn/src/icons/pin-off.tsx index 0f1bf606275..3f824d8f97a 100644 --- a/packages/emcn/src/icons/pin-off.tsx +++ b/packages/emcn/src/icons/pin-off.tsx @@ -13,7 +13,7 @@ export function PinOff(props: SVGProps) { viewBox='0 0 24 24' fill='none' stroke='currentColor' - strokeWidth='1.75' + strokeWidth='1.55' strokeLinecap='round' strokeLinejoin='round' aria-hidden='true' diff --git a/packages/emcn/src/icons/pin.tsx b/packages/emcn/src/icons/pin.tsx index 0e9fbfec2a0..12168b5675f 100644 --- a/packages/emcn/src/icons/pin.tsx +++ b/packages/emcn/src/icons/pin.tsx @@ -13,7 +13,7 @@ export function Pin(props: SVGProps) { viewBox='0 0 24 24' fill='none' stroke='currentColor' - strokeWidth='1.75' + strokeWidth='1.55' strokeLinecap='round' strokeLinejoin='round' aria-hidden='true' diff --git a/packages/emcn/src/icons/repeat.tsx b/packages/emcn/src/icons/repeat.tsx new file mode 100644 index 00000000000..31d1f51ec12 --- /dev/null +++ b/packages/emcn/src/icons/repeat.tsx @@ -0,0 +1,28 @@ +import type { SVGProps } from 'react' + +/** + * Repeat icon component - two arrows joined into a rectangular loop + * @param props - SVG properties including className, fill, etc. + */ +export function Repeat(props: SVGProps) { + return ( + + ) +} diff --git a/packages/emcn/src/icons/rss.tsx b/packages/emcn/src/icons/rss.tsx new file mode 100644 index 00000000000..4336e0dc59c --- /dev/null +++ b/packages/emcn/src/icons/rss.tsx @@ -0,0 +1,27 @@ +import type { SVGProps } from 'react' + +/** + * Rss icon component - two concentric quarter arcs radiating from a corner dot + * @param props - SVG properties including className, fill, etc. + */ +export function Rss(props: SVGProps) { + return ( + + ) +} diff --git a/packages/emcn/src/icons/scissors.tsx b/packages/emcn/src/icons/scissors.tsx new file mode 100644 index 00000000000..f68fc1a8f9d --- /dev/null +++ b/packages/emcn/src/icons/scissors.tsx @@ -0,0 +1,28 @@ +import type { SVGProps } from 'react' + +/** + * Scissors icon component - crossed blades with ring handles + * @param props - SVG properties including className, fill, etc. + */ +export function Scissors(props: SVGProps) { + return ( + + ) +} diff --git a/packages/emcn/src/icons/select-all.tsx b/packages/emcn/src/icons/select-all.tsx index a57f4a87c8f..752177bf8d6 100644 --- a/packages/emcn/src/icons/select-all.tsx +++ b/packages/emcn/src/icons/select-all.tsx @@ -12,7 +12,7 @@ export function SelectAll(props: SVGProps) { viewBox='0 0 24 24' fill='none' stroke='currentColor' - strokeWidth='2' + strokeWidth='1.55' strokeLinecap='round' strokeLinejoin='round' xmlns='http://www.w3.org/2000/svg' diff --git a/packages/emcn/src/icons/send-to-back.tsx b/packages/emcn/src/icons/send-to-back.tsx new file mode 100644 index 00000000000..4d9515107a9 --- /dev/null +++ b/packages/emcn/src/icons/send-to-back.tsx @@ -0,0 +1,26 @@ +import type { SVGProps } from 'react' + +/** + * SendToBack icon component - a solid rounded square in front of a clipped one behind it + * @param props - SVG properties including className, fill, etc. + */ +export function SendToBack(props: SVGProps) { + return ( + + ) +} diff --git a/packages/emcn/src/icons/send.tsx b/packages/emcn/src/icons/send.tsx index 3ec578aafd6..e29808cce5b 100644 --- a/packages/emcn/src/icons/send.tsx +++ b/packages/emcn/src/icons/send.tsx @@ -1,7 +1,7 @@ import type { SVGProps } from 'react' /** - * Send icon component - paper plane / arrow + * Send icon component - paper plane * @param props - SVG properties including className, fill, etc. */ export function Send(props: SVGProps) { @@ -19,8 +19,8 @@ export function Send(props: SVGProps) { aria-hidden='true' {...props} > - - + + ) } diff --git a/packages/emcn/src/icons/settings.tsx b/packages/emcn/src/icons/settings.tsx index 8064409535b..42925fdd7f9 100644 --- a/packages/emcn/src/icons/settings.tsx +++ b/packages/emcn/src/icons/settings.tsx @@ -1,7 +1,7 @@ import type { SVGProps } from 'react' /** - * Settings icon component - gear/cog + * Settings icon component - six-tooth gear with a hub * @param props - SVG properties including className, fill, etc. */ export function Settings(props: SVGProps) { @@ -9,7 +9,7 @@ export function Settings(props: SVGProps) { ) { aria-hidden='true' {...props} > - - + + ) } diff --git a/packages/emcn/src/icons/share.tsx b/packages/emcn/src/icons/share.tsx new file mode 100644 index 00000000000..2324fb9a1e6 --- /dev/null +++ b/packages/emcn/src/icons/share.tsx @@ -0,0 +1,28 @@ +import type { SVGProps } from 'react' + +/** + * Share icon component - three nodes joined by two connecting lines + * @param props - SVG properties including className, fill, etc. + */ +export function Share(props: SVGProps) { + return ( + + ) +} diff --git a/packages/emcn/src/icons/sparkles.tsx b/packages/emcn/src/icons/sparkles.tsx new file mode 100644 index 00000000000..d5dde83c6bd --- /dev/null +++ b/packages/emcn/src/icons/sparkles.tsx @@ -0,0 +1,27 @@ +import type { SVGProps } from 'react' + +/** + * Sparkles icon component - one large four-pointed star with two smaller ones + * @param props - SVG properties including className, fill, etc. + */ +export function Sparkles(props: SVGProps) { + return ( + + ) +} diff --git a/packages/emcn/src/icons/square.tsx b/packages/emcn/src/icons/square.tsx index 66bea3a4a09..2e3a45d5177 100644 --- a/packages/emcn/src/icons/square.tsx +++ b/packages/emcn/src/icons/square.tsx @@ -3,8 +3,8 @@ import type { SVGProps } from 'react' /** * Square icon (stroke/outline version) — used as the "stop" glyph in * media-control-style buttons (per-row run/stop, table action bar, context - * menus). Same visual as `lucide-react`'s `Square` so existing call sites - * migrate without any visual change. + * menus). Drawn as a plain rounded square so it reads as a neutral stop mark + * next to `Play` in the same control cluster. */ export function Square(props: SVGProps) { return ( @@ -14,7 +14,7 @@ export function Square(props: SVGProps) { viewBox='0 0 24 24' fill='none' stroke='currentColor' - strokeWidth='1.75' + strokeWidth='1.55' strokeLinecap='round' strokeLinejoin='round' xmlns='http://www.w3.org/2000/svg' diff --git a/packages/emcn/src/icons/strikethrough.tsx b/packages/emcn/src/icons/strikethrough.tsx new file mode 100644 index 00000000000..f4b1f093678 --- /dev/null +++ b/packages/emcn/src/icons/strikethrough.tsx @@ -0,0 +1,26 @@ +import type { SVGProps } from 'react' + +/** + * Strikethrough icon component - capital S cut by a full-width horizontal rule + * @param props - SVG properties including className, fill, etc. + */ +export function Strikethrough(props: SVGProps) { + return ( + + ) +} diff --git a/packages/emcn/src/icons/sun.tsx b/packages/emcn/src/icons/sun.tsx new file mode 100644 index 00000000000..8539a5a5183 --- /dev/null +++ b/packages/emcn/src/icons/sun.tsx @@ -0,0 +1,27 @@ +import type { SVGProps } from 'react' + +/** + * Sun icon component - centered disc with eight radiating rays for light mode + * @param props - SVG properties including className, fill, etc. + */ +export function Sun(props: SVGProps) { + return ( + + ) +} diff --git a/packages/emcn/src/icons/table.tsx b/packages/emcn/src/icons/table.tsx index 57be040a2c2..f9a585ba0a5 100644 --- a/packages/emcn/src/icons/table.tsx +++ b/packages/emcn/src/icons/table.tsx @@ -1,7 +1,7 @@ import type { SVGProps } from 'react' /** - * Table icon component - displays a grid table + * Table icon component - rounded frame with a header row and a column divider * @param props - SVG properties including className, fill, etc. */ export function Table(props: SVGProps) { @@ -22,7 +22,7 @@ export function Table(props: SVGProps) { - + ) } diff --git a/packages/emcn/src/icons/task.tsx b/packages/emcn/src/icons/task.tsx index 85b39b243b8..8df5039a0aa 100644 --- a/packages/emcn/src/icons/task.tsx +++ b/packages/emcn/src/icons/task.tsx @@ -1,26 +1,25 @@ import type { SVGProps } from 'react' /** - * Task icon component - inbox tray representing incoming tasks + * Task icon component - rounded speech bubble with a tail * @param props - SVG properties including className, fill, etc. */ export function Task(props: SVGProps) { return ( ) } diff --git a/packages/emcn/src/icons/text-quote.tsx b/packages/emcn/src/icons/text-quote.tsx new file mode 100644 index 00000000000..56e9fa0305b --- /dev/null +++ b/packages/emcn/src/icons/text-quote.tsx @@ -0,0 +1,26 @@ +import type { SVGProps } from 'react' + +/** + * TextQuote icon component - two full lines above two indented lines marked by a quote bar + * @param props - SVG properties including className, fill, etc. + */ +export function TextQuote(props: SVGProps) { + return ( + + ) +} diff --git a/packages/emcn/src/icons/trash-outline.tsx b/packages/emcn/src/icons/trash-outline.tsx deleted file mode 100644 index 7b112541b96..00000000000 --- a/packages/emcn/src/icons/trash-outline.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import type { SVGProps } from 'react' - -/** - * Outline-style trash icon matching the stroke-based nav icon convention - * @param props - SVG properties including className, fill, etc. - */ -export function TrashOutline(props: SVGProps) { - return ( - - ) -} diff --git a/packages/emcn/src/icons/trash.tsx b/packages/emcn/src/icons/trash.tsx index 6c9a9fc078b..015efa97a19 100644 --- a/packages/emcn/src/icons/trash.tsx +++ b/packages/emcn/src/icons/trash.tsx @@ -1,24 +1,27 @@ import type { SVGProps } from 'react' /** - * Trash icon component + * Trash icon component - lidded bin * @param props - SVG properties including className, fill, etc. */ export function Trash(props: SVGProps) { return ( ) } diff --git a/packages/emcn/src/icons/trash2.tsx b/packages/emcn/src/icons/trash2.tsx deleted file mode 100644 index 9148d809864..00000000000 --- a/packages/emcn/src/icons/trash2.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import type { SVGProps } from 'react' - -/** - * Trash2 icon component - * @param props - SVG properties including className, fill, etc. - */ -export function Trash2(props: SVGProps) { - return ( - - ) -} diff --git a/packages/emcn/src/icons/unlink.tsx b/packages/emcn/src/icons/unlink.tsx new file mode 100644 index 00000000000..884d9044acb --- /dev/null +++ b/packages/emcn/src/icons/unlink.tsx @@ -0,0 +1,27 @@ +import type { SVGProps } from 'react' + +/** + * Unlink icon component - a broken chain, two half links pulled apart with break marks + * @param props - SVG properties including className, fill, etc. + */ +export function Unlink(props: SVGProps) { + return ( + + ) +} diff --git a/packages/emcn/src/icons/users.tsx b/packages/emcn/src/icons/users.tsx index 2169b250b1e..3605b1d51b6 100644 --- a/packages/emcn/src/icons/users.tsx +++ b/packages/emcn/src/icons/users.tsx @@ -1,7 +1,7 @@ import type { SVGProps } from 'react' /** - * Users icon component - two person silhouettes + * Users icon component - two person silhouettes side by side * @param props - SVG properties including className, fill, etc. */ export function Users(props: SVGProps) { @@ -19,10 +19,10 @@ export function Users(props: SVGProps) { aria-hidden='true' {...props} > - - - - + + + + ) } diff --git a/packages/emcn/src/icons/wand.tsx b/packages/emcn/src/icons/wand.tsx new file mode 100644 index 00000000000..bcf9bdddd85 --- /dev/null +++ b/packages/emcn/src/icons/wand.tsx @@ -0,0 +1,27 @@ +import type { SVGProps } from 'react' + +/** + * Wand icon component - angled wand with sparkles at its tip + * @param props - SVG properties including className, fill, etc. + */ +export function Wand(props: SVGProps) { + return ( + + ) +} diff --git a/packages/emcn/src/icons/webhook.tsx b/packages/emcn/src/icons/webhook.tsx new file mode 100644 index 00000000000..457e178251c --- /dev/null +++ b/packages/emcn/src/icons/webhook.tsx @@ -0,0 +1,28 @@ +import type { SVGProps } from 'react' + +/** + * Webhook icon component - three arcs radiating from a shared centre into node dots + * @param props - SVG properties including className, fill, etc. + */ +export function Webhook(props: SVGProps) { + return ( + + ) +} diff --git a/packages/emcn/src/icons/workflow.tsx b/packages/emcn/src/icons/workflow.tsx index 6ee0d55c27c..b31622ae9c6 100644 --- a/packages/emcn/src/icons/workflow.tsx +++ b/packages/emcn/src/icons/workflow.tsx @@ -1,7 +1,7 @@ import type { SVGProps } from 'react' /** - * Workflow icon component - two nested concentric rounded squares conveying a workflow + * Workflow icon component - two nodes joined by an elbow connector * @param props - SVG properties including className, fill, etc. */ export function Workflow(props: SVGProps) { @@ -19,8 +19,9 @@ export function Workflow(props: SVGProps) { aria-hidden='true' {...props} > - - + + + ) } diff --git a/packages/emcn/src/icons/zap.tsx b/packages/emcn/src/icons/zap.tsx new file mode 100644 index 00000000000..a0758035691 --- /dev/null +++ b/packages/emcn/src/icons/zap.tsx @@ -0,0 +1,25 @@ +import type { SVGProps } from 'react' + +/** + * Zap icon component - lightning bolt + * @param props - SVG properties including className, fill, etc. + */ +export function Zap(props: SVGProps) { + return ( + + ) +} diff --git a/packages/emcn/src/index.ts b/packages/emcn/src/index.ts index 5c8509c774d..178bef24c0e 100644 --- a/packages/emcn/src/index.ts +++ b/packages/emcn/src/index.ts @@ -5,6 +5,17 @@ export * from './components' * the COMPONENT; the icon stays available from `@sim/emcn/icons`. */ export { Calendar, type CalendarProps } from './components/calendar/calendar' +/** + * `Code` exists in BOTH `./components` (the code editor) and `./icons` (a + * glyph). Same resolution as `Calendar` and `Table`: the barrel yields the + * COMPONENT, and the icon stays available from `@sim/emcn/icons`. + */ +export { + CODE_LINE_HEIGHT_PX, + Code, + calculateGutterWidth, + getCodeEditorProps, +} from './components/code/code' /** * `Table` exists in BOTH `./components` (data-table element) and `./icons` * (glyph). This explicit re-export resolves the ambiguity to the COMPONENT — diff --git a/packages/workflow-renderer/package.json b/packages/workflow-renderer/package.json index 169217aba7c..ec9d8e0ed30 100644 --- a/packages/workflow-renderer/package.json +++ b/packages/workflow-renderer/package.json @@ -28,7 +28,6 @@ "peerDependencies": { "@sim/emcn": "workspace:*", "@sim/utils": "workspace:*", - "lucide-react": ">=0.479.0", "react": "^19", "reactflow": "^11.11.4", "remark-breaks": "^4.0.0", @@ -39,7 +38,6 @@ "@sim/tsconfig": "workspace:*", "@sim/utils": "workspace:*", "@types/react": "^19", - "lucide-react": "^0.511.0", "react": "19.2.4", "reactflow": "^11.11.4", "remark-breaks": "^4.0.0", diff --git a/packages/workflow-renderer/src/edge/workflow-edge-view.tsx b/packages/workflow-renderer/src/edge/workflow-edge-view.tsx index 6256d8f70df..29c4c6e5ffc 100644 --- a/packages/workflow-renderer/src/edge/workflow-edge-view.tsx +++ b/packages/workflow-renderer/src/edge/workflow-edge-view.tsx @@ -1,5 +1,5 @@ import { useMemo } from 'react' -import { X } from 'lucide-react' +import { X } from '@sim/emcn/icons' import { BaseEdge, EdgeLabelRenderer, type EdgeProps, getSmoothStepPath } from 'reactflow' import type { EdgeDiffStatus, EdgeRunStatus } from '../types' diff --git a/packages/workflow-renderer/src/note/note-block-view.tsx b/packages/workflow-renderer/src/note/note-block-view.tsx index c66369f9e97..bb0c6d48f62 100644 --- a/packages/workflow-renderer/src/note/note-block-view.tsx +++ b/packages/workflow-renderer/src/note/note-block-view.tsx @@ -138,7 +138,7 @@ const NOTE_COMPONENTS = { {children} ), blockquote: ({ children }: { children?: ReactNode }) => ( -
+
{children}
), @@ -216,7 +216,7 @@ export function NoteBlockView({ > {actionBar} -
+