From 9b9da81a27434dbc362485b9a6dd2ae0b00aff80 Mon Sep 17 00:00:00 2001 From: Emir Karabeg <78010029+emir-karabeg@users.noreply.github.com> Date: Tue, 4 Aug 2026 10:28:00 -0700 Subject: [PATCH 01/19] improvement(platform): drop lucide-react for the in-house icon set, flatten the type and border scales, and retire scheduled tasks and workflow references (#6241) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * border styling * improvement(platform): migrate off lucide-react, flatten the font-weight scale, and retire scheduled tasks and workflow references * chore(platform): drop the dead schedule client layer and repair stale rule and skill docs Follow-up cleanup for the platform commit, which removed the workspace scheduled-tasks surface and migrated off lucide-react. Both left dead tails that type-check clean, so nothing flagged them. Six mutation hooks in hooks/queries/schedules.ts lost their only consumer when the scheduled-tasks page was deleted: useDisableSchedule, useResumeSchedule, useDeleteSchedule, useExcludeOccurrence, useUpdateSchedule, useCreateSchedule. They are removed along with the three contract objects that served only them — disableScheduleContract, excludeOccurrenceContract, deleteScheduleContract. disableScheduleBodySchema and excludeOccurrenceBodySchema are deliberately kept: both are members of scheduleUpdateSchema, the discriminated union the live PUT /api/schedules/[id] route parses. Dropping them would collapse the union and 400 the disable and exclude_occurrence actions. The schedule-calendar tree and its utils stay unmounted for later reuse. Its TSDoc now says so, since it has no importer and would otherwise read as dead code on the next sweep. The add-enrichment skill templated an import from lucide-react, a dependency the platform commit deleted, so running it produced an unresolvable import. It now points at @sim/emcn/icons, matching all five shipped enrichments. The emcn-design-review skill and several rule files still pointed at apps/sim/components/emcn/**, which moved to packages/emcn/**. Also corrects the documented Chip variant list — it advertised a ghost variant that never existed and omitted border — repoints the sim-url-state date-parser example at an inline snippet now that its source file is gone, and normalizes the one strokeWidth the icon migration left at 1.5 in bubble-chat-delay. * fix(platform): mark the resource chrome as client components `skills/page.tsx` is a Server Component, and this branch moved its `IntegrationTabsHeader` import onto the `@/app/workspace/[workspaceId]/components` barrel. That barrel re-exports `SortDropdown` from `resource-options`, which calls `useState`, so the server graph now reaches a client-only module and `next build` fails. `resource-header` has the same latent problem (`useState`, `useEffect`, `useRef`). Both files are genuinely client components, so they get the directive rather than the page dropping the barrel import — local feature barrels are the convention here. Also drops a stale `lucide-react` mention now that the dependency is gone. * chore(scheduled-tasks): remove the scheduled-task logic Scheduled tasks are retired. This removes the `sourceType = 'job'` half of `workflow_schedule` from the application, leaving the workflow Schedule trigger (`sourceType = 'workflow'`) untouched. Gone: - the job orchestration layer (`lib/workflows/schedules/orchestration.ts`) and the agent-job runner in `background/schedule-execution.ts` - the job claim/dispatch half of the schedules execute tick - POST /api/schedules (job creation) and the job branches of GET /api/schedules and PUT/DELETE /api/schedules/[id] - the copilot job tools and handlers, the `scheduledtask` resource type and chat-context kind, and the VFS `jobs/` materialization - the scheduled-task analytics events and the job variant of the schedule-disabled email Kept on purpose: `scheduled-tasks/components/schedule-calendar/**` and `scheduled-tasks/utils/**`, which the agents module will reuse. `packages/db/schema.ts` is deliberately untouched — the columns stay for now and come out in a follow-up with a proper expand/contract migration. The generated copilot catalog and VFS snapshot types are regenerated from the matching copilot PR, which removes the tools and the `jobs` snapshot field at the source. Verified: 23/23 type-check, biome, api-validation, production build, and the full vitest suite (18361 passing; the one failure in executor/handlers/pi/cloud-review-tools.test.ts predates this branch). * fix(sidebar): derive the settings and switcher widths from SIDEBAR_WIDTH This branch moved `SIDEBAR_WIDTH.DEFAULT` from 248 to 238 but left two hardcoded `248px` chrome widths behind, so both sat 10px wider than the live sidebar: - the workspace-switcher menu, which is meant to line up with the sidebar column it drops out of - the standalone settings sidebar, whose own comment says to keep it in step with the in-workspace chrome Both now read `SIDEBAR_WIDTH.DEFAULT` directly rather than repeating the number, so the next change to the constant cannot leave them stale again. * fix(schedules): stop the API accepting actions it no longer handles Adversarial pass on the scheduled-task removal found a real regression in PUT /api/schedules/[id]. Removing the job-only `update` and `exclude_occurrence` handlers left them in `scheduleUpdateSchema`, so those bodies still parsed. The handler chain is `disable` first and then an unguarded fall-through to reactivate, so an `action: 'update'` request would have silently REACTIVATED the schedule instead of being rejected. Both actions are dropped from the discriminated union, so `parseRequest` now rejects them with a 400. Their bodies, response types and the orphaned `createScheduleContract` (its POST route is gone, and nothing imported it) go with them. * chore(landing): retire the scheduled-tasks marketing surface The feature is gone from the product, so the marketing pages stop selling it. - deletes the `/scheduled-tasks` landing page and its calendar-loop hero, and the `LandingPreviewScheduledTasks` panel - drops the view from the landing preview: the `SidebarView` member, the nav entry and its now-unused Calendar icon, the callout label, both render branches, and the staged chat copy in `workflow-data` - removes the navbar and footer links and the sitemap entry - removes the route from `LANDING_ROUTES`, the COEP exemption list that must list every `app/(landing)` route `/scheduled-tasks` is indexed, so it 301s to `/workflows` rather than starting to 404 — that is the surface that still carries scheduled execution via the workflow Schedule trigger. Left alone deliberately: `demo-scheduler` is the Cal.com booking embed for the demo page, unrelated to this feature, and the scheduling library article is a generic SEO piece that never pitched it. * perf(chat): stop the resource picker fetching schedules it no longer shows Dropping the `scheduledtask` group from the add-resource dropdown left `useWorkspaceSchedules` behind, so the picker still issued a workspace schedules request whose result never reached a group. Worse than a wasted request: `schedulesPending` was still in the hydration gate, so the whole picker waited on that response before it could settle, and `schedules` was still a `useMemo` dependency, re-running the group build when it resolved. The hook and its route stay — `/api/schedules?workspaceId=` still correctly lists workflow schedules, unlike `createScheduleContract`, whose route this branch removed. * chore(scheduled-tasks): drop the leftovers the removal stranded An independent audit of the branch turned up dead code and stale docs that the compiler cannot see — nothing behavioural, but all of it rots silently. - README still sold the feature: the "Scheduled tasks" tile, the prose listing it as a workspace surface, and the now-unreferenced screenshot. The landing surface went in c61770a8c; this tile was missed. - `resource-content.tsx`: `SCHEDULE_STATUS_LABEL`, `formatScheduleInstant` and `ScheduledTaskField` were orphaned when the schedule render branch went. - `computeNextRunAt`: zero callers, including tests — its only consumer was the removed agent-job runner. - `applyScheduleUpdate`'s `allowCompleted` option: no call site passes it, and its comment described self-completion, which no longer exists. The guard stays (legacy `sourceType='job'` rows still carry `status='completed'` until the DB follow-up); it is simply unconditional now. - Three TSDoc blocks still described a create-job route and "opening a scheduled-task artifact". Type-check re-run with --force, since a cached turbo replay is not a check. --------- Co-authored-by: Waleed Latif --- .agents/skills/add-enrichment/SKILL.md | 4 +- .agents/skills/emcn-design-review/SKILL.md | 6 +- .claude/commands/add-enrichment.md | 4 +- .claude/commands/emcn-design-review.md | 6 +- .claude/rules/emcn-components.md | 4 +- .claude/rules/sim-styling.md | 20 +- .claude/rules/sim-url-state.md | 17 +- .cursor/commands/add-enrichment.md | 4 +- .cursor/commands/emcn-design-review.md | 6 +- .cursor/rules/emcn-components.mdc | 4 +- .cursor/rules/sim-styling.mdc | 2 +- README.md | 7 +- apps/docs/components/ai/ask-ai-panel.tsx | 4 +- apps/docs/components/ai/ask-ai.tsx | 6 +- .../components/docs-layout/page-footer.tsx | 2 +- .../docs-layout/page-navigation-arrows.tsx | 2 +- apps/docs/components/page-actions.tsx | 4 +- apps/docs/components/ui/code-block.tsx | 6 +- apps/docs/components/ui/faq.tsx | 2 +- apps/docs/components/ui/heading.tsx | 2 +- apps/docs/components/ui/response-section.tsx | 2 +- apps/docs/components/ui/search-trigger.tsx | 2 +- .../workflow-preview/block-icons.tsx | 8 +- .../workflow-preview/block-inspector.tsx | 2 +- .../workflow-preview/output-bundle.tsx | 2 +- .../workflow-preview/workflow-preview.tsx | 4 +- apps/docs/content/docs/de/copilot/index.mdx | 6 +- apps/docs/content/docs/es/copilot/index.mdx | 6 +- apps/docs/content/docs/fr/copilot/index.mdx | 6 +- apps/docs/content/docs/ja/copilot/index.mdx | 6 +- apps/docs/content/docs/zh/copilot/index.mdx | 6 +- apps/docs/package.json | 1 - .../(auth)/components/auth-submit-button.tsx | 1 - .../app/(auth)/components/password-input.tsx | 2 +- .../components/social-login-buttons.tsx | 3 - .../(auth)/components/sso-login-button.tsx | 1 - .../auth/password/password-auth.tsx | 8 +- .../chat/components/input/input.tsx | 6 +- .../message-container/message-container.tsx | 2 +- .../message/components/file-download.tsx | 2 +- .../message/components/markdown-renderer.tsx | 12 +- .../chat/components/message/message.tsx | 8 +- .../[executionId]/resume-page-client.tsx | 2 +- .../changelog-actions/changelog-actions.tsx | 2 +- .../changelog-timeline/changelog-timeline.tsx | 2 +- .../components/auth-modal/auth-modal.tsx | 2 +- .../build-chat-animation.tsx | 3 +- .../components/feature-card/feature-card.tsx | 2 +- .../(landing)/components/footer/footer.tsx | 1 - .../hero-chat-loop/hero-chat-loop.tsx | 10 +- .../components/hero-visual/stage-home.tsx | 4 +- .../hero/components/hero-visual/stage-kb.tsx | 2 +- .../landing-preview-chat/chat-input.tsx | 2 +- .../landing-preview-home.tsx | 5 +- .../landing-preview-logs.tsx | 2 +- .../landing-preview-resource.tsx | 2 +- .../landing-preview-scheduled-tasks.tsx | 88 --- .../landing-preview-sidebar.tsx | 41 +- .../landing-preview-stage-header.tsx | 5 +- .../preview-block-node.tsx | 2 +- .../landing-preview-workflow/workflow-data.ts | 4 - .../landing-preview/landing-preview.tsx | 15 +- .../components/mobile-nav/mobile-nav.tsx | 4 +- .../components/nav-menu-chip/constants.ts | 5 - .../nav-menu-chip/nav-menu-chip.tsx | 7 +- .../(landing)/components/navbar/navbar.tsx | 10 +- .../components/share-button/share-button.tsx | 5 +- .../shared/editor-loop/editor-loop.tsx | 2 +- .../solutions-pill-cta/solutions-pill-cta.tsx | 2 +- .../components/contact-form/contact-form.tsx | 2 - .../demo/components/demo-form/demo-form.tsx | 3 - .../enterprise-home-stage.tsx | 4 +- .../enterprise-sidebar.tsx | 4 +- .../files/components/files-hero-loop.tsx | 2 +- .../components/knowledge-hero-loop.tsx | 2 +- .../logs/components/logs-hero-loop.tsx | 2 +- .../components/pricing-card/pricing-card.tsx | 8 +- .../scheduled-tasks-calendar-loop/index.ts | 1 - .../scheduled-tasks-calendar-loop.module.css | 28 - .../scheduled-tasks-calendar-loop.tsx | 300 --------- .../app/(landing)/scheduled-tasks/page.tsx | 20 - .../scheduled-tasks/scheduled-tasks.tsx | 199 ------ apps/sim/app/_styles/globals.css | 74 ++- apps/sim/app/api/emails/preview/route.ts | 4 +- .../app/api/mothership/execute/route.test.ts | 32 +- apps/sim/app/api/mothership/execute/route.ts | 14 +- apps/sim/app/api/schedules/[id]/route.ts | 138 +--- .../app/api/schedules/execute/route.test.ts | 38 +- apps/sim/app/api/schedules/execute/route.ts | 112 +--- apps/sim/app/api/schedules/route.ts | 181 +----- .../workflows/[id]/references/route.test.ts | 88 --- .../api/workflows/[id]/references/route.ts | 37 -- apps/sim/app/f/[token]/public-file-auth.tsx | 4 +- .../sim/app/invite/components/status-card.tsx | 9 +- apps/sim/app/layout.tsx | 10 +- apps/sim/app/playground/page.tsx | 4 +- apps/sim/app/sitemap.ts | 3 - apps/sim/app/unsubscribe/unsubscribe.tsx | 27 +- .../generate-prompt-control.tsx | 5 +- .../[workspaceId]/components/error/error.tsx | 6 +- .../[workspaceId]/components/index.ts | 1 + .../integration-tabs-header/index.ts | 1 + .../integration-tabs-header.tsx | 15 +- .../components/invite-modal/invite-modal.tsx | 1 - .../resource-header/resource-header.tsx | 16 +- .../resource-options/resource-options.tsx | 6 +- .../components/resource/resource.tsx | 5 +- .../components/file-viewer/document-table.css | 2 +- .../file-viewer/document-table.test.ts | 6 +- .../file-viewer/editor-context-menu.tsx | 3 +- .../components/file-viewer/file-viewer.tsx | 4 +- .../file-viewer/preview-toolbar.tsx | 6 +- .../rich-markdown-editor/code-block.tsx | 8 +- .../mention/mention-list.test.tsx | 2 +- .../rich-markdown-editor/mention/types.ts | 2 +- .../menus/bubble-menu.tsx | 14 +- .../menus/link-hover-card.tsx | 4 +- .../rich-markdown-editor/menus/table-menu.tsx | 16 +- .../menus/toolbar-button.tsx | 2 +- .../rich-markdown-editor.css | 10 +- .../slash-command/commands.ts | 14 +- .../components/file-viewer/xlsx-preview.tsx | 2 +- .../chat-context-kind-registry.tsx | 7 +- .../agent-group/tool-permission-card.tsx | 10 +- .../components/chat-content/chat-content.tsx | 28 +- .../components/options/options.tsx | 2 +- .../components/question/question.tsx | 2 +- .../components/special-tags/special-tags.tsx | 10 +- .../home/components/message-content/utils.ts | 4 +- .../mothership-chat-skeleton.tsx | 2 +- .../mothership-chat/mothership-chat.tsx | 4 +- .../add-resource-dropdown.tsx | 16 - .../browser-session/browser-session.tsx | 5 +- .../resource-content/resource-content.tsx | 146 ----- .../resource-registry/resource-registry.tsx | 16 +- .../queued-messages/queued-messages.tsx | 4 +- .../suggested-actions/suggested-actions.tsx | 93 ++- .../attached-files-list.tsx | 2 +- .../components/chip-clipboard-codec.ts | 3 - .../user-input/components/constants.ts | 7 +- .../home/components/user-input/user-input.tsx | 6 +- .../user-message-content.tsx | 4 +- .../app/workspace/[workspaceId]/home/home.tsx | 4 +- .../home/hooks/stream/stream-helpers.ts | 5 - .../[workspaceId]/home/hooks/use-chat.ts | 2 - .../app/workspace/[workspaceId]/home/types.ts | 1 - .../integration-block-detail-fallback.tsx | 4 +- .../[block]/integration-block-detail.tsx | 2 +- .../[block]/integration-skills-section.tsx | 6 +- .../connect-slack-bot-modal.tsx | 4 +- .../integration-tabs-header/index.ts | 1 - .../showcase-with-explore.tsx | 2 +- .../integrations/integrations.tsx | 4 +- .../[workspaceId]/integrations/page.tsx | 2 +- .../knowledge/[id]/[documentId]/document.tsx | 5 +- .../knowledge/[id]/[documentId]/loading.tsx | 3 +- .../[workspaceId]/knowledge/[id]/base.tsx | 15 +- .../[id]/components/action-bar/action-bar.tsx | 8 +- .../add-connector-modal.tsx | 2 +- .../add-documents-modal.tsx | 4 +- .../connector-config-fields.tsx | 4 +- .../connectors-section/connectors-section.tsx | 24 +- .../edit-connector-modal.tsx | 6 +- .../create-base-modal/create-base-modal.tsx | 2 +- .../[workspaceId]/knowledge/knowledge.tsx | 3 - .../execution-snapshot/execution-snapshot.tsx | 6 +- .../components/trace-view/trace-view.tsx | 4 +- .../components/log-details/log-details.tsx | 17 +- .../app/workspace/[workspaceId]/not-found.tsx | 5 +- .../app/workspace/[workspaceId]/prefetch.ts | 31 +- .../calendar-toolbar/calendar-toolbar.tsx | 6 +- .../schedule-calendar/schedule-calendar.tsx | 13 +- .../schedule-list-context-menu/index.ts | 1 - .../schedule-list-context-menu.tsx | 53 -- .../components/task-context-menu/index.ts | 1 - .../task-context-menu/task-context-menu.tsx | 114 ---- .../components/task-delete-dialog/index.ts | 1 - .../task-delete-dialog/task-delete-dialog.tsx | 98 --- .../components/task-details-modal/index.ts | 1 - .../task-details-modal/task-details-modal.tsx | 106 --- .../components/task-modal/index.ts | 1 - .../task-modal/recurrence-section.tsx | 301 --------- .../task-modal/secret-access-section.tsx | 67 -- .../components/task-modal/task-modal.tsx | 358 ---------- .../[workspaceId]/scheduled-tasks/error.tsx | 15 - .../scheduled-tasks/hooks/use-calendar.ts | 180 ------ .../hooks/use-scheduled-tasks.ts | 292 --------- .../[workspaceId]/scheduled-tasks/loading.tsx | 18 - .../[workspaceId]/scheduled-tasks/page.tsx | 28 - .../scheduled-tasks/scheduled-tasks.tsx | 237 ------- .../scheduled-tasks/search-params.ts | 78 --- .../components/activity-log/activity-log.tsx | 2 +- .../settings/components/api-keys/api-keys.tsx | 4 +- .../settings/components/billing/billing.tsx | 6 +- .../password-detail/password-detail.tsx | 4 +- .../components/byok/byok-key-manager.tsx | 5 +- .../settings/components/copilot/copilot.tsx | 2 +- .../components/custom-tools/custom-tools.tsx | 3 +- .../settings/components/general/general.tsx | 6 +- .../inbox-settings-tab/inbox-settings-tab.tsx | 5 +- .../inbox-task-list/inbox-task-list.tsx | 3 +- .../mcp-server-form-modal.tsx | 2 +- .../settings/components/mcp/mcp.tsx | 3 +- .../row-actions-menu/row-actions-menu.tsx | 6 +- .../settings-resource-row.tsx | 3 +- .../team-seats-overview.tsx | 2 +- .../components/teammates/teammates.tsx | 6 +- .../workflow-mcp-servers.tsx | 5 +- .../workspace/[workspaceId]/skills/page.tsx | 2 +- .../workspace/[workspaceId]/skills/skills.tsx | 5 +- .../new-column-dropdown.tsx | 2 +- .../components/row-modal/row-modal.tsx | 2 - .../components/table-grid/table-find.tsx | 3 +- .../workflow-sidebar/workflow-sidebar.tsx | 9 +- .../comparison-table/comparison-table.tsx | 2 +- .../components/plan-card/plan-card.tsx | 1 - .../components/action-bar/action-bar.tsx | 8 +- .../components/chat/chat.test.tsx | 7 +- .../w/[workflowId]/components/chat/chat.tsx | 12 +- .../components/chat-message/chat-message.tsx | 2 +- .../output-select/output-select.tsx | 6 +- .../components/command-list/command-list.tsx | 4 +- .../w/[workflowId]/components/error/index.tsx | 2 +- .../deploy-modal/components/api/api.tsx | 2 +- .../deploy-modal/components/chat/chat.tsx | 4 +- .../general/components/versions.tsx | 4 +- .../components/field-item/field-item.tsx | 2 +- .../connection-blocks/connection-blocks.tsx | 6 +- .../checkbox-list/checkbox-list.tsx | 4 +- .../sub-block/components/code/code.tsx | 4 +- .../condition-input/condition-input.tsx | 3 +- .../credential-selector.tsx | 12 +- .../document-tag-entry/document-tag-entry.tsx | 2 +- .../env-var-dropdown/env-var-dropdown.tsx | 2 +- .../components/eval-input/eval-input.tsx | 3 +- .../components/file-upload/file-upload.tsx | 2 +- .../components/filter-rule-row.tsx | 2 +- .../filter-builder/filter-builder.tsx | 2 +- .../grouped-checkbox-list.tsx | 4 +- .../knowledge-base-selector.tsx | 2 +- .../knowledge-tag-filters.tsx | 2 +- .../components/long-input/long-input.tsx | 4 +- .../messages-input/messages-input.tsx | 3 +- .../selector-combobox/selector-combobox.tsx | 2 +- .../components/short-input/short-input.tsx | 4 +- .../components/skill-input/skill-input.tsx | 4 +- .../slack-setup-wizard/slack-setup-wizard.tsx | 4 +- .../sort-builder/components/sort-rule-row.tsx | 2 +- .../components/sort-builder/sort-builder.tsx | 2 +- .../components/starter/input-format.tsx | 3 +- .../components/tag-dropdown/tag-dropdown.tsx | 6 +- .../components/tools/credential-selector.tsx | 4 +- .../tool-input/components/tools/parameter.tsx | 2 +- .../components/tool-input/tool-input.tsx | 16 +- .../variables-input/variables-input.tsx | 3 +- .../editor/components/sub-block/sub-block.tsx | 12 +- .../subflow-editor/subflow-editor.tsx | 2 +- .../panel/components/editor/editor.tsx | 8 +- .../panel/components/toolbar/toolbar.tsx | 2 +- .../w/[workflowId]/components/panel/panel.tsx | 5 +- .../workflow-search-replace.tsx | 2 +- .../components/subflows/loop/loop-config.ts | 4 +- .../subflows/parallel/parallel-config.ts | 4 +- .../filter-popover/filter-popover.tsx | 6 +- .../components/output-panel/output-panel.tsx | 10 +- .../toggle-button/toggle-button.tsx | 2 +- .../components/terminal/terminal.tsx | 14 +- .../[workflowId]/components/terminal/utils.ts | 21 +- .../components/variables/variables.tsx | 3 +- .../wand-prompt-bar/wand-prompt-bar.tsx | 6 +- .../workflow-controls/workflow-controls.tsx | 8 +- .../preview-editor/preview-editor.tsx | 27 +- .../components/subflow/subflow.tsx | 4 +- .../w/components/preview/preview.tsx | 2 +- .../collapsed-sidebar-menu.tsx | 9 +- .../components/file-list/file-list.tsx | 8 +- .../components/help-modal/help-modal.tsx | 6 +- .../w/components/sidebar/components/index.ts | 2 + .../components/search-modal/search-modal.tsx | 19 +- .../settings-sidebar/settings-sidebar.tsx | 27 +- .../components/sidebar-footer/index.ts | 1 + .../sidebar-footer/sidebar-footer.tsx | 259 ++++++++ .../components/sidebar-section/index.ts | 1 + .../sidebar-section/sidebar-section.tsx | 119 ++++ .../components/context-menu/context-menu.tsx | 22 +- .../components/folder-item/folder-item.tsx | 3 +- .../reference-tree/reference-tree.tsx | 68 -- .../references-modal/references-modal.tsx | 74 --- .../workflow-item/workflow-item.tsx | 20 +- .../view-invitations-menu-item.tsx | 2 +- .../workspace-header/workspace-header.tsx | 106 ++- .../w/components/sidebar/constants.ts | 30 +- .../w/components/sidebar/sidebar.tsx | 274 ++++---- .../sim/background/schedule-execution.test.ts | 76 +-- apps/sim/background/schedule-execution.ts | 610 +----------------- apps/sim/blocks/blocks/chat_trigger.ts | 8 +- apps/sim/blocks/blocks/clickhouse.ts | 13 +- apps/sim/blocks/blocks/generic_webhook.ts | 8 +- apps/sim/blocks/blocks/input_trigger.ts | 8 +- apps/sim/blocks/blocks/manual_trigger.ts | 8 +- apps/sim/blocks/blocks/schedule.ts | 8 +- apps/sim/blocks/blocks/sftp.ts | 12 +- apps/sim/blocks/blocks/wait.ts | 8 +- apps/sim/blocks/custom/custom-block-icon.tsx | 5 +- apps/sim/components.json | 3 +- .../agent-stream/agent-stream-chrome.tsx | 12 +- .../notifications/schedule-disabled-email.tsx | 14 +- .../emails/render-notifications.test.ts | 7 +- apps/sim/components/emails/render.ts | 1 - .../sim/components/permissions/member-row.tsx | 2 +- .../components/pii/custom-patterns-editor.tsx | 4 +- apps/sim/components/settings/navigation.ts | 12 +- .../settings/standalone-settings-shell.tsx | 4 +- .../ui/generated-password-input.tsx | 2 +- apps/sim/components/ui/select.tsx | 2 +- .../content/blog/enterprise/components.tsx | 4 +- .../components/access-control.tsx | 2 +- .../components/group-detail.tsx | 24 +- .../components/custom-block-detail.tsx | 2 +- .../components/custom-blocks.tsx | 2 +- .../components/data-retention-settings.tsx | 3 +- apps/sim/ee/sso/components/sso-settings.tsx | 2 +- .../components/whitelabeling-settings.tsx | 2 +- .../fork-workspace-modal.tsx | 4 +- .../ee/workspace-forking/components/forks.tsx | 7 +- .../company-domain/company-domain.ts | 2 +- .../enrichments/company-info/company-info.ts | 4 +- .../enrichments/phone-number/phone-number.ts | 2 +- apps/sim/hooks/queries/schedules.ts | 237 +------ apps/sim/hooks/queries/workflow-references.ts | 39 -- apps/sim/hooks/use-workspace-invite-policy.ts | 48 ++ apps/sim/lib/api/contracts/schedules.ts | 128 +--- .../lib/api/contracts/workflow-references.ts | 54 -- apps/sim/lib/billing/workspace-permissions.ts | 17 + apps/sim/lib/copilot/chat/post.ts | 3 - apps/sim/lib/copilot/chat/process-contents.ts | 50 +- .../sim/lib/copilot/chat/workspace-context.ts | 78 +-- .../lib/copilot/generated/tool-catalog-v1.ts | 190 +----- .../lib/copilot/generated/tool-schemas-v1.ts | 156 +---- .../lib/copilot/generated/vfs-snapshot-v1.ts | 14 - .../lib/copilot/resources/extraction.test.ts | 60 -- apps/sim/lib/copilot/resources/extraction.ts | 22 - apps/sim/lib/copilot/resources/types.ts | 4 - .../tool-executor/register-handlers.ts | 12 - .../lib/copilot/tools/client/store-utils.ts | 2 +- apps/sim/lib/copilot/tools/handlers/jobs.ts | 445 ------------- .../lib/copilot/tools/handlers/resources.ts | 24 - .../tools/server/jobs/get-job-logs.test.ts | 131 ---- .../copilot/tools/server/jobs/get-job-logs.ts | 242 ------- apps/sim/lib/copilot/tools/server/router.ts | 2 - .../lib/copilot/tools/tool-display.test.ts | 12 +- apps/sim/lib/copilot/tools/tool-display.ts | 16 - apps/sim/lib/copilot/vfs/serializers.ts | 43 -- apps/sim/lib/copilot/vfs/workspace-vfs.ts | 119 +--- apps/sim/lib/mothership/inbox/response.ts | 4 +- apps/sim/lib/posthog/events.ts | 13 - .../workflows/references/operations.test.ts | 309 --------- .../lib/workflows/references/operations.ts | 327 ---------- .../schedules/disable-notifications.test.ts | 27 - .../schedules/disable-notifications.ts | 20 +- .../workflows/schedules/orchestration.test.ts | 133 ---- .../lib/workflows/schedules/orchestration.ts | 600 ----------------- apps/sim/lib/workflows/schedules/utils.ts | 39 -- apps/sim/lib/workspaces/lifecycle.test.ts | 2 +- apps/sim/lib/workspaces/lifecycle.ts | 18 - apps/sim/next.config.ts | 10 +- apps/sim/package.json | 1 - .../public/static/readme-scheduled-tasks.png | Bin 203341 -> 0 bytes apps/sim/stores/constants.ts | 4 +- apps/sim/stores/modals/search/store.ts | 6 +- apps/sim/stores/panel/types.ts | 1 - apps/sim/tailwind.config.ts | 33 +- bun.lock | 6 - packages/emcn/package.json | 2 - packages/emcn/src/AGENTS.md | 2 +- .../emcn/src/components/button/button.tsx | 29 + .../components/calendar/calendar-day-cell.tsx | 1 - .../emcn/src/components/calendar/calendar.tsx | 18 +- .../emcn/src/components/checkbox/checkbox.tsx | 2 +- .../chip-copy-input/chip-copy-input.tsx | 2 +- .../chip-date-picker/chip-date-picker.tsx | 7 +- .../chip-dropdown/chip-dropdown.tsx | 4 +- .../src/components/chip-input/chip-input.tsx | 2 +- .../src/components/chip-modal/chip-modal.tsx | 19 +- .../components/chip-select/chip-select.tsx | 2 +- .../components/chip-switch/chip-switch.tsx | 1 - .../emcn/src/components/chip-tag/chip-tag.tsx | 6 +- .../chip-time-picker/chip-time-picker.tsx | 8 +- packages/emcn/src/components/chip/chip.tsx | 19 +- packages/emcn/src/components/code/code.tsx | 2 +- .../emcn/src/components/combobox/combobox.tsx | 3 +- .../dropdown-menu/dropdown-menu.tsx | 48 +- .../src/components/info-card/info-card.tsx | 4 +- .../src/components/input-otp/input-otp.tsx | 2 +- packages/emcn/src/components/modal/modal.tsx | 5 +- .../emcn/src/components/popover/popover.tsx | 4 +- .../progress-item/progress-item.tsx | 5 +- .../secret-reveal/secret-reveal.tsx | 6 +- .../src/components/tag-input/tag-input.tsx | 6 +- .../components/time-picker/time-picker.tsx | 2 +- packages/emcn/src/components/toast/toast.tsx | 3 +- .../{user-plus.tsx => arrow-left-right.tsx} | 12 +- packages/emcn/src/icons/arrow-up-left.tsx | 26 + packages/emcn/src/icons/ban.tsx | 26 + packages/emcn/src/icons/bold.tsx | 26 + packages/emcn/src/icons/box.tsx | 27 + packages/emcn/src/icons/brain-circuit.tsx | 28 + packages/emcn/src/icons/brain.tsx | 27 + packages/emcn/src/icons/bubble-chat-delay.tsx | 4 +- packages/emcn/src/icons/building.tsx | 27 + packages/emcn/src/icons/camera.tsx | 27 + packages/emcn/src/icons/chevron-up.tsx | 25 + packages/emcn/src/icons/chevrons-down-up.tsx | 26 + packages/emcn/src/icons/chevrons-up-down.tsx | 26 + packages/emcn/src/icons/circle-pause.tsx | 27 + packages/emcn/src/icons/circle-x.tsx | 27 + packages/emcn/src/icons/circle.tsx | 25 + packages/emcn/src/icons/clipboard.tsx | 2 +- packages/emcn/src/icons/code.tsx | 26 + packages/emcn/src/icons/compass.tsx | 27 + packages/emcn/src/icons/credit.tsx | 6 +- packages/emcn/src/icons/database.tsx | 8 +- packages/emcn/src/icons/eye-off.tsx | 2 +- packages/emcn/src/icons/file-text.tsx | 27 + packages/emcn/src/icons/files.tsx | 8 +- packages/emcn/src/icons/folder-input.tsx | 2 +- packages/emcn/src/icons/folder-open.tsx | 26 + packages/emcn/src/icons/form-input.tsx | 27 + packages/emcn/src/icons/globe.tsx | 27 + packages/emcn/src/icons/heading1.tsx | 28 + packages/emcn/src/icons/heading2.tsx | 28 + packages/emcn/src/icons/heading3.tsx | 28 + packages/emcn/src/icons/help-circle.tsx | 8 +- packages/emcn/src/icons/highlighter.tsx | 27 + packages/emcn/src/icons/index.ts | 55 +- packages/emcn/src/icons/infinity.tsx | 25 + packages/emcn/src/icons/italic.tsx | 27 + packages/emcn/src/icons/library.tsx | 12 +- packages/emcn/src/icons/list-checks.tsx | 26 + packages/emcn/src/icons/list-ordered.tsx | 28 + packages/emcn/src/icons/list.tsx | 28 + packages/emcn/src/icons/menu.tsx | 27 + packages/emcn/src/icons/mic-off.tsx | 28 + packages/emcn/src/icons/minus.tsx | 25 + packages/emcn/src/icons/moon.tsx | 25 + packages/emcn/src/icons/more-vertical.tsx | 27 + packages/emcn/src/icons/music.tsx | 27 + packages/emcn/src/icons/panel-right.tsx | 26 + packages/emcn/src/icons/phone.tsx | 25 + packages/emcn/src/icons/pilcrow.tsx | 27 + packages/emcn/src/icons/pin-off.tsx | 2 +- packages/emcn/src/icons/pin.tsx | 2 +- packages/emcn/src/icons/repeat.tsx | 28 + packages/emcn/src/icons/rss.tsx | 27 + packages/emcn/src/icons/scissors.tsx | 28 + packages/emcn/src/icons/select-all.tsx | 2 +- packages/emcn/src/icons/send-to-back.tsx | 26 + packages/emcn/src/icons/send.tsx | 6 +- packages/emcn/src/icons/settings.tsx | 8 +- packages/emcn/src/icons/share.tsx | 28 + packages/emcn/src/icons/sparkles.tsx | 27 + packages/emcn/src/icons/square.tsx | 6 +- packages/emcn/src/icons/strikethrough.tsx | 26 + packages/emcn/src/icons/sun.tsx | 27 + packages/emcn/src/icons/table.tsx | 4 +- packages/emcn/src/icons/task.tsx | 9 +- packages/emcn/src/icons/text-quote.tsx | 26 + packages/emcn/src/icons/trash-outline.tsx | 29 - packages/emcn/src/icons/trash.tsx | 19 +- packages/emcn/src/icons/trash2.tsx | 24 - packages/emcn/src/icons/unlink.tsx | 27 + packages/emcn/src/icons/users.tsx | 10 +- packages/emcn/src/icons/wand.tsx | 27 + packages/emcn/src/icons/webhook.tsx | 28 + packages/emcn/src/icons/workflow.tsx | 7 +- packages/emcn/src/icons/zap.tsx | 25 + packages/emcn/src/index.ts | 11 + packages/workflow-renderer/package.json | 2 - .../src/edge/workflow-edge-view.tsx | 2 +- .../src/note/note-block-view.tsx | 4 +- .../src/subflow/subflow-node-view.tsx | 4 +- scripts/README.md | 2 +- 482 files changed, 3184 insertions(+), 9155 deletions(-) delete mode 100644 apps/sim/app/(landing)/components/landing-preview/components/landing-preview-scheduled-tasks/landing-preview-scheduled-tasks.tsx delete mode 100644 apps/sim/app/(landing)/scheduled-tasks/components/scheduled-tasks-calendar-loop/index.ts delete mode 100644 apps/sim/app/(landing)/scheduled-tasks/components/scheduled-tasks-calendar-loop/scheduled-tasks-calendar-loop.module.css delete mode 100644 apps/sim/app/(landing)/scheduled-tasks/components/scheduled-tasks-calendar-loop/scheduled-tasks-calendar-loop.tsx delete mode 100644 apps/sim/app/(landing)/scheduled-tasks/page.tsx delete mode 100644 apps/sim/app/(landing)/scheduled-tasks/scheduled-tasks.tsx delete mode 100644 apps/sim/app/api/workflows/[id]/references/route.test.ts delete mode 100644 apps/sim/app/api/workflows/[id]/references/route.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/components/integration-tabs-header/index.ts rename apps/sim/app/workspace/[workspaceId]/{integrations => }/components/integration-tabs-header/integration-tabs-header.tsx (56%) delete mode 100644 apps/sim/app/workspace/[workspaceId]/integrations/components/integration-tabs-header/index.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/schedule-list-context-menu/index.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/schedule-list-context-menu/schedule-list-context-menu.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/task-context-menu/index.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/task-context-menu/task-context-menu.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/task-delete-dialog/index.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/task-delete-dialog/task-delete-dialog.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/task-details-modal/index.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/task-details-modal/task-details-modal.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/task-modal/index.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/task-modal/recurrence-section.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/task-modal/secret-access-section.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/task-modal/task-modal.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/scheduled-tasks/error.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/scheduled-tasks/hooks/use-calendar.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/scheduled-tasks/hooks/use-scheduled-tasks.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/scheduled-tasks/loading.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/scheduled-tasks/page.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/scheduled-tasks/scheduled-tasks.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/scheduled-tasks/search-params.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/sidebar-footer/index.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/sidebar-footer/sidebar-footer.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/sidebar-section/index.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/sidebar-section/sidebar-section.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/references-modal/components/reference-tree/reference-tree.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/references-modal/references-modal.tsx delete mode 100644 apps/sim/hooks/queries/workflow-references.ts create mode 100644 apps/sim/hooks/use-workspace-invite-policy.ts delete mode 100644 apps/sim/lib/api/contracts/workflow-references.ts delete mode 100644 apps/sim/lib/copilot/tools/handlers/jobs.ts delete mode 100644 apps/sim/lib/copilot/tools/server/jobs/get-job-logs.test.ts delete mode 100644 apps/sim/lib/copilot/tools/server/jobs/get-job-logs.ts delete mode 100644 apps/sim/lib/workflows/references/operations.test.ts delete mode 100644 apps/sim/lib/workflows/references/operations.ts delete mode 100644 apps/sim/lib/workflows/schedules/orchestration.test.ts delete mode 100644 apps/sim/lib/workflows/schedules/orchestration.ts delete mode 100644 apps/sim/public/static/readme-scheduled-tasks.png rename packages/emcn/src/icons/{user-plus.tsx => arrow-left-right.tsx} (56%) create mode 100644 packages/emcn/src/icons/arrow-up-left.tsx create mode 100644 packages/emcn/src/icons/ban.tsx create mode 100644 packages/emcn/src/icons/bold.tsx create mode 100644 packages/emcn/src/icons/box.tsx create mode 100644 packages/emcn/src/icons/brain-circuit.tsx create mode 100644 packages/emcn/src/icons/brain.tsx create mode 100644 packages/emcn/src/icons/building.tsx create mode 100644 packages/emcn/src/icons/camera.tsx create mode 100644 packages/emcn/src/icons/chevron-up.tsx create mode 100644 packages/emcn/src/icons/chevrons-down-up.tsx create mode 100644 packages/emcn/src/icons/chevrons-up-down.tsx create mode 100644 packages/emcn/src/icons/circle-pause.tsx create mode 100644 packages/emcn/src/icons/circle-x.tsx create mode 100644 packages/emcn/src/icons/circle.tsx create mode 100644 packages/emcn/src/icons/code.tsx create mode 100644 packages/emcn/src/icons/compass.tsx create mode 100644 packages/emcn/src/icons/file-text.tsx create mode 100644 packages/emcn/src/icons/folder-open.tsx create mode 100644 packages/emcn/src/icons/form-input.tsx create mode 100644 packages/emcn/src/icons/globe.tsx create mode 100644 packages/emcn/src/icons/heading1.tsx create mode 100644 packages/emcn/src/icons/heading2.tsx create mode 100644 packages/emcn/src/icons/heading3.tsx create mode 100644 packages/emcn/src/icons/highlighter.tsx create mode 100644 packages/emcn/src/icons/infinity.tsx create mode 100644 packages/emcn/src/icons/italic.tsx create mode 100644 packages/emcn/src/icons/list-checks.tsx create mode 100644 packages/emcn/src/icons/list-ordered.tsx create mode 100644 packages/emcn/src/icons/list.tsx create mode 100644 packages/emcn/src/icons/menu.tsx create mode 100644 packages/emcn/src/icons/mic-off.tsx create mode 100644 packages/emcn/src/icons/minus.tsx create mode 100644 packages/emcn/src/icons/moon.tsx create mode 100644 packages/emcn/src/icons/more-vertical.tsx create mode 100644 packages/emcn/src/icons/music.tsx create mode 100644 packages/emcn/src/icons/panel-right.tsx create mode 100644 packages/emcn/src/icons/phone.tsx create mode 100644 packages/emcn/src/icons/pilcrow.tsx create mode 100644 packages/emcn/src/icons/repeat.tsx create mode 100644 packages/emcn/src/icons/rss.tsx create mode 100644 packages/emcn/src/icons/scissors.tsx create mode 100644 packages/emcn/src/icons/send-to-back.tsx create mode 100644 packages/emcn/src/icons/share.tsx create mode 100644 packages/emcn/src/icons/sparkles.tsx create mode 100644 packages/emcn/src/icons/strikethrough.tsx create mode 100644 packages/emcn/src/icons/sun.tsx create mode 100644 packages/emcn/src/icons/text-quote.tsx delete mode 100644 packages/emcn/src/icons/trash-outline.tsx delete mode 100644 packages/emcn/src/icons/trash2.tsx create mode 100644 packages/emcn/src/icons/unlink.tsx create mode 100644 packages/emcn/src/icons/wand.tsx create mode 100644 packages/emcn/src/icons/webhook.tsx create mode 100644 packages/emcn/src/icons/zap.tsx 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 57eaa363874c09b773ceab0a1a80af1badedbeef..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 203341 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

tyPG(8HETB5Q5<2j)kEG#EV1k} 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Ξ zY>WX~gdcM;IeUL}BxBiJou@-|sJa*C+(St6@)N)$39dsIHSTiP|3}hU$2Il7eSD+4 zJBBpUDGehh9ZE_{gLL;OX^?IhrIa9{bV&*V10@Bd8C|2pXW!qmKla*x=j`0qeV;4e zALW+`{kBUE=uwcEg)eupU1*3m4DjhJb~ev89xp=py54X4_c(A?90VMJDAh@R(2O&W zT2wH3??-0kid7W0H8*Mz&O$?%wcMx|2Lr-S0DTY$mdc(zSAc8p%^~66zPNwX<^K7D z3t~c7I{Kni%S=%^q6GRqSco^s_%6<=TysOLzZ9{k+AIS)a$VN_S*c$9>UgBn@Ih#> zfea6pQxKTn34oNdD9CI-Zqv)Zl@Y~XRS3Q2H8_`mslt`#kIk2dr_;czJ(U|2?C_-#?2?MN->dz z)!eW}GfF>^1(Ea*ecypDXVV3rl+N=Nz}|>`u7{Z3ds>f^RUGr^Zd^imlw(wOd8_Kb z%fx{*2Ssc+0Ii#<>|{-{tlTs3B3ZiC0M8SgG3fa?msZKd96#VRg$btK2QboEr=P-4 zo9}^ef5l)3>W%eBsg`XoQ5@2kr%WKj;Q2g4q)K1rw=5lI!$tN50bHMf5QNg;O4=?_ zU(U2C_n=n`QBRkX&b%R1BO)IDx3x2>aC>)T3+`o|$}@_9`kA6Oc%zykzo$n;lxIE< zbZF@nBb}sFkilMFsafYM4gxiJpA-`>dRL;ad;fON94>H4DnhSxz;i#lv+!sD z4>2S8>XR@yN+l^li$LM&&lRT>P2aGEscDE+c?{7|4 z!mc^&$a+Miq$FsF8Iw-VI6b8JJn5%Mdx7JFr>Cc`tgt++(8nFM7iz{<-Y8fIKHn_L z&$lPfEitLT{;a$)$!8W>A}J&+6<0#i)gSP{As8Td2FfjUAHuete-`#43Tepv3xyaT zQ1blpeeld%vr8nsO)gL`UE zCLJ@q=URoU8U|7n#H+xeRcpabC-PWR3fHQn5mv`aPUcJ>Ll?H{es5*`DrvRPed<%j z=Px6g-VP}lix7i`X3UFTFZCw6BDSC(`Bs8CsT6b=A}KfBXn7*W*E8*ry^to&M)=7z<7Jt~hXp|o=nsPBhKUuiPJ`deXR@Ok&xY*Q~hxLqP87#rI< z@)Rpy+gM$EJCKu*x9)5;_#LMPMf6|0I+-8AZRYomssU8%aUBt%!mYkgd+knTxac?@ zl>dv-7O==0ilx|FsyvBTp2yOy{vEBMQ z;stD7@+6w1JRB^RA*1XmGqenF%&T7dkERT*uV^c%Vn3>@(dGLJcNfWJ;9A#qC zltL&!uGRn5#KE*9fKM`@`$UNQWu!Ci$*!ud^@z*VQQZ8>x?u&|h)r8p{;r`S( z*o||5&oA|O$2x!5M|XTFloY5jHjV>FF-TCMJnM9L^p| zt=CXys~0U3OAWNLL=_W`>!hMAE!4C4`hrrvx;vC{hiA6?wI%LpkG}&ddx(bE$l5r7N4TGYus}sf0>q-|u}D%Xs_aYv z^x+2vB@n^w8z?G2Mn_#Et4~wy%cfq*tAuDXI18JM+T@_3AGDzB;`!A8n^rO~k2R7$ zfo5WbF*wN@DaWDfGJDd}m}+3M1hJQ~FjV`SnZ3F6yD}Pu^tV_As}yk9iW@Yqr4VZx z?Y)g(uZU1?Ss7EnvD*;-%b-WHq5*Y7b$dYouHSgCFsJ#hBUE3C56cr#rH2Q`FPcU2 z3U80HmHv3WBlsNp&DdG!LSUP5e|2|7j!1Hk<}o(o4*7xiZbig_yXYtCtQQ0OGc9-D z6SgTFb`_|=-4BOMlo-_0h*UCOR54_}8!eg%&MNe%YaZNJBd?%jaXE6NG(O=Zlq$pM zlr^szOH*C5QWTk6h5nGQnj#K0_;O)Dts|4fbxd5;6p?GclVb7FyV(-+!&H;FfaexU zOIK(x2b8Qhcvln;ISJ zFE4nceXzBSAZjz9fp`SlPK8J_b?Xiu>&noy^18FnCE zv3TqSEz>FGG2Pn(x-P!$ee3~wvNsAnwf^ew77v;3GCPAizcRR^fBex1RFUk%U5O~dWK};( zIddor>UlWuMzyh;mSL{t_HOixSe6P4?>y30nhh4L&Ox>HLRfYy9DH8mTius-om2DrbcRWY9dXxwY_I^VmEG^rWCkRmat!l>9hZ-G@SZf5#I@G(YpT5 z&;NMNDZo2}IGAN<*4tep6P(8-`j>~|Z7(Ls2=t0qYtMrkc6xsmoMzf8QBDV|CCz>P zHS@k&!$AC5z+;)`_}7{%Q+szDsKq-dHr|}F2M7$p_pi)-bIZWFa9%L)34LX zM^Vj$z`g3sEs+k!*hA<~N7{4=uS|_j-5U1K7RuklTkM#^j!>8$U%$OaQ|Cl|UbT2w za8+yT+h&d9klCRhf1)0{J=N3t2v|fHOTKn!sN?EK1xC&0?CdW6y($H?=pjbYr5Iu3 zRMcBQ*9U<8HV%x*pK1#@0etilATz}jRe?98eck*jo8N4cisl4*c`|F%`=@q}ORI6? zNQ6%AsE5gwoO>-;PV_%Z zK+##{p~VICQ2t4DR5fC}VgFR~fJQ|LWqHT*uLck+ov)Jp_qqb^(}Au(T~L0Mp1)D5 zbHwqBpiKfH(OSgc^EQnhHIhUB9*C@UfzK+&^gGIZj#Ge_$rP^PTW>SVJCn>yFtp ztnzh3n7{`ifgF4gObUc~7a7D~ov|~Hj>7-B%AOSK?-u%up1`3Fe)BXvi>-Q|@8$N~ zKl|BJM`Kl^Q2e=LXc#Xsf&y%`!-?yS4`;*%c(8c_K6)BdiURH~=PhE>YE;o-mb*Pf zlTMVsO)0x%gJ&p?7cwv$i$)b{(=lDkv%`%ry?-mXb@#E#Z=a{Oxeo+Awh`{BPd-kQ1iQwU0DAG8GY6` zE91Cp**yXlkEv+PCrJh%rf?WFPGYE-VZoVMl97{+RAWm6zFvs z&Bs=LxTL{Dg0&cTTHu%2I#0tFz@6{Txo&bmL-!L_MO5$E`?8m{LlxIn#S%P*&Z7>~ zSp%=s)m`KeAS&foRMt8A7-|E}b0RT$V$g9eet$n7|0|zs4-#)7*0S6Ej^7OK-M{!} z3jQ0man8*~0USpi$aYv@ox#5d0GB+YM@ANb=1h=`9fcpfSeDpozk@R)D;v5?jU;Cb z2LCjV44$h8)RI3#*1M^1VX2qX=FbMbV5>pa_(@AJgfM()T><)a60voX$v_C|5f-!e z{lzK7DBtcs^AQ3Tm9YJk1ebq<|G898P1rHY<9<}4kg({t9=vdANbB7s>=vUF8CraF zaX&S!bnhyDn`J+geKW{X!j@!ZVa554{m@>)C*fwfGQTj3iC@nB`)b39Bk19DI|n$GFxF41U*anV&z;?J8+hgNYgzMkD%NZ<&2Ev2WCx9IP{jzH~$04=k$G0D#ePr1i|7>0Z?^mK*Df<)>t! zL@;2McB-h<+;~Ix&&_?KOw-D`HTfaewBM^Nb_=4D z%lSaXaeC9w`YP4K68Y2``&R3&#B```_^&sM#)SEwFjO;tD>){ACnE_KLdJ5wB1w+b zC0-=b3g0o5`x+B)5=#oFwA&W(;)wj;W<(uy?2#xECqX~n;2s{49+9sO>eD2DXZOOQ zALGm@^PI6W!#O+hN0c+40`KGS@rqN;zXF0)yq$H#@%~Ia1F4NHN^mmH-%x=`bV)+W zBQk9V_x6w_AN++qduc>BzMHk~-M?74y^%E^PdINuMl@E$RDLK62fEbw>z~4~%Ldoa zg@xz=lel0WdAgm$to=DaH~tfIx%AgBlo#?H{-ouE@w~<+@#H8yx$f>C%q?$+f^WqL z%BpSm4)(^>3x3~0R(=ao&2+6_>YfV*m9nN)L^AYkyvSw?=niZTBzjgkX`UrU4P@{FUnS-79L5$|6s(lK^5 zx0e2{GUdxOko{h*@GgvUaW*RJNSEhipZ&<>Z27Wg_5?Ykx-KRk&1fJ2Yu^1IY8}`z zgi;1^19o7tB5=+mXZov0GwP(Pf=J^!SMsk@@=_wEjBGWBU&qi-C|O zk%PdhRBRnM`AtN~ep9|A8CI4(=WxkzRVTw2(YS~)tQy!BFp**$>Hz<;S z-38(x2qdk>akvpX^%B=s7nb(P=@Ba>Q@<9LSLHkqq*oYV-1XMcfe?E`1)q23L_0p< zLw$&YvfSAFRh?s71505Xvcp>$!&}`n{5$RBn7atlNl0n zWx13@oAxFS670A|uGJIiOJ#OUk`BCPZqS^Mx|DD{RGc!`Ad>- znEw!d^oakua5s-XE_?ggxN3H?Y>)B}zt#=qCf0Gb^g6cpk^1IbkuQ>?b5*6HI$}Pu zGZGd`9ulAz`SkBE!7*LXMKE_Mg25B8ZrE&hON(>Fb$zud=AyC}ue4LiaYg5+>+QmB z2g8!%!;IV-v>(F48(yn_t{?weLU}nG`&{K}o;n_VKNbJGEjbA+G3xh6e%2__g7OC( z-R@pk9l{!2>G{3AC+w~k-m}j4q_~EfTK_3eJ$DYuP5m~&pd*Ky*yV65!?)hkHCfXT z3kv)E=H#pBiQTG^<;5U*z)pOFJSG!PiKV1%k(isg%i}%M-C4=aOl|7c*}>RdOY8Km z_dkuth3jyE5W_qEZ*D7i*W-UWeR+yD`x_koT+L!H-yzCV_qV%a+jmSg9;XkXerfv~ zp{v&!#xnq{$Fo(`u>4Kkx9~dlZeO4y1M4Z|l$SN3BA?)gzS8IN^_Qa0<_B-+w>&lA zuS?4&x!>V32cx~x5Hbp45sL8%QXk=uA7m~YT*w6=Z;Tn78uB^1-p!GjCgVOIA&T=R z2#s)T+0<7Q_U3wqrQ>th^ueJ^NOP(B*>VUig6Ixfwvcs*{)y26(@ZWLH0QlzL-R?D$o>74q`I3fO_rU;JWO~V) zc8sL3yS-1BUIu1!j&AksbNa6B#Wx$p4M(Ho(4MLLr@Nf*f<1*f1)o|&YQu=G8<)?{ za!!Q=!NlmnSY)yZy!p2x=srFagKsL}kvWl*haaA$iW)X06_5rrZN1uptGQu*So89n z9Mbr*IG~`8@z%bOy9-#DMaEZzn>@;9-XyEuZS*CHBq%V7Ov}&l8GN)qk9R~l@*~X* z4LZ_JU%Koy`UNczfx@{y9=>WgYIHpN=KAKFOs3eO^w)q1Dh98xuj??%o9VjXMW)m# z^jA?4zX`bhkhcw9oDz5#Sqy~8h6r5JHMw_}Sg($EHFRJ--JX#LY-|_0{<+C_Mf%-# zkCIt@v!I)IH)-FIZLoBi;JZ6(l|5sJOj2#$@iFaAH-Q5G)STEIvDe)Wk`qsxuOBR& z59VA_H>n6PwiO@|-zYT}aK&@3K|J%8?I9buA+`_m0nWq^7i-NjXM_K<%MH!*t0I`^ zyH_@|Q|nLJsbO{ONt_@SXfYiK)Z?=^LxRVc!;2OgAMJ#J7&n461gp5dnhN=IO@h?cg@Uc;@q2XaO>gw}gD^)eKi`-G&NcbI^_SZ8?)NXkjl{y;@# zd$$M`V#mgdaT}YEi1qu4@e&p2>h13X0r_*BhXL8|o7QwL2kEEHAvfi7(>#b-!?muu zt9I8vpbXBituxL&42$@V`2o~^erNGdufaCU=<8DGCHoV`x5I#2>qouQd$E$B&)bg; zf#IouKZycM^3j$jo8epO@@P*Xe!5n@Pd{y|pScie>Pi5T9e}X@P`Yl5OlS5M1g8omep||jyAlL-Cy@G} z+Ov!*9ytxP@ag1W`+`CWBt^Lu(N1OX6q>A$qngalKfsaw@2FF`&!js&c|S46`)i*= z8mv^Ly781<=R|NS%c-PBhB1n9tHW>dFc7;XA&-<7|J)ckaa-M?eH_e?1~-h1BG%pN z6-jy&GNn3)X^AEyI#~QC!}d}(zNUR3 z7Ag4!ux#W=vQjTp*nY+_UsG8JR6>%tqo!D?@XCx!Qg!(6Gc4Y((K$vhQW>*qz?2cu zY)=UZjFq$#G+NY=#S@SwZ;wjmqMN1#(@|ex=xQ6YoJ+Y$oThU}TE?vFF<5LQ zdzz&*d*-|Tnh`a`GsxX|`rBw z8l)GNGWD|h8edcs`m}@vM063N!=+!~faSk>0)j46xC;Q?#RNef0VFg?6|f`P{yHfD zK?-zS`^Cy?Jd6g}4k|ZM9_69y{w_SCe#P)+v3#2lW7pFfZd;K3r6M- z95MFoD_e9hz!NkCt*kn=t}=}$OYDuD<=$j-9!x6Bxv^W;6xNS2DVW%l$bWY<+Uk&-nik&^Nr7&`itqX za}V61ffQ~}(zqcfaSl$~QTTG6qVl4qu<8wOMwZR@PYo|k$=0Ol0V*Cn?`Esa=H5g! zHcrK16;X=c{Tv1&iX@450_EvoC854H0H0MXG=w=lI?Qg@o)zF@QEyYko}#cd1(=Fr zQ@)^4EJa@r#DE7z8@Ftql6(LSWkbt#eO$@R2`B25VFF5(8_5TEN@$LFF!Il)uTwlz zo@hRK8}W*aRLy#Q02K?XkJOzy_Y6oFRgui3;`8FC+Uwap47kB0{+3 z5(%KF85fQfg_VpxLNZA~L`Y0X_&l>G`iZEOyc( zHozn0F?krx^0dtry$@mb!>w+#`;k!`EflqyGGstcT`}{q{a83cVtbZ(N!37yr!9;( z>@xXmTnOse!Snn%l1q7FGsnivzG{S692oT9#Zh1KS^!r<7~F$_gN(3m!dcWCVQoGX z+4s}FYu$RT46_W<9Bh#6T#Y_KHC=0jSxR1^0NJF`9Z^)yJPQm#`JpsoeUVQ6dd0*Q zZc)=*_lTGDrM!Cx%JVqzR(MjG0#4c4Px=a}DD2=6O$`Q$pGXs2jU~GjO8%7lGwR2? zr-@@~n7k8jTR7C>tVzDqQa*XvJSFuE7#1mjfjStMZ8q=XrH&8(u?I6=UvDtI9Iq|( z#YEt&2{S5e&;alVxllB@aGYsYHqEHp6qy;@Q>$9hx#nkJ8>(W3V#p^6>n(4AP5d2xlc zacQUQur)haTHVP~!{Hu!#f^&+QoXW4M{+VO^tvryJ^}9(l2fq|*e{A7xu9XXZnyWC z2<$K6Hus#1-;Vlw2+&YB>TPQUeks}}3EraNShvaLAO;o-rDY}(;uglzek83FpzUaB zt1wA06pQ^?7GZ5OP9>4IfcRvOV7LujKu-O;fpg65gp)sK{#&NJ>S0lW2 zz)zmjLl?>#mSxYf={|^4+o&`jKu{lyzLB$r6n^>4-7GZfwHsiswLZiUxa8xxfURP% z)-9Rgj!Ih%wfZf$fZDR;NC7$V7^=MT7v^S*u4_)$yG8XmWd6_ z8lKWe6Gw1$O-ulg%ivBvG|>i>dG3?swrot&6+`rH+BBb15Zii1uC}+8?Mma6ZWnkx zdt3%~1Jeri1i$05zB6xf>rn0Ag!+w~RXl^d_lj>5MwTi^(t;Xq=-mgCo(~J1tCCyv z0xy#S2mx30@(=HX5uYsNzr4pH-0Q{E{?vR*E8YLJ1q?c14b;xLNHjdU?g08k;Kz#0 zHy#6yxO_T`QZ6wZD-}+Bg3SUCu;4EUm-KuDIp_7(F{3on3v6Y*JHfKQM+OwS-5NeWD3Fj?kbM?re&dmFvL1>nzOVd$1IFcq{iAk5D zk3FT?6M_F*>1~M?*5mCbyg2}3nemUsW#Dr0K(IH67c76(?y#~tU@^7JG;Zzy^^;fu zwf^xBc%(%R#IVPj%jMWS0!$YV&%5MO=Zn}AK4V9{S9Ebu>(4H9-;j4=K3!bKe&~PK zdU^|+xY>4S#18Fb+Syl8hWuv1%I*)l67c%;Ve1iskP?RAl6fl2Lrfx(8 z-a$|fI(m$)`>-hU>5cO^RkBn5-KPu{Jd6jPvOr|I^@LQ^w(skHL!m*w?9`Qc4*P8p^Cp%;A5l zcVmZNQqC4e;1aU4r4K4M(%X9$kN(_KhR!IjT<@D(9HVJE;)A^nw^&~BEx+FJD&f2u zO2<$kF9a?_J(>y#D12$g4hUx*KEec?$QJ+}hdAfbCA?b0>*1{~^OHq^Q(HS*VQ)iw zjQ~E!o5YUZN|}c~py@wTLaWvsf*vx#8wr}6&CqEfJ&!|GuLYSqr!%}LMvll(6%WG)BZqfF+d=)I& zH_511k^KE~KA;HyewM#=zMby`h>lVT*G~P@et9ufM-G;^nI)IT3;jE`0?3Gtv1lEC zg)~04IY4Y!L9dQi&)8cXR#5Vk|AKq`9D9j@OYKK{4xurrN5#3{dhp3pI zC}JYmK)eSV0U9>Lz1l|H>GO53Xu<&9A+I#upt!_-+FabW_KgoL|9}aQdfi|p;2!C> z4MOq~EJBPY{tT*6%>0%lMRw*d4nI=nXz3J-Z;1(Lj4Ee&R0Pelh20~axIH7vzH@;k z_u-mp&l%~oo}Gf-*}iz4Ilq12zLR+Sx|1yR(Ssc3iqgXYJx5C5o3AQK5>vsLyxTMq z3f=x5wCgm;gLeZ~Qr`ifSnadh=Yi+nKGTE4Y=e18RGWI3Q`4STUa-#N zCg()OO76lmYDzx3b`fKV3h;K#Z?f-_ZIt*n8=JUEWmwJFG$(Hxr#=$jSpXhyR^1On z&!8iF0c!+Ht-;qQHDvU=FP}xg=n?!OGVYALXp4X&?`+;qA$_F6 zuG^xK{>^WG_h~9L#K=4$%p2s%;O8EGcos_v^Oq6CVmydaKoEqlTNqz$N>*FYZn1a} z!cRHTzJ5R?W=2AO21OUnWEzhpTH6iVbc)90Y*=IDp*o9m%7s>5$5@F=@mu={wu(0usKM1d`$q=_;d|Q0kK<2WxJlN6e$!z1Q!Kz8 z@^C9l41%>%+%rr`%hV?W;IZ90vL&5BKu5)BVuH%2r` zsS-~tlJ#5e*_h2oGF-3?09os}1RhQ@lTtlp&>+dd`kkV8ytlvsc4&wBzgs~y-EqZekC@?VOTMQEa1>Zfi$oX6XzLgW{_cz<(~Kpp#D=}I1Aht99o&lmMmrV^ zDb0d54EpLJy1@PgX>Q_HvtMQuVSDMh=ir?-QEpk>z;s+iawFPDHtDj*VQx)->7UXWXV2xR(gc6MNI~>?fMe` z)TBIFID2;JrdsWP+{_=aI?!1h;x&?TiHTSiKz9mdb5PPG3tG0AzjBp4*x7&)JO26R z4i#DUzNs#}#ByZl?2h?!LDaSqD;v|^ez3a%-Sv(bOzZsI=e-u;I%Y1rxy}zo)PIr8 zYMM0C zM1#cV={t-Gf4zKL4wTwWEBXsn{4T2_gCb|4`Dh>W#AxqEj9dUp=kpZgw?mH3P*$0` zMMWWrf5G&_!~y!=~07Zw-aL!Z1yz%h7{I_G0s~0fN8>{ZcCi1o`TG-9DP2vMO{PjW zs@%MJwp;RVrg-GG}1M2jh zvt*_5ztGVQwBh9jDyicHyZ<2q5I)K=71HeNCtM+YE`4G;JK|LFGq8L>aRkxWe6gZ? zGm9j$RX+oa+1&a?%9@2YJ@?b!JM1ebl7z+&q`$4n<1q@+2*NUv9Q`4Lb<3DD!td>8 zPEo|D_uU5UXggCAZ>Y%NS7!%E+Y!O}eb_S)sOYEZaQk1paxk}BvYd7waGXKR0P{SA zrgd7xaNAo}em=lXl5Me;s-?^I(IlFuQ8K8BS>>e2l2AMXW_N zX(KP|8>V!nahj~H-eQmaQcvQb9khZUUi5hnaC@n#1d(W3GJvR}{a~+%5^*+vmMm5a zsWy3qBz?jYnjvB%!q^^H&a4J9TU}CS!korVxxc$dhR`j%C^Yv=y1)91>+awm5Q%~v4J)Fwci^bDeS~wXy@|MiRMYX-0y2w=(8^bX?_t3GE-yxS$Hd;U`EGd zDwt?9KSKWxC5DQ9Z<%v}l=^pA#~E}y3wfbAUQ37umr!fFO1#xXLtZVTjsXNoX*1nA z2o$=02*hC=E~6TB)c%m7OrcC%_SA3T^xVEb!+Jbj{N2zd(8`j51)4IfMrUX!CF-z7 z@hpdgMUgyFRZS^9{beBNlNY7cOoH^U2TR4l9gJ_U;?>a4YH>NHL``r&BwrSjLr{LJ zH90IiS$^QC1PdAl8*Te9NfAm6J&u-CoG5hyB9>?82f|X~2ulOwMdCt9|Io0tsfK{o z%E_vmd4Hk7*9!dxNxc?}m?P9|SQxxBY9uNZAx_1DL73y3G=*uD=x_^W)ev_3S+b_G z2>sY}7_fu9CR>pCV5P0;ze}Muq)Vp+3Y;4S^!en-z}$}zR1mfDX>(wYCHfV@JncZQ zzPT87uKr7tC2=M#3-J?1I4>P1IZ|e(O!KNTvd-EglFMGfa@s`AD669%S3@(soY#_# z?0fMKJU&M)JvFmk##iElWa4G0OEm)TB9c=v5ZE(fcdQ*j@>YbVzOPMUr)ON zRR<8b%6j9=ua7GJwBv%4R7AQ^$bztX`6^JYGU}QBFB1QeFE2LotYl>b5m1(_t!>YG zj;$bt$U{#%7f|^(?kZ2KL3s;m%7vl*BOWfJm*}RZYYfaTv_hYiEVIrjXVn?pS26yx zKYzlY0W@TD@Eq!1#DhCgsUUDHKPxyE96KyOyaxgtYHz`R!hS?R_+?CCy*5@*R}dOp zN}JC4J2p;jnj%EA0BJRd=1lgPF_$!9uJwinpW(G0%}}8HzMaaW0jijJEn$~jW~1bh zpGoBSP6A}`!m*gC#V0A6?YTMSZIprY+l2zM5ma2V`W}dIc!2?y@A}_byg(^#M;Wp} zjJPlaR|p+CI;R;{ez|%f_)#YJt}@Pc%`?--SjY+IVV*!rQ6Dm>&L?Iqg+}L7*sMqz zJifZPYJH^nDwnnhqClJ5EjEZ4x19upmQ- zA48|^cw>POwK+ila)Ec`_o?F(-tS04zH#F+9thT{-}6cLgb+i+zBJ&W-_JX3EdZAq znc;cBJBxxTots7TQc|m_Q-~o^^lp91WJ-Pg%+5s2muc&nyf(C)Lg5&O|gvRq0u- z?}2Mx$D7-c@sUReQAvnN7IjQ$zgV__5dd^<2 zvM!vZtjXM*E`{()rxgVv|4U{S0q;stXNe>|28@v&7iNjNmt;(Mf7+EUM38155A6xd z6X;0D)=^IGo5GJ?_*qif!yll0?c|86CNOZX{P{aiU)rIxon~Ce_NY;a8BWB}$ENf-xN)Gy#?xiT)u(73*9kE`22-(JLsLtRIM+O za`J16Mew?OiAy6v484<1+FT3DYV^#q`Q(gSsZ8=xbT=ts%+ps!V`fyY7MOqL*8svp z)nLV*rh&+g2OMz{u>9HnFaXijo#Goz>!L+gFAZ+s;c{n#VqvX-$l{JSex(q}_zE;O7-#|%F+jKW;P#fvf2mSNLq2j-oausz#y zG~`lP1|a~>UFSK{kTlslNbEnW_We`Om8V=BL+oa8&9y-1*MO}?YKzGqCh|BpqiWv^ z@3EI%MZeK(f|$_i_WIaYdG0$KJ0<3P3%*VQ7#W=1sj()=AYTaKdC&5nM(G*L(3!=k zzJeJw{E4Nd-to&s?z{2%ATKZVVX=K-26M1Wrs>^GY+LKdXk z__-^qYxIzXV)c=(b;ObG9P#v;1mEl9)0zcNGh&P6Hi#e(-24KQ7;DV$?PMVDcR zLgwfS$KOA;GuShNm){hpMCT_?Y(nebzVDJtO*cB|6*&qL3;^p}DDxzrwms9(@iEBfv2 z?caUm7T0LK=f9o#HJ`ul?cpUB(D%-SJI<1CPIEdjEv+VBI0`DxPk)xwobdqigibbY zdq2n-1`9|^zWG+y1`=>@EUg2=nZDYTTzpW1?t5!i-V#;J$splMZ$NheV|_|*n1TXq z&|20v0&-WEl!#qdYN#F*&Uls;Tiv$BJkrpa&@y&hH49sxWxaPFFO@5r%9}z;n&!}m zk{`t~h1v4`?}0^F1wVVakQ5H&nDVGkrXZ{nQqG9|fFS9VUWlIO^Nv$(M2VO$CycI; zA0K5`561D^u6IxkJO74_Y{myltofs$Z9KGBr58dmha#BFobQ%!!6bFckdE38Xhw(5uW3Jx7PFUh#9`^eTW%K zC%WG)e|{LYJ)3iNH2&8GjN}0S+XpZ5GUb#+fN`NW+`pcc(@0!hE3FW6nnMF{&hGaDwgQnu4f084P$`)I#ifHjs+4UXQ)AwX-4M( zllTJig$2!efTiV`t`_-!Yrw$Rn|_9c@V=mVY~oXspc4F5yqoe4Nf+P@$B& zGCe)Jn2fVhNz+m`9XHhn^8_Sfu}9lFZI31zYnd{5qW#6FW!NZS%}^EPP$le$0pB@4 z5PB|peh=4t$Dmv7Wng7uo3hYWHb#}EZQ2~o;)w$ezlgiP!0aIlS6ipX>pi#|3)2(w z3;JXk5;9-!{#YV6GRYEdt~Nc{Eq{4@O%bRO5@Wf9ItV$z(xKrGkK%vtT0`?sea*!F)o}~{*deR=}ZR& z2`r)8G{3pu(+S7mJUQR?55u4itC`nx2zy}N;H1EM7MU0bIRXMzj5zBn`m`|^PQaEM^BwVPG+JW zkdUC|KC$rAxdQRk9?#7Q@iY8;IkWr6e+Bn2rLT-3K^Y440LFKmSd4wWU@jWKQQm8W zF3Fc%yVMV2OMUDsxT<@_$NaUKDlu8-zA)%BZPuvcV~gnz*@998fMklh1pA+wFyGFa zs(|tzbd0Io-i!s}w!?yBuirFpD{GQ&+s^DXirlme!7i?;ilkZ^;IGVEhy`_Ml98U8{R)Z!H5Z|bPdSK5fQ`gkfEis;@ z|Ks4-j3WkUkhH%W8d3lqez$mi_p4QI@T?!3iMQ_u8j*yF*~96;_1}M|1MThYx9!Ao zW;Y&TMA~wFXHWOuGH#n_$0G9o*0H@-u)9K-Q2Bq$fN;N|+#e60pNY1_Pg37U3}ocD zKj&HQa80=m{JAm-YW)rK_A;UqcQ|qXS<(FyrM9+G;4(+_nUl`bN`9kRKvw#?-!&wh zX}O2+kvcXCDr{Insp z3iKo;rPI0T2xMSl0S}xj^{dBxxQLz#Ay<-43A}_RmDN5QILd>U7Ru0rr4#hMTPcL6 zLb*O?huy~FzEm!IN1$NhqM?&l7Tym$--J(T4TgqUziB7alZ%@yMf8P(2Sl)154 z?x!j1u;Uk@GOZ`@HNxyJrO0jKCSV!WfknJ z6P~*he-nW0Bi&EmuBXKG$Lhal0o`)*)L}cJEmPMqMkNQ=VRoqCUJDTF5#;HHLZxoZ zkO4*eTVkeK&`Qynx3N!nZVIW&dpgkj0g-LFM>0SJ9qBhxwEivSKoeT9&lWJ4r{~oj5qJ; zO@Oum^gv4sh|qw_K|11JOAE{tg3}HVnkls6(gEVuaD-CBwvMilD*hG+o}e@#cXw9@ z(&tX{dq7c3m#zty?0*L+9Eq_j3GJ%Wl7`WVTPg*|3C0zIt9)oKZ4nXLoD#J7G9tD( zpcPab0KM1gKeT`U!EY484D?GL-!7V3hOx&peF*#-1=A&;|Eqsrc*t3D_{C}8Q`frp z|C87MPtVRxfc`ZBv^#Tmjv(;E4^Y-XfYu~>0dW^#U~v&a4sl`r1Cl~MxUG>8G5JFQ zX4{xYCcQ9k2WX+S0L3@I`|5TZK!4%Ay8zrb^#jinAi)FNo+JI`LwYdA*|}Q=90Pjc zxp~}B+@4$DEz11D$^kd8MxxVB1n8WN0?_c>yY}+a7p9hYn>B}D34JRcs_o!BZ@WH} zk7aQlVR}x$mp>Q&TJQ^nRX^nIu6z*lYwg;=xH)rky5Q06`Xku{XaMM;CZN#Fo=M?A zh;HNaZSAdCX2|ZlU`W*%>N>-YHYU#@%Z1k!6qqIfq?>4VH#ceNv4a1JL4)`VGBnNr z%JiQ&IHKcb6$(YzQM;?FGKnae{<5BAJqswCbAuX~lUBWV%*LSiy;kCR9*jb-@8AE` zL#3{l6oY=*jR2kht?3enJLPx#r;87y{9FG7&K|#1_|hgo|C#{WJ1a`^Oge-3ytKG9 zlb%`naN+%QdUlb1&5P+Yz~xLT1E}y}COvD(3~;|pDwWpwyrjoWYBrN0e4R6)zyRpf zYmeCg`cUfb>`ju67iMMvb?Noc{b3$#;r8v>OnQbk6mtvd)GZK6(&+_!{`4%bluo7K zc0~2N7MC{%fqk~(ar5R(h9uXVo%y*N3!t|+Tp9X6Zq26Csq`#-H47iY#2+B;hxl2b z;sGB5jD>|vDl>oQ)}2gh_QSdPTeoOG^@0onxU=D{XF=oV2qm_|dX3XEV2S~^hVV09y8`Z-96ZVL%O6DkWxOR5JTGf-#% z=x%BTfQAn4$zPJaw~ghLch*0=lm*bTzkZukj@0Yhx0?X%Oy8YFaJaRUDM>BOXP~w; z1 zGt-$lL!qYEF*9>`3jy@r4F}Nf8Qx-L+#Zi7y$l~Rd^h;1{5y}gBn@wSDdq8aJ)YFk zbkRGDckaxVxMyiAL;#&B@p#=+^u;VLEKmFH>ir|pNQ_BVnQG-t*3C_TUIEYpjeyTB zgwG^%1eip(1j&5IRV?6PU@ zSO zgaFMiK>&Kz>q!kCPA|_aWxU>LpLZcu^n;(eCIA!w6^aT0f>YmgrUAX3MKibO^*mh4 zRhLQ)yHiU`^E0!HOLNSgXAz(mJS9lo41hK#>;Rx&dnON6XtvqqFhJu-dEx11ectI3 zH?qYUcs*Y4%(N@{j;nJS@OLRyRP>iichiMM^m?2#fIskZ6X|K!4;g~y-=DiXGq<#` zxO97l)RvjsfY5L`<%Ycgf@pj(@TzVigFd_iU&pTfpmc12v?LACxr#y0PZ$05^n015 znfDh;yzjxMk}fX!2aj(NKMLoKRI21~Cbr8xGq;#3EV{XHcb)-yDOU80^t;6=_zE*K zi_8D=@Em+NbNKnpWaxWYYq}La5(enpO@KBu=w_ZKv_=ap;JRQdSnz;H2e=MAL{N-HpK;Q;$XFld^aG?t1Z4gd>S)w=K?TVW_(^GUrQxEj}IWXs3^F+^iE;X zjM_oIeQU&Gi)p9(+e=HsMMcP<7yi}> z46~%<_FqmfefY0aY5)J~D@iX;hy32)%u>1pUfXi2x&$7%57t%yEfU9!>q&VX8w;Sf zDS%!sai>6E%6NZ~Df-WM(?zMja;0b9a%C3hmZsrtFQ*TsYF$Ms*wb+q-&|BKyk(#3 z_HyR%^im4m{`B(yB9-1?>dP^YXbC50BGEJZUVFg(1GSAh0Ki#pz%zQ zaJy9%f(IC%gFJs2Kpolu!47sSN$ocx$kUVGQi@su<-}j=& z{nFR==kMJF=wA__zqnNa^i2flxwLb7dDau2@ulB!W|lLql;1bE^sajrcdLt1KX+x8 zr`;u<`FSwY%xEq5R;KX1<)sX$ptBzLq}SzZb^S%gTbue_&%EE60f1gI09xrAnnAz5 z0-$HunVc&689ZtF?u>gn4I2z+W+~(Pn-bsh;`HH}B~ZkRrc>~G($4td%$&#PN-u#A zC0*?O0o2>v^p-5oy5CMYeWPAa#$Ox^x~66TpMZA$L!qa{Q|Jd}0O893=uGieVe%UV zpm!obZ{6lNECB5-@_PIWvrte}lwQube|XlHxtW6ZJr4k#%4B}zO#4gRCCecC6wQ*< zj_+_`y6AW+m71IJ`cozDo4@V4`+awHwFmYyrl&pA%kyGuSp}eBexPvMGn)Wy1L)@F z=EeYGbYsxQqp^TRG3%hX@Td|T0h;4Fw7tW!NlWa17fBt2&`>e1(AnV7AqVw98RtHN z;$|MV0z{3Dg4`+vRgQ7%#;68oTVNfVHtfvV=DgE^{gP*gss?yBULkSg9*tC9dg&YA z+?)T(E1LlQD+2V6FK*qc0QwdJ^o-l{=NF6rbUCvWFP>Sx=?3Td-KA+4$Op?^#lWin zW_kK>syLnY+$e^*zj;C1bU|u)zQpbQyV=A3x&Pnm)4-@R@1@i47G+8ppe+shmRVea zb`8*=H9+GFDNHZ0#4(pH{<|f>pSc;&-z;VBP6I`rbxrt6mTw|1-(C9G@G@X?k}6EQ zrqiw)E`;1$Gq6Q*d((FTr)FFeum=FU>8TRW%xoFF?9|P)>)oHYQknlpapooou)yF| z2?sFf?N6}}dcy$pZV0E^u|om$5-qMnpnY1gl1j=%u+vjz^z3eaP zalc>Xd%I``b}{a94bjZceV)VK%;H<`rGC$u`K`klmoJ@meW&tU$<({)rMq`wHTy*%DAW%Vz`TH1lAgKBB5Fjnvz8{7F)HNTQTSrNp^Tc z7C`3abY}cckT}dq z!fb$E9Qt~m9iUSJ(6=(hKSPmaZszdc!0Sm7K>xYVw>*dVG&kpgOf9GHCNOK??^Su} zI`laJ^Y(aedi(&;*HW&T#o6M&1lGO$o)cd90$l&DcV_9wuDeSXK;vcttLa>i5rf{n zVHk8C0`yjtIItq})@&-XFz0T9&mvt6vjpz`BfgoNu(d>hcK!sc8Z*l^C9t34`SbtX zIkU(Bef#zd>`(w*u9)$8JZ@*^+lSLmP!fwSqkIznya$gb;7t_9nC+YLKF-+x2GmtNexd(X>X-UR4h z5ul&402<80peCn4sBld$-z1!c<3%s1m|pZ_6osZ!?-Zq{i!;CH1~IfKqa_a5efG^R zFM+->&mzWehdKp6W5{uG`=3L2;cAky^!|8OrNV|849wCrr}eW{`14>rGHzTL4Ym-Tgt-H za-Hveuh*6NF#$Av<_oFA{^jLGwq&6V#0=$TswjgyzxM3=8gR$QOaB(~@rw%W8j1i!{B`Ke0X2zGg>Gl5H<6B6({&dte4GKqz zd*;?W03TpMy72HkGH5Mc&ZRuQpHG(*PcJEe_M{5l1sg{CpF3yVz9n4~k%a-cm|h`q zXn-!BUc6c2dDG*a2Mroz6QIV6Dc8UHSFZnk>BA+z_txxu#`SLipwmvz2LR9u@cO5V z{i*-#>R$GNXTIPk=|b;2UU#Otux_yUEqH6d@zZYsK!Y73y{KKlT7#zGi9D;+d%Y1l z4>ahU+(!hU=fGE!hHc)m_x}xF#$SQ9o_3Y|$o0G6$3TE4<4h3ta0-0OKP*ImMgo27 z7VN~_NS!PCo3wKhzmSrmr4m>2&%CK0yMCT3eh2I@pmD&i4IXS?4tdlIAKjcJ%wo_D zBySKZ^SLHeIpElgRt}J%n*+*yhZx<0>IUL;8|l$Jd&p_hK>}=8r;XqlPuekD30O39 z=#Yib$e?v+l!QX)sIlY@vN7&FC}t11Vl1xKiqpNKCUs7lmJU5zsESzd@5UxTZwMDh zuER4kPrqlF>j(2i2J8-LA37jz|8+^p-+;n#tL*GFiem=Y82WscbEdRymtiPn@=l*ocw^%d90Buc0aiKPm>6>%^E=%GlAO?*E$TKBS zdXX;0SCN6o1QQ0FoBcVw9#B|s`9W1#T0V0o?U}h%0?+CPM*?v9|LFC3+rUEt@mlTa zbkWHYcqjjztIRtH9NZ6)Tg$2c*R6$&&zD}3LTRS<1khAT2*U410ce~ggdPF_(0Adp zdLKOMCBDB{n)gmGXTYrvc9rQ8-~Wgn_2rZYoQQW9m!|!<@T;5l&Vmndj*&Lg24d5@ zvkQaXlE0|V{54D>M*SuKVECz^{N&Q0O<7QBw>~hnj{W20HH~PZX$qg5^N3x zv`rfQ(d_WRa>GDS88|ErB*ej`!KD10WZaiO1Bv?QGeoDNGtA->Wo@{j31=hI4V`La8?a^c_%fGmFc&Gc#z*fFJ)d z6sduQ4k|`q2_+s7UU_#3&d+3S>ohe;@R=nK2U=~HZf53|pw12MeS2nZ7S3e2s@QVe zGA;^0|DwPyaqOL5WP0ypjcQ!K* zlo1OL)3_SmZDt7s+6=s#GA|TG0-WPZf)@_lnW`h_=dE-h0%)ov%-eVXU6fwL4-0Be z@H2)_e|Z_+%EHpp`x*Lf7UnbQ+bC5mWHNK`!N4blCGN8AxkF#gGJTOV3p4QHVE!n5 zv6)4sgk(z`n*hBgKsPitSP0!j6NNAUoST3|H>&)kAlo^bTAF}CLRl}l0Xa&-t zww4z3dVmIvBM1b#J*27;LmHrw6GNaKL2Yv7oS$tTkejAZMIpApiepc#6=JiQyOFfz zbEu&K`Xll8BgZB{{|b|YxjBXD4^b1ob0@17aEU9>RDJ^&fz#0ZQ^L|P!Gmwo#oo(H z%gcE0JCclH187dlTqxOXW6;|iZ_Uk6+$g@7JI3o-T+%hlh^q68l=`!9XYno;pWIo1 z%><-s&dpo*0>c^DgO`^7c?rG@$$x^C=P}A-UMB}JK;KFievT>{H;%+X01b%rt<(~H z91w4`pb9i?-wh>}s9>l$`dskCkS!uyT3YQpmCu8NJCwx1a|AMf3m)}??F`VHVM$8j z09bBlXsm0*1CAyNDgz&rMV&x=rRT*Ck}$9K95xD81lRJ5M9UdNoQY21+@X?D)PD|aIT zG`79@uj%8!&)jmrpl5Gp zygO;*x!E*UGw2AQv%+Qu==#QbKJ!{A&4?yQY6GnC5K?Qh(f+`SC12n17 z?X7K4Gmb18;b{Di# zK$3Bhr#At5W4JhS^R^wv{b)o)OtHAjI4hxNckX-$2Z-$d5Y7Nd+@Y)Qy#GG!c+ZI; z1L^F%5bb#kp<{f%yFgqOVw|%98XRu7XG`|)umf~X!M|6r@nTUHhW^3(wJuMY^8lkWGT%**o)hQT$%uFk}_!59vU+KDc#f4z>VyXiIVPCK&oS zn0f;OG(=?2aqKQ!I~-jt0lZm(eA_bA&W*{U)W%dD(~ ze7`Tg@S8s`$a!WHpzRVzT^&dqbqvw8tZxF4CXs^x8qryu!7{@Z3|DGGls24qfuO2o zQL)lg&jK(VY=?Rm(m6=vP!^9ahteVvf}-&iz#P@5;?=-%LcsEvf@Q$zF_NJPkrOtI zkE7^Jw-7_ui?>Ga3!r~F$X*9jzQ5PC@mw6h(xBrN{G8DjJcl{VcL~@&m}3Lh`_=$R zz`$WR64)U=$}N)Ae0Kq^pCb+jA;k=U@W2EjR?h$XBrDAPS>d*)x#ty?SmSWGrj5QH zIzM`qxZ;m)lyY+xoX$5{?Y?>QX7-JOYVY5^c{}CVowxl_NY~7>3RTuW51PN4+?~*5 zMqV?ywt39Xn(s(jqFA2|HAqo!RInO(G2iLt%BCvWSNs=OZXS-DO@P(_9VU1dg`~583e|10&67cNGy7FiNoyGZC$@Aj^C4R zzp#N=^G46&0P6ah``@MQJqBlVLp!stplW|NWA8mUGow2e@(nY(Pu|Rm&Vi~YVTp75 zquUvv^Y+UNd2Cnl=~Vh-bkCdl)mKUXTi4g~ayJ@4gUyZsI`_cgf19>CF#b|a24~E# z={{N#roUVK!gd-t{5m!P8Z_u|Lmf%bb#;Qyq;k}w@BvD+@aIM@HI!MzCYmS=5JoFl z=8m&L<(9@$mH>4&gfs0J=h05>S(GVHBn~w*IL<<}R+(2DSlANd;5(zXB~Idq269nK z5RH?F`2x|1&e*cO)`e4X8PjSKa9U7qX zHJN|9(B+|ZzWr@ntoq2`#_PY$Lsqn1#4~=n=%?{*W2|^E?_uX2DD&R90G;!j@ZEg- zw|?u}zxCU{^;>X+voOB>ZCb!p-~R32{x%)q8a#n(@DnI8-t=$r%6RABR(Hm0dBqa* zQ^6~|uEO6`UA;E}S^;!pxPkE*5W0~Ny1ucl9{4k&F;;;$6Kz)TOeKabh|o=xQ7Yc( zfb_~Tnr5LC?$Amn`;^vvR>;P@WA=wf5{woZ3Zvyl$3D8uz_F6%~>jg4if1QqWQeYde z>sw_M=(Sd^tnTspit)|-oq2iBsIK0*)_(Mk9na+-DtI7+Uj6H@z4*m1J|}Rr;Q(5` zo3FlxueXo~ohgu|c`VQ;YYgkYe0Ht%_P>yqw+YZI8T18W&&;6l6ac!hp}wJ>{Ty7r zq5zr=9!*X4sByG3DS+0IN*mH-tZwzht^wH@mbN$3bE-P)n-$_TFLx z935L_mT*O1Sm}0!@KDzi3DCbD zfQJ8mL@4=P;svTB(<68Faj)ALeb>5WJ7@JXK=0L`!|l5rD42Xb2v|ya1sXxZoc$SyL zu0z|xk`h#3i1-cw9cKzHkAird6j>uK;GB#RbjPzaXu#TUehWHvzx47i4bZzk4FLVz z#sp}>YU5F3WTEY|&%t;W2EguX|@(EVrXlQiwHet zy5y{aZB>ZEcN!=1Ed_}y!{7hn?|ug7|Nig)zwg2L`On{@lVAMeAAbH1aOwZ^pZ*T7fi+}ITc>p0 zG!9t4Cu{xeXMgwJ&;AqL^tR9W%nRJu#RO>lpw<(Oa(Dk4zMUOA@W=ptkJSL#^)ma* z`RoF8*866FK38WSAaEc|8x@A)!Ktpv@eYqT1D z4^e}TQ$9vQ!s_C2!DMpLC0VYpd7-lk1QVi3^C6Bhc9LNjR!Sgqo=7An5TdDpEYNIt zGL|@m`0gz%Ja`zoc7QUr-(bhP_B!2%oqH{a&Od;Qz59y`KT_p*06hAIUjm??*aYZ% z0CcV=m08_FH=oGJZNi=^t5+B?;?N@|L_I{=#}ICKjv*T?s4BufW9Aq zuIn-xv>^5Q^9|tRXsjpQgJ%$pT9$@-vPmBZ z(hgROlVpBN5KJc&Jdd%$JeFO@%@D34`Nt*91Hd_nDhD#~7)#E?m!W5P0v8h#a3+C> zji)y*Yk>aJ^Uj76-+-$CCe{6}FKFc5wb%ROA4faA8nbhYya#u=Jo^rwb3c~9_p1cf zdtch~@|RxTvj_4=U)}`hPdPw)xs5p2moG|RZmJ)b(nf3Onhh+Zk%4FBT6Kjrpn}C_ zslLTdXz1VK&3l>v`k8-}0R55o1^C_s==)>P%%RUQlfHll^4`_+>|vuedEj}_J^;IM zs?ds3Vk4e~(BzfIfiiS+3j=fjkI16gk0$_4KITM{c{Cs>d*={*kw?e%pX!ui8tSmF z5YahF0F7YGMK5?If;0hhB0dT5k2fVr=(sVi0DAXJ&$}+V-0s(&E5c-WiyH5AO?!)r zJSi8R-wjyoaKC%#Y45*yalgy?mHe-MHGfb3{=Hv)ao2Bt=}TYQ^TL;5?EVx0^w&2e zK>ylc`hFfQ`&ku0yV7^(?#^?P(E{N!BlY3}!!$SPrt6?24ohYjb~)z5f?}$c`|ty- zn8C6dDu0I3o7=ZmCFi%(E(g7=XEv-l_?g9OhACU&Vof{-yu$!(By0f0FQ{Vm zyA-RmpaQIK&EB#K$tkTLduE67b3Sbn#{(Lh0IdM}TvzS6bLVOqq*>v(0B1pkZm>KZ zB5>5P3XL{tKRZ>WmF`C>q&;lZLA1rBT)PS{KfZOe181kFjH37*JfcqRrdvwak! zB(>a0t#ZVbB^vHRMm-cW5RwtPLCTmAe3mf@TpHmUAbKM4wqnpPzx?9!?)afE00z7F zJ>vBjyri1OLYXU==D$!_u=h}P#9MizFvpkndiv9zg8!!{$M^5Oz8l{@^cBy~e7^to z`^DetD)EN>hhDM(`h^!hae%((_;g7e8v&r7pI)NOpSe3r%g{<3bqvVFgZz(q$ve7( zmzS6B&Mho2;d+Zp5XFRr=TJO;3ohK@sE`G%gCIgf_}-#K>Z$AyYp~b>{hdB-62~S$ zKWK@g3rRD9b1f6;vqYvZ)G@6~N=q#jy1*O!iyzmy54`Aov*?@7>m{G_ zqzVs}!0|tL9sZv<3*FO2&S_s^VM(gU^^2lIE??npU5NMs_**|=fZln}@xsQFIBcl6 z@c{Z;sYL+jTXPGUR4PMBr%UNnYIbpcKAp-?u=K6e^e^5^&CF*~Y3LTdkWS4=L#>;D zvMC6Qo@e2DNiP;}r_$UQk$Z35n$7CLhanX+E=o&!#%y`D-T=MISGEbz>nd@aV|=Dj zdrkxoHC=els9`zB08I#8ParMnN_F-)yv9aFp@BR%Wyh6r-h!`BWBvIv&6B%0os|K=bqkP547jTMin&^JfD2+(u$5}CYAdDKfwx4l1KhLZ)!pThWtn6QKF4aclxK0dzyxIW-8S>v(wq=L$`4v<;#Gqs{m`n-{N+3)-RX3fC_G|+5rxu{8 zC*iGIGd@=;1sx9OprMr~{ny1A>`CbLK%cGoOp!mG{@2b_y08GcsHUAoGYX(*ZkD)# zYlG}DL)};AxZ?=)Q*pnud<*)DEU0c<*#M1JJQoA>F2^Q7ZvynH09{;X0W?E2DIB$e z%ZfViH;CDE=+n7Fc>K`nKqK5W=tX?qx8YxJ&7wIPEWx7ZX^!pF+lI$?ez5? z+UNFpz0R$k(xQW36@bpK_^10``pP4}J)JY~U!eTU<$laFedtB+PhUFt55CX&eGh-t znJO+$AO0sh-Kk=ytFXwOa_!s%=ubI7-@ZK)nqFG+`e#Z4v(wN>umHMur=eqP3Y3aW z(T}0$6QFdlvt)TGRD}J-sb3e^qJ8dJ6f$5M8CvtrT=05JGSEKJQ&RNTv(;X2E%#Ae z*}ay#dJ{nB6c%iKWP<_pI%D)EKx+*;q}2^Lo;`QA*3O$npEj<$&~U!t!i9zgJ$+aw zC~W~Xo{-DK-Z0$oR`IQJQ0G%n`j>#jr-#GZlCO~fj^eOHnAC8%Pxumj0xB$aiS>CP0768T9O}xy9wQ$1}T} zfp(|M?;UncPZt3Wd-|41k)`C;uh|Af7Hv z&8CV{qlf+`{U6iI3u^)NY{rEyj=bD2J+TSUn*hBUK=1N|f;CK@adEa*q>kEiTHdgx z3kjb|g}#7?3yqBz>a@ZkRy?X?!D9nD@#+bl8&#roK>MZH2MslTNg&MttT7NfCXz{> z$fKFQB>S4#*MW85cq>gSCQ#j=FpdPv7)gb~31rZ6X_ELeU*h!ytgrvLC*;$wmfWH4AcssxZ zUF3PsxwEj?Rb23uJ-Z)x7RSd)=h(0Sy?%B-255Q>0MIuPWS7z&Z21Z!Ju?gQgutK| zXWfxy=;U2gn}S6D<&-PqeyhZvf~ME#?ht?m*W9gCfd@wV7ci~p0>4k@cDg8)@%udP z)Y2ju?N*mKxLD&ibFGhJ;{fz}el>ZoewJJuYi*_+jsw0(9icM~z~?i7))#6oklT)s zx~^8x`CKhjfb;BOef@=ob0kEo`W2mrhv1C0hMeYs>of~OOK538((zD(2HK2Gtl}DA z2I%}vfX3%?`XhqQcyjjaxiiS55tL=pkoApP7Cg>#8OsI3*P)=9 zZYQ(!`SbNOFj5+fcnwIQ>l%$nX~Ad*=*DDBo*>L6wOQeCax#`=M@CH0SrRwmT&%*m zbqT5SopJRxKe*z2Y7?8$?4A7)vuqO_IHkK4KMnWd_vZ_tU)GP&s2_;-4 zM8-gl%yP!0g5oKl(UWx3lw20f{^5^H3U{hLR(rqtm3)vccJH(0)4lxVFTctF{k5+l zMy~{DR6UA71ljuvs?U2q4GjA0X6nHvKtoH!M=b`8jZrf}-;py%Z_M79UdkLTIX*qV zoF4q0VT9tvObH?KQo1BFdwzO3{RbKBaLWZK@0HZll-*cNRo9kHFQ)(LAO6K)bUED^ z`P*=MCR*Z;0lsU1wkiqP2fYcLpeg=%p(7|+Wne5D%tua5FLATsIoX#%7;a29h z2IyOSH@r(`ei@d~BXxOcCNn!T4|hjX^gRH&XbUxw*`2cq(3=3g3V}Xc7OG=>J`D(6 z0| z!ifaCH5gnOg6V9GLE8Xm>TH#ubrLa&5LI)8`eaf_^ArJeBAGp=l0ST>q-fhSFWjRm z;ma^S5}>i@aJL5N7dJ9Mzpx3=s{yn~9664+7bqHI?hY1eFDx!uW0B%CsMchj7qAW_ z-a!)59r0$&&1c=my!*luW^}9y(6h7erwSdL0KEy&YXJ1aIZj^>6KGhRJ`IP{X8@_s zVzEjs+d9PEaqc`I^qI5gan#j<(1C{O3l@PsS7*6C>Ph1`57ppQZ;1F@XEmjj(iQ}0 zsOn2h@@${xx(vC@s3(O{TT%y+=LA(CCOI)Fk+2hHt#24hbq#48czpAnDevo#zOZ_` z?q6=Kng_jWql_1J5vVWVSjU;pwlnsT==D5Y_NpPZy11W8T zg6tLu;1?(iPk>pC+qYMVOY-fcZDt;M$~FOd6QFG)|B&}wunvfG&1r<_Q>T$f*PcFe z_Vih0jHUx>ALr`MoCETViwhTsJ=awPt}MJeY(~f=h%CP-sp2=?>KWFwT&|@eO$P3PUrj6Le(7t zEY}l!H`GzVVO?E=Wy)(*-NQlRm_n(Tu@ys%CMM$+8|FZMk~FjqEhU3g7CynNu`k*f z354ItDJIT_#KH0g12&$#{dT}xxO3+(0d;FZ`3o!5;SB}QpMQMA0Ceug1L)kGZ+UN| zGdSHhqZTvzM2<3tn6b{t#Thxlk)fkrNj<{Kk^V2f!l$I2$RYZ@U!ghu!k(kUU?P@W zdk}ZlJ`L13CgOa*IE8Ky8Z#hICgqIG6Dl!!%05px_4YeaU(tc>dA~MkVj}}Iyq?cL zR0c6Poez|@cHcN^Jh6%DSOl+&qjPkOa`Ac zkF#~>M60%4TdWot|%rJq$&(`d1B&{|1dFNnEFa3mHTS30xdrnAouu5@wG z8aLj)?)Nwk?aawD8kDRARr^@${k<$^^cEu*bN2Hc^e0y_ZuAya~|Kd zci$_oy!!R8fBn^0Ufr|jWg4%(`U+MmflcR?uiRC#>@FmPf1-U zSX*6#e>Ep-PM$sm@LXHN8ixVXq)neffIfv5JV0oS>o}_{9+H8F%d_X{GFduf5Ja#`YC=y!ZOQ-1z8+-BR$XV`Ge5$A%gC zwqJ?*sCI0{VV-~DqlW&NcNiCK_q9I`+-m&Nb9Qf-Ph{)M8{>84?AGGD;2Eo6<;7lsV$y29MjIN>SLaAIKl!kHU6f@}B(%CQrySxgX@jwco7VkSpIaGEp@oCCznPz>j1r6X=m0b0wcDa*KF z%@!&dn$BE*`|Y=r!4gO*`xFhgxzmjM&9e9GvF|{yGQ1zP!e?-J*1GnAe3c*dI@aYA z@oea?<}>gw>GAr?YC=MvPtp+ngy3HxNJm(PLT$lzyhNAjYQBW$TH&M}*J*8s(c0SH z8fP&?Q<1g01asEwBjE)6kRgv;3j; zVrM75jS#%>PB_~I@1(P{8xGx_J>75t-)0!zGaljAuw0h*@Hj{#G8hRD%BB86 z{Eq$?Sim1#8tBKH`y+HifA3)LV1GY6G~X|RhXFkF^$p;QQLhPJM3`P=Umt#-y**+4 z3*(o^Z;Ib1U4iemqYb}S`jX)j>I!ysw!xMv4X$)A6FD}JH))zhGF zjlaZJI5amyqvz)DwKhP*=VnfIZw_FRFwFEzmN0p=n!peM0hWz=SGj?I<>$n)TU$f; zP)k|H2oX>Tclz`>+DAWk4vwe5K~{6>O!bMH>e}D^Z+`c8i5emVRacV?3+MyzbP^7y zPMxaZ>yTiI2w)7`WmvK5WEHJRbo3;iIavkQRae!pynC{`8rFcT;5@7h!+7LsKJpWq zk1V(0nbUl7>IC@Vs!tGZO5vYST+t*9^nbf#ZiigvFaT-h%NaT)^?BeO%8TQ|3UXkL_V5t@+X< zXO|>mYmad}(1rpQv>8yxE2CHN@?{v8;4pgm5^NJ>Tpk@69Ui%O@#4tE!NJjC1QJ+` z4$2sSO$BZ!Muq^a0Ez|%F7^)qiuDin^C0X(7>==;5L zmA=(sS`5NsWMpt;?}FOj}L7z4Pe!tI2dgY5=v8sG?FB{){YN{KoDat9w*M4yQ2LzFblz4!} zH9E*Tlh?0LUb}W}a`M{L#I62X z0ciB{mCNu`8y&uKWfYKVgusjF^#}~0*h53Z1IV%ACkB@;64~qT>+2^fh(D!1`ZbPiJ?Rj1FO5NW$8IpLPV>I@&_&hoqRUrePucUA6N65iZ~#tF65aNOvIg zN1;DzX%W)>M@Yv)f24@k(Jlucr zBA&Z==@Ra@T!dYhOT(9j;M~yAB^W~kcmsUkBlyYF&dtd1sNRa;VtAOhZ!Hpok_&)evBmKPt5$4tr;@7+%0DA`E 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 { 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} -
+
Date: Tue, 4 Aug 2026 10:54:02 -0700 Subject: [PATCH 02/19] improvement(docs): inherit the platform border and font-weight scales (#6252) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * improvement(docs): inherit the platform border and font-weight scales #6241 consolidated the app's neutral border tokens and flattened its font-weight scale. `apps/docs` was carrying an untouched copy of the pre-migration values, so the two have visibly drifted — the docs `@theme` block already declares it "mirrors apps/sim/tailwind.config.ts", so the drift is against stated intent rather than a deliberate divergence. Borders — same consolidation as the app: - `--border` #dedede -> #d8d8d8 (light), #333333 -> #444444 (dark) - `--border-1` and `--border-muted` become aliases of `--border`, so the 19 existing `var(--border-1)` consumers pick up the unified colour without being touched - `--divider` is retired; its single consumer moves to `--border` Font weights — the arbitrary values the app dropped: - `font-[480]`/`font-[470]`/`font-[500]` -> `font-medium`, `font-[430]` -> `font-normal`, `font-[600]` -> `font-semibold` The navbar's active tab and its invisible width-reserving ghost both used `font-[480]`; they move to `font-medium` together, so the anti-layout-shift trick still holds. Deliberately NOT ported: the app's `--border-width` hairline (0.5px on hi-dpi). The app wires it through `borderWidth.DEFAULT` in a Tailwind v3 JS config; docs is Tailwind v4 CSS-first, which hardcodes `border: 1px` in the utility with no theme key, so matching it means overriding a Tailwind utility. That is a site-wide visual change and wants its own PR with visual review. Also unchanged: the inline SVGs. `components/icons.tsx` (328) is the brand and integration set, `sim-logo` is a brand mark, and the handful of remaining shapes are bespoke and positioned by hand. Docs already consumes `@sim/emcn/icons` in the 15 places where a shared icon is the right call, and imports zero lucide. * improvement(docs): finish the platform token sweep Follow-on within the same PR. A full comparison of every custom token docs rolls against the platform found three more classes of drift. Text scale — #6241 retuned these and docs kept the old values: - `--text-body` #3b3b3b -> #434343 (light), #cdcdcd -> #c1c1c1 (dark) - `--text-muted` #707070 -> #7a7a7a (light), #787878 -> #6e6e6e (dark) - `--text-icon` #525252 -> #5a5a5a (light), #a0a0a0 -> #969696 (dark) Docs and the app now agree on all 62 shared token names, with zero divergent values. Missing shared-component tokens. Docs renders `@sim/emcn` (Badge, Chip, ChipLink) and `@sim/workflow-renderer` (block, subflow and note views), but never defined 14 of the tokens those components reference — an undefined `var()` silently falls back to `currentColor`, so the failure is invisible until the branch that uses it renders. Several are live: `--warning` on an edited subflow, `--caution` on inline code inside a note, `--text-placeholder` on an empty note, `--border-success` on a successful run edge. Added with the app's values, along with `--text-icon-muted` (new in #6241) and the four Badge palettes docs lacked (teal, cyan, pink, blue-secondary), so any variant renders correctly rather than being one prop away from black. Type scale — docs declares micro/xs/caption/small/base/md in `@theme` but 20 call sites bypassed it with identical raw values (`text-[13px]`, `text-[0.8125rem]`, `text-[12px]`, `text-[15px]`, `text-[10px]`). Each now uses the token; every value is byte-identical, so this is a rename, not a restyle. The class reordering in the same files is biome's `useSortedClasses` reacting to the rename — verified as a pure permutation, with the class multiset unchanged in every file. Deliberately left alone: - `#33C482` / `#2FB3FF` — brand-mark SVG fills and default props in demo data, not styling. - The Ask AI button's inverted `#383838`/`#575757`/`#e0e0e0`/`#cfcfcf`. The platform's `--surface-inverted`/`--surface-inverted-hover` hold *different* values, so adopting them would restyle the control rather than tokenize it. Worth doing, but as a visual change with review. --- apps/docs/app/global.css | 64 +++++++++++++++---- apps/docs/components/ai/ask-ai-panel.tsx | 4 +- .../docs-layout/sidebar-components.tsx | 8 +-- apps/docs/components/navbar/navbar.tsx | 6 +- apps/docs/components/page-type-badge.tsx | 2 +- apps/docs/components/ui/faq.tsx | 4 +- apps/docs/components/ui/search-trigger.tsx | 4 +- apps/docs/components/ui/video-chapters.tsx | 2 +- apps/docs/components/ui/video-placeholder.tsx | 2 +- .../components/ui/what-you-will-learn.tsx | 4 +- .../workflow-preview/block-inspector.tsx | 2 +- .../workflow-preview/output-bundle.tsx | 16 ++--- .../workflow-preview/workflow-preview.tsx | 4 +- 13 files changed, 78 insertions(+), 44 deletions(-) diff --git a/apps/docs/app/global.css b/apps/docs/app/global.css index 09ffa108b86..d7709776139 100644 --- a/apps/docs/app/global.css +++ b/apps/docs/app/global.css @@ -65,16 +65,17 @@ body.wp-lightbox-open [aria-label="Ask Sim"] { --surface-5: #f3f3f3; --surface-6: #e5e5e5; --surface-active: #ececec; - --border: #dedede; - --border-1: #e0e0e0; - --divider: #ededed; + --border: #d8d8d8; + /** Legacy neutral-border aliases. New work should use --border. */ + --border-1: var(--border); --text-primary: #1a1a1a; --text-secondary: #525252; --text-tertiary: #5c5c5c; - --text-body: #3b3b3b; - --text-muted: #707070; + --text-body: #434343; + --text-muted: #7a7a7a; --text-subtle: #8c8c8c; - --text-icon: #525252; + --text-icon: #5a5a5a; + --text-icon-muted: #5c5c5c; --text-inverse: #ffffff; --text-error: #ef4444; --text-muted-inverse: #a0a0a0; @@ -84,9 +85,26 @@ body.wp-lightbox-open [aria-label="Ask Sim"] { --surface-inverted: #1b1b1b; --surface-inverted-hover: #363636; --border-inverted: #363636; - --border-muted: #e4e4e4; + --border-muted: var(--border); --badge-success-bg: #bbf7d0; --badge-success-text: #15803d; + /* Shared-component tokens: referenced by @sim/emcn and @sim/workflow-renderer. + Undefined here, a var() silently falls back to currentColor. Values mirror + apps/sim/app/_styles/globals.css. */ + --border-success: #e0e0e0; + --success: #22c55e; + --error: #dc2626; + --warning: #ea580c; + --caution: #f59e0b; + --text-placeholder: #8d8d8d; + --badge-teal-bg: #99f6e4; + --badge-teal-text: #0f766e; + --badge-cyan-bg: #cffafe; + --badge-cyan-text: #0891b2; + --badge-pink-bg: #fbcfe8; + --badge-pink-text: #be185d; + --badge-blue-secondary-bg: #bae6fd; + --badge-blue-secondary-text: #0369a1; --badge-blue-bg: #bfdbfe; --badge-blue-text: #1d4ed8; --badge-purple-bg: #e9d5ff; @@ -116,16 +134,17 @@ body.wp-lightbox-open [aria-label="Ask Sim"] { --surface-5: #363636; --surface-6: #454545; --surface-active: #2c2c2c; - --border: #333333; - --border-1: #3d3d3d; - --divider: #393939; + --border: #444444; + /** Legacy neutral-border aliases. New work should use --border. */ + --border-1: var(--border); --text-primary: #e6e6e6; --text-secondary: #cccccc; --text-tertiary: #b3b3b3; - --text-body: #cdcdcd; - --text-muted: #787878; + --text-body: #c1c1c1; + --text-muted: #6e6e6e; --text-subtle: #7d7d7d; - --text-icon: #a0a0a0; + --text-icon: #969696; + --text-icon-muted: #949494; --text-inverse: #1b1b1b; --text-error: #ef4444; --text-muted-inverse: #b3b3b3; @@ -135,9 +154,26 @@ body.wp-lightbox-open [aria-label="Ask Sim"] { --surface-inverted: #242424; --surface-inverted-hover: #363636; --border-inverted: #3d3d3d; - --border-muted: #424242; + --border-muted: var(--border); --badge-success-bg: rgba(34, 197, 94, 0.2); --badge-success-text: #86efac; + /* Shared-component tokens: referenced by @sim/emcn and @sim/workflow-renderer. + Undefined here, a var() silently falls back to currentColor. Values mirror + apps/sim/app/_styles/globals.css. */ + --border-success: #575757; + --success: #22c55e; + --error: #f87171; + --warning: #ff6600; + --caution: #f59e0b; + --text-placeholder: #8d8d8d; + --badge-teal-bg: rgba(20, 184, 166, 0.2); + --badge-teal-text: #5eead4; + --badge-cyan-bg: rgba(14, 165, 233, 0.2); + --badge-cyan-text: #7dd3fc; + --badge-pink-bg: rgba(236, 72, 153, 0.2); + --badge-pink-text: #f9a8d4; + --badge-blue-secondary-bg: rgba(51, 180, 255, 0.2); + --badge-blue-secondary-text: #7dd3fc; --badge-blue-bg: rgba(59, 130, 246, 0.2); --badge-blue-text: #93c5fd; --badge-purple-bg: rgba(168, 85, 247, 0.2); diff --git a/apps/docs/components/ai/ask-ai-panel.tsx b/apps/docs/components/ai/ask-ai-panel.tsx index e6af234ebb6..c55b05dca0c 100644 --- a/apps/docs/components/ai/ask-ai-panel.tsx +++ b/apps/docs/components/ai/ask-ai-panel.tsx @@ -145,8 +145,8 @@ export function AskAIPanel({ locale, open, onClose }: AskAIPanelProps) { className={cn( 'space-y-3 text-[var(--text-primary)] text-base leading-relaxed', '[&_a]:text-[var(--text-primary)] [&_a]:underline [&_a]:decoration-dashed [&_a]:underline-offset-4', - '[&_strong]:font-[600]', - '[&_h1]:font-[600] [&_h2]:font-[600] [&_h3]:font-[600] [&_h4]:font-[600]', + '[&_strong]:font-semibold', + '[&_h1]:font-semibold [&_h2]:font-semibold [&_h3]:font-semibold [&_h4]:font-semibold', '[&_li]:my-1 [&_ol]:my-3 [&_ol]:list-decimal [&_ol]:pl-5 [&_ul]:my-3 [&_ul]:list-disc [&_ul]:pl-5', '[&_code]:font-mono [&_pre]:my-3 [&_pre]:overflow-x-auto [&_pre]:rounded-lg [&_pre]:bg-[var(--surface-5)] [&_pre]:p-3 [&_pre]:text-small' )} diff --git a/apps/docs/components/docs-layout/sidebar-components.tsx b/apps/docs/components/docs-layout/sidebar-components.tsx index d4c279e255b..cb7cb3e8dda 100644 --- a/apps/docs/components/docs-layout/sidebar-components.tsx +++ b/apps/docs/components/docs-layout/sidebar-components.tsx @@ -56,7 +56,7 @@ const ITEM_BASE = const ITEM_ACTIVE_MOBILE = 'bg-[var(--surface-active)] font-medium text-[var(--text-primary)]' const ITEM_DESKTOP = - 'lg:mb-[0.0625rem] lg:block lg:rounded-lg lg:px-2.5 lg:py-1.5 lg:font-normal lg:text-[13px] lg:leading-tight' + 'lg:mb-[0.0625rem] lg:block lg:rounded-lg lg:px-2.5 lg:py-1.5 lg:font-normal lg:text-small lg:leading-tight' const ITEM_TEXT = 'lg:text-[var(--text-body)]' const ITEM_HOVER = 'lg:hover:bg-[var(--surface-3)]' const ITEM_ACTIVE = 'lg:bg-[var(--surface-active)] lg:font-normal lg:text-[var(--text-body)]' @@ -144,7 +144,7 @@ export function SidebarFolder({ item, children }: { item: Folder; children: Reac 'flex flex-1 items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors', 'text-[var(--text-muted)] hover:bg-[var(--surface-active)] hover:text-[var(--text-body)]', active && ITEM_ACTIVE_MOBILE, - 'lg:block lg:flex-1 lg:rounded-lg lg:px-2.5 lg:py-1.5 lg:text-[13px] lg:leading-tight', + 'lg:block lg:flex-1 lg:rounded-lg lg:px-2.5 lg:py-1.5 lg:text-small lg:leading-tight', FOLDER_TEXT, !active && FOLDER_HOVER, active && FOLDER_ACTIVE @@ -171,7 +171,7 @@ export function SidebarFolder({ item, children }: { item: Folder; children: Reac className={cn( 'flex flex-1 items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors', 'text-[var(--text-muted)] hover:bg-[var(--surface-active)]', - 'lg:flex lg:w-full lg:cursor-pointer lg:items-center lg:justify-between lg:rounded-lg lg:px-2.5 lg:py-1.5 lg:text-left lg:text-[13px] lg:leading-tight', + 'lg:flex lg:w-full lg:cursor-pointer lg:items-center lg:justify-between lg:rounded-lg lg:px-2.5 lg:py-1.5 lg:text-left lg:text-small lg:leading-tight', FOLDER_TEXT, FOLDER_HOVER )} @@ -214,7 +214,7 @@ export function SidebarSeparator({ item }: { item: Separator }) {

{item.name} diff --git a/apps/docs/components/navbar/navbar.tsx b/apps/docs/components/navbar/navbar.tsx index 20d8020c0bf..8b2ee8fcc2a 100644 --- a/apps/docs/components/navbar/navbar.tsx +++ b/apps/docs/components/navbar/navbar.tsx @@ -77,12 +77,12 @@ export function Navbar() { className={cn( '-mb-px relative flex items-center border-b text-[14px] tracking-[-0.01em] transition-colors', isActive - ? 'border-[var(--text-muted)] font-[480] text-[var(--text-primary)]' - : 'border-transparent font-[430] text-[var(--text-muted)] hover:border-[var(--border-1)] hover:text-[var(--text-secondary)]' + ? 'border-[var(--text-muted)] font-medium text-[var(--text-primary)]' + : 'border-transparent font-normal text-[var(--text-muted)] hover:border-[var(--border-1)] hover:text-[var(--text-secondary)]' )} > {/* Invisible bold text reserves width to prevent layout shift */} - {tab.label} + {tab.label} {tab.label} ) diff --git a/apps/docs/components/page-type-badge.tsx b/apps/docs/components/page-type-badge.tsx index e1f7eb30db5..df46fd1d962 100644 --- a/apps/docs/components/page-type-badge.tsx +++ b/apps/docs/components/page-type-badge.tsx @@ -42,7 +42,7 @@ export function PageTypeBadge({ type, className }: PageTypeBadgeProps) { return ( -

{title}

+

{title}

{items.map((item, index) => (
Search… - - K + + K ) diff --git a/apps/docs/components/ui/video-chapters.tsx b/apps/docs/components/ui/video-chapters.tsx index 3010195bcb8..4d8850dd50d 100644 --- a/apps/docs/components/ui/video-chapters.tsx +++ b/apps/docs/components/ui/video-chapters.tsx @@ -43,7 +43,7 @@ export function VideoChapters({ title = 'Chapters', chapters, className }: Video return (
) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.tsx index 66774054cc6..012158e64f5 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.tsx @@ -112,7 +112,7 @@ export function AgentGroup({ )} 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 44b27dba5ff..5756f57c72f 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 @@ -268,7 +268,7 @@ export function QuestionDisplay({ 'before:absolute before:inset-[-8px] before:content-[""] disabled:opacity-50' )} > - + Previous question @@ -286,7 +286,7 @@ export function QuestionDisplay({ 'before:absolute before:inset-[-8px] before:content-[""] disabled:opacity-50' )} > - + Next question
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 6372643c46f..67708c96aee 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 @@ -1383,7 +1383,7 @@ function OptionsDisplay({ data, onSelect }: OptionsDisplayProps) { Suggested follow-ups 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 8a78cf03537..8641b9b5eab 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 @@ -362,7 +362,7 @@ export function SuggestedActions({ onSelectPrompt }: SuggestedActionsProps) { */} - + 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 a88b1726e19..bc82dbf7f57 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 @@ -104,7 +104,7 @@ export function FieldItem({ {hasChildren && ( = { */ const STYLES = { row: 'group flex min-h-[30px] cursor-pointer items-center gap-2 rounded-lg px-2 -mx-2 hover-hover:bg-[var(--surface-active)]', - chevron: - 'h-[7px] w-[9px] flex-shrink-0 text-[var(--text-muted)] transition-transform duration-100', + chevron: 'size-[14px] flex-shrink-0 text-[var(--text-muted)] transition-transform duration-100', keyName: 'text-sm text-[var(--text-primary)]', badge: 'rounded-sm px-1 py-[0px] text-xs', summary: 'text-sm text-[var(--text-secondary)]', 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 c23902878da..b33dbab6314 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 @@ -24,7 +24,7 @@ export const ToggleButton = memo(function ToggleButton({ isExpanded, onClick }: > 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 4634acf786e..75c3c183bfa 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 @@ -206,7 +206,7 @@ const IterationNodeRow = memo(function IterationNodeRow({ {hasChildren && ( @@ -316,7 +316,7 @@ const SubflowNodeRow = memo(function SubflowNodeRow({ {hasChildren && ( @@ -443,7 +443,7 @@ const WorkflowNodeRow = memo(function WorkflowNodeRow({ {hasChildren && ( 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 0b41a7caf2b..c57b08ea2ff 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 @@ -113,7 +113,7 @@ export const WorkflowControls = memo(function WorkflowControls() {
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 130096badc6..610365f9bac 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 @@ -468,7 +468,7 @@ function ConnectionsSection({ diff --git a/apps/sim/ee/workspace-forking/components/fork-file-tree/fork-file-tree.tsx b/apps/sim/ee/workspace-forking/components/fork-file-tree/fork-file-tree.tsx index 76fa5a5e2b1..695edefa6c9 100644 --- a/apps/sim/ee/workspace-forking/components/fork-file-tree/fork-file-tree.tsx +++ b/apps/sim/ee/workspace-forking/components/fork-file-tree/fork-file-tree.tsx @@ -142,7 +142,7 @@ function ForkFileFolderRow({ diff --git a/apps/sim/ee/workspace-forking/components/fork-resource-picker/fork-resource-picker.tsx b/apps/sim/ee/workspace-forking/components/fork-resource-picker/fork-resource-picker.tsx index 9762d82e74d..3a6ddd0f7cc 100644 --- a/apps/sim/ee/workspace-forking/components/fork-resource-picker/fork-resource-picker.tsx +++ b/apps/sim/ee/workspace-forking/components/fork-resource-picker/fork-resource-picker.tsx @@ -74,7 +74,7 @@ export function ResourceKindRow({ @@ -169,7 +169,7 @@ export function FileKindRow({ diff --git a/apps/sim/ee/workspace-forking/components/fork-sync/fork-sync-view.tsx b/apps/sim/ee/workspace-forking/components/fork-sync/fork-sync-view.tsx index 02e22593e73..0423b05b3f8 100644 --- a/apps/sim/ee/workspace-forking/components/fork-sync/fork-sync-view.tsx +++ b/apps/sim/ee/workspace-forking/components/fork-sync/fork-sync-view.tsx @@ -473,7 +473,7 @@ function MappingKindRow({ controller, group, summary }: MappingKindRowProps) { 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 b131ccee387..11f8de5a6ce 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 @@ -124,7 +124,7 @@ const ChipDatePicker = forwardRef( aria-hidden className='inline-flex size-[16px] flex-shrink-0 items-center justify-center text-[var(--text-icon)]' > - + )} diff --git a/packages/emcn/src/components/chip-dropdown/chip-dropdown.tsx b/packages/emcn/src/components/chip-dropdown/chip-dropdown.tsx index 6173e1ed96d..e9360eecf26 100644 --- a/packages/emcn/src/components/chip-dropdown/chip-dropdown.tsx +++ b/packages/emcn/src/components/chip-dropdown/chip-dropdown.tsx @@ -139,7 +139,7 @@ type ChipDropdownProps = ChipDropdownSingleProps | ChipDropdownMultiProps * * The trigger reuses `chipVariants` for visual parity with `Chip`. The label * is `flex-1`, so the trailing chevron is pushed flush right. The chevron is - * owned by the component and rendered at `h-[6px] w-[10px]` (matching the + * owned by the component and rendered at `size-[14px]` (matching the * workspace-header chevron) — there is intentionally no `rightIcon` prop. The * trigger and menu shell are identical across modes; only the selection * semantics (label, item handlers, open state, search) branch on `multiple`. @@ -318,7 +318,7 @@ const ChipDropdown = forwardRef( {displayLabel} - + diff --git a/packages/emcn/src/components/chip-select/chip-select.tsx b/packages/emcn/src/components/chip-select/chip-select.tsx index fd528fe6913..2602d07f678 100644 --- a/packages/emcn/src/components/chip-select/chip-select.tsx +++ b/packages/emcn/src/components/chip-select/chip-select.tsx @@ -240,7 +240,7 @@ export function ChipSelect({ aria-hidden className='inline-flex size-[16px] flex-shrink-0 items-center justify-center text-[var(--text-icon)]' > - + diff --git a/packages/emcn/src/components/chip/chip-chevron.tsx b/packages/emcn/src/components/chip/chip-chevron.tsx index aaafd4b0587..82c1fbd338b 100644 --- a/packages/emcn/src/components/chip/chip-chevron.tsx +++ b/packages/emcn/src/components/chip/chip-chevron.tsx @@ -8,7 +8,7 @@ interface ChipChevronDownProps { /** * Canonical trailing chevron adornment for chip-style dropdown triggers — a - * 16px hidden-from-AT slot centering the 10×6 {@link ChevronDown} glyph in + * 16px hidden-from-AT slot centering the 14px {@link ChevronDown} glyph in * `--text-icon`, matching the chevron `ChipDropdown` owns internally. Use it * inside hand-built `chipVariants` triggers (breadcrumb dropdowns, header * "New column"-style buttons) instead of re-deriving the span + icon markup. @@ -28,7 +28,7 @@ export function ChipChevronDown({ className }: ChipChevronDownProps) { className )} > - + ) } diff --git a/packages/emcn/src/components/dropdown-menu/dropdown-menu.tsx b/packages/emcn/src/components/dropdown-menu/dropdown-menu.tsx index 39bffd11bd8..220c61fb684 100644 --- a/packages/emcn/src/components/dropdown-menu/dropdown-menu.tsx +++ b/packages/emcn/src/components/dropdown-menu/dropdown-menu.tsx @@ -117,7 +117,7 @@ const DropdownMenuSubTrigger = React.forwardRef< {...props} > {children} - + ) }) diff --git a/packages/emcn/src/components/time-picker/time-picker.tsx b/packages/emcn/src/components/time-picker/time-picker.tsx index 8b6ccb0bfaa..7efcea48a3c 100644 --- a/packages/emcn/src/components/time-picker/time-picker.tsx +++ b/packages/emcn/src/components/time-picker/time-picker.tsx @@ -240,7 +240,7 @@ const TimePicker = React.forwardRef( diff --git a/packages/emcn/src/icons/chevron-down.tsx b/packages/emcn/src/icons/chevron-down.tsx index 7a85e52f3bd..ab99f55ed71 100644 --- a/packages/emcn/src/icons/chevron-down.tsx +++ b/packages/emcn/src/icons/chevron-down.tsx @@ -1,28 +1,25 @@ import type { SVGProps } from 'react' /** - * ChevronDown icon component + * ChevronDown icon component - single chevron pointing down * @param props - SVG properties including className, fill, etc. */ export function ChevronDown(props: SVGProps) { return ( ) } diff --git a/packages/emcn/src/icons/chevron-left.tsx b/packages/emcn/src/icons/chevron-left.tsx index f79b8280e7f..907b924f056 100644 --- a/packages/emcn/src/icons/chevron-left.tsx +++ b/packages/emcn/src/icons/chevron-left.tsx @@ -1,28 +1,25 @@ import type { SVGProps } from 'react' /** - * ChevronLeft icon component + * ChevronLeft icon component - single chevron pointing left * @param props - SVG properties including className, fill, etc. */ export function ChevronLeft(props: SVGProps) { return ( ) } diff --git a/packages/emcn/src/icons/chevron-right.tsx b/packages/emcn/src/icons/chevron-right.tsx index 2f2a591ae87..4197e20aaad 100644 --- a/packages/emcn/src/icons/chevron-right.tsx +++ b/packages/emcn/src/icons/chevron-right.tsx @@ -1,28 +1,25 @@ import type { SVGProps } from 'react' /** - * ChevronRight icon component + * ChevronRight icon component - single chevron pointing right * @param props - SVG properties including className, fill, etc. */ export function ChevronRight(props: SVGProps) { return ( ) } diff --git a/packages/emcn/src/icons/folder-input.tsx b/packages/emcn/src/icons/folder-input.tsx index 022eecc6cf7..1b15bd3371e 100644 --- a/packages/emcn/src/icons/folder-input.tsx +++ b/packages/emcn/src/icons/folder-input.tsx @@ -21,14 +21,14 @@ export function FolderInput(props: SVGProps) { From a19ae165e0f688cbf83967223e037c3b881678e2 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Tue, 4 Aug 2026 12:45:01 -0700 Subject: [PATCH 08/19] fix(landing): restore homepage view tracking (#6260) --- apps/sim/app/(landing)/landing.test.tsx | 36 +++++++++++++++++++++++++ apps/sim/app/(landing)/landing.tsx | 2 ++ 2 files changed, 38 insertions(+) create mode 100644 apps/sim/app/(landing)/landing.test.tsx diff --git a/apps/sim/app/(landing)/landing.test.tsx b/apps/sim/app/(landing)/landing.test.tsx new file mode 100644 index 00000000000..bfb21e0c574 --- /dev/null +++ b/apps/sim/app/(landing)/landing.test.tsx @@ -0,0 +1,36 @@ +/** + * @vitest-environment node + */ +import { renderToStaticMarkup } from 'react-dom/server' +import { describe, expect, it, vi } from 'vitest' + +vi.mock('@sim/emcn', () => ({ + cn: (...values: Array) => values.filter(Boolean).join(' '), +})) + +vi.mock('@/app/(landing)/components', () => ({ + Cta: () => null, + Features: () => null, + Hero: () => null, + HomeStructuredData: () => null, + Mothership: () => null, + ProductDemo: () => null, +})) + +vi.mock('@/app/(landing)/components/landing-layout', () => ({ + LANDING_SECTION_RHYTHM: 'gap-test', +})) + +vi.mock('@/app/(landing)/landing-analytics', () => ({ + LandingAnalytics: () => landing-analytics-marker, +})) + +import Landing from '@/app/(landing)/landing' + +describe('Landing', () => { + it('mounts the landing page analytics tracker exactly once', () => { + const markup = renderToStaticMarkup() + + expect(markup.match(/landing-analytics-marker/g)).toHaveLength(1) + }) +}) diff --git a/apps/sim/app/(landing)/landing.tsx b/apps/sim/app/(landing)/landing.tsx index 5eabaaaeb2c..84260a613ff 100644 --- a/apps/sim/app/(landing)/landing.tsx +++ b/apps/sim/app/(landing)/landing.tsx @@ -8,6 +8,7 @@ import { ProductDemo, } from '@/app/(landing)/components' import { LANDING_SECTION_RHYTHM } from '@/app/(landing)/components/landing-layout' +import { LandingAnalytics } from '@/app/(landing)/landing-analytics' /** * Landing page root - owns the section order and the `
` content region. @@ -26,6 +27,7 @@ import { LANDING_SECTION_RHYTHM } from '@/app/(landing)/components/landing-layou export default function Landing() { return (
+ From 39c3fe60d9289aa85adf80f6c8fb4cfe0c6f6f15 Mon Sep 17 00:00:00 2001 From: Waleed Date: Tue, 4 Aug 2026 12:45:10 -0700 Subject: [PATCH 09/19] improvement(docs): remove Ask AI, add the missing platform surfaces, align the sidebar (#6259) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * improvement(docs): remove Ask AI, add missing surfaces, align sidebar to the app - Removes the Ask AI widget, its /api/chat route, and the four deps exclusive to it (@ai-sdk/openai, @ai-sdk/react, ai, streamdown). lib/embeddings and docsEmbeddings stay — /api/search uses them. - Adds --surface-7 and --surface-hover, the last two platform surfaces docs lacked. - Aligns the sidebar with the app's canonical nav chrome: px-2.5 -> px-2, text-small -> text-sm, hover --surface-3 -> --surface-active, and an active hover of --surface-6, matching the Chip the app's sidebar items are built on. * fix(docs): make the sidebar hover CSS agree with the utilities Review caught that the sidebar hover alignment in this PR had no visual effect. `global.css` carries !important rules for both the link and button sidebar items — they exist to beat fumadocs' own styles — and they were still forcing the pre-alignment values: --surface-3 on an inactive hover, and --surface-active on an active hover. So the Tailwind utilities were dead on arrival. The global rules now carry the app's values instead (--surface-active inactive, --surface-6 active), matching the utilities rather than fighting them, with a comment noting the two must move together. --- apps/docs/app/[lang]/layout.tsx | 2 - apps/docs/app/api/chat/route.ts | 363 ------------------ apps/docs/app/global.css | 19 +- apps/docs/components/ai/ask-ai-panel.tsx | 234 ----------- apps/docs/components/ai/ask-ai.tsx | 57 --- .../docs-layout/sidebar-components.tsx | 18 +- apps/docs/package.json | 4 - bun.lock | 10 - 8 files changed, 20 insertions(+), 687 deletions(-) delete mode 100644 apps/docs/app/api/chat/route.ts delete mode 100644 apps/docs/components/ai/ask-ai-panel.tsx delete mode 100644 apps/docs/components/ai/ask-ai.tsx diff --git a/apps/docs/app/[lang]/layout.tsx b/apps/docs/app/[lang]/layout.tsx index 4fb73ecee32..f27d3187e69 100644 --- a/apps/docs/app/[lang]/layout.tsx +++ b/apps/docs/app/[lang]/layout.tsx @@ -3,7 +3,6 @@ import { defineI18nUI } from 'fumadocs-ui/i18n' import { DocsLayout } from 'fumadocs-ui/layouts/docs' import { RootProvider } from 'fumadocs-ui/provider/next' import { Geist_Mono, Inter } from 'next/font/google' -import { AskAI } from '@/components/ai/ask-ai' import { SidebarFolder, SidebarItem, @@ -124,7 +123,6 @@ export default async function Layout({ children, params }: LayoutProps) { {children}
- diff --git a/apps/docs/app/api/chat/route.ts b/apps/docs/app/api/chat/route.ts deleted file mode 100644 index 915fe9a39c4..00000000000 --- a/apps/docs/app/api/chat/route.ts +++ /dev/null @@ -1,363 +0,0 @@ -import { openai } from '@ai-sdk/openai' -import { - convertToModelMessages, - jsonSchema, - stepCountIs, - streamText, - tool, - type UIMessage, -} from 'ai' -import { sql } from 'drizzle-orm' -import { db, docsEmbeddings } from '@/lib/db' -import { generateSearchEmbedding } from '@/lib/embeddings' - -export const runtime = 'nodejs' -export const maxDuration = 30 - -/** Model used for the Ask AI chat. Override with OPENAI_CHAT_MODEL in the environment. */ -const CHAT_MODEL = process.env.OPENAI_CHAT_MODEL || 'gpt-5.4-mini' - -/** Max documentation chunks returned per search to ground an answer. */ -const SEARCH_LIMIT = 6 - -/** Candidates pulled before locale filtering, so a locale still yields SEARCH_LIMIT results. */ -const SEARCH_CANDIDATES = SEARCH_LIMIT * 4 - -/** Minimum cosine similarity for an English vector match (mirrors the site search route). */ -const SIMILARITY_THRESHOLD = 0.6 - -/** Locales the docs are published in (mirrors the site search route). */ -const KNOWN_LOCALES = ['en', 'es', 'fr', 'de', 'ja', 'zh'] -const DEFAULT_LOCALE = 'en' - -/** Postgres full-text config per locale (mirrors the site search route). */ -const TS_CONFIG: Record = { - en: 'english', - es: 'spanish', - fr: 'french', - de: 'german', - ja: 'simple', - zh: 'simple', -} - -/** - * Abuse guards. This endpoint proxies a paid LLM, so an unauthenticated public - * route is a target for scripted "free inference". These bounds cap the cost of - * any single request; an in-memory per-IP rate limit (below) caps volume on the - * hot path. A shared-store rate limit, a provider spend cap, and edge bot - * protection remain the durable controls (see the PR checklist). - * - * The size cap counts only user-authored text — NOT the conversation history, - * assistant turns, or retrieved doc chunks we add via the searchDocs tool, which - * legitimately grow large over a multi-turn chat. - */ -const MAX_MESSAGES = 200 -const MAX_USER_INPUT_CHARS = 400_000 -const MAX_OUTPUT_TOKENS = 4000 -const MAX_STEPS = 6 -/** Backstop on the sanitized model payload — bounds total LLM input (e.g. stuffed assistant text). */ -const MAX_TOTAL_CHARS = 1_000_000 - -/** - * Per-IP rate limit. Fixed window, in-memory: this bounds volume from a single - * source on a warm instance without external infra. It is best-effort on - * serverless (state is per-instance, not shared across regions/cold starts); - * a shared store (e.g. Vercel KV) and an edge WAF remain the durable controls, - * but this closes the "no volume limit at all" gap on the hot path. - */ -const RATE_LIMIT_MAX = 20 -const RATE_LIMIT_WINDOW_MS = 60_000 -const rateLimitHits = new Map() - -/** Resolve the client IP from forwarding headers, falling back to a shared bucket. */ -function getClientIp(req: Request): string { - const forwarded = req.headers.get('x-forwarded-for') - if (forwarded) return forwarded.split(',')[0].trim() - return req.headers.get('x-real-ip') ?? 'unknown' -} - -/** Fixed-window check. Returns retry-after seconds when the caller is over the limit, else null. */ -function rateLimit(ip: string, now: number): number | null { - const entry = rateLimitHits.get(ip) - if (!entry || now >= entry.resetAt) { - rateLimitHits.set(ip, { count: 1, resetAt: now + RATE_LIMIT_WINDOW_MS }) - return null - } - if (entry.count >= RATE_LIMIT_MAX) { - return Math.ceil((entry.resetAt - now) / 1000) - } - entry.count += 1 - return null -} - -/** Drop expired buckets so the Map doesn't grow unbounded on a long-lived instance. */ -function sweepRateLimit(now: number): void { - if (rateLimitHits.size < 10_000) return - for (const [ip, entry] of rateLimitHits) { - if (now >= entry.resetAt) rateLimitHits.delete(ip) - } -} - -/** A structurally valid UI message: has a role and a parts array. */ -function isValidMessage(message: unknown): message is UIMessage { - return ( - typeof message === 'object' && - message !== null && - typeof (message as { role?: unknown }).role === 'string' && - Array.isArray((message as { parts?: unknown }).parts) - ) -} - -/** Total length of user-authored text across the conversation. */ -function userInputChars(messages: UIMessage[]): number { - let total = 0 - for (const message of messages) { - if (message.role !== 'user') continue - for (const part of message.parts) { - if (part.type === 'text' && typeof part.text === 'string') total += part.text.length - } - } - return total -} - -/** - * Strip everything the model shouldn't trust from client-supplied history: - * drop `system` messages (client-injected instructions) and every non-text part - * (e.g. crafted tool results faking searchDocs output). Only user/assistant text - * survives, so grounding comes from the server-run searchDocs tool — not the - * client's payload. - */ -function sanitizeMessages(messages: UIMessage[]): UIMessage[] { - return messages - .filter((message) => message.role === 'user' || message.role === 'assistant') - .map((message) => ({ - ...message, - parts: message.parts.filter((part) => part.type === 'text' && typeof part.text === 'string'), - })) - .filter((message) => message.parts.length > 0) -} - -/** - * Reject obvious cross-origin calls. Same-origin browser requests send an - * `Origin` header matching the host; we allow those, plus any host in - * DOCS_ALLOWED_ORIGINS (comma-separated). Requests with no Origin (e.g. curl) - * are allowed through to the cost caps rather than blocked, since Origin is - * trivially spoofable and is a filter, not a security boundary. - */ -function isAllowedOrigin(req: Request): boolean { - const origin = req.headers.get('origin') - if (!origin) return true - - let originHost: string - try { - originHost = new URL(origin).host.toLowerCase() - } catch { - return false - } - - const forwardedHost = req.headers.get('x-forwarded-host') ?? req.headers.get('host') - const requestHost = forwardedHost?.split(',')[0].trim().toLowerCase() - if (requestHost && originHost === requestHost) return true - - const allowlist = (process.env.DOCS_ALLOWED_ORIGINS ?? '') - .split(',') - .map((value) => value.trim().toLowerCase()) - .filter(Boolean) - return allowlist.includes(originHost) -} - -const SYSTEM_PROMPT = `You are the documentation assistant for Sim — the open-source AI workspace where teams build, deploy, and manage AI agents. - -Answer questions about Sim using the documentation. Always call the searchDocs tool before answering anything specific about Sim's features, configuration, or usage — do not answer from memory. Base your answer only on the returned documentation; if the docs do not cover the question, say so plainly rather than guessing. - -Guidelines: -- Be direct and concrete. Lead with the answer, then the detail. -- Reference the relevant pages by their titles so the user knows where to read more. -- When you show configuration or code, keep it minimal and correct. -- The agent is called "Sim" and the chat surface is "Chat" — never say "Mothership" or "copilot". -- If a question is unrelated to Sim, briefly say it's outside the docs' scope.` - -const SEARCH_COLUMNS = { - chunkId: docsEmbeddings.chunkId, - title: docsEmbeddings.headerText, - url: docsEmbeddings.sourceLink, - content: docsEmbeddings.chunkText, - sourceDocument: docsEmbeddings.sourceDocument, -} - -/** Reciprocal-rank-fusion constant, matching the site search route. */ -const RRF_K = 60 - -/** - * SQL predicate selecting only the locale's documents, so the row limit applies - * to matching rows: non-English docs are prefixed with their locale segment; - * English is everything not prefixed with another locale. - */ -function localeFilter(locale: string) { - const firstSegment = sql`split_part(${docsEmbeddings.sourceDocument}, '/', 1)` - if (locale === DEFAULT_LOCALE) { - const others = KNOWN_LOCALES.filter((l) => l !== DEFAULT_LOCALE) - return sql`${firstSegment} not in (${sql.join( - others.map((l) => sql`${l}`), - sql`, ` - )})` - } - return sql`${firstSegment} = ${locale}` -} - -type SearchRow = { - chunkId: string - title: string - url: string - content: string - sourceDocument: string -} - -/** - * Retrieve candidate chunks for grounding, mirroring the site search route's - * hybrid strategy: Postgres full-text keyword search for every locale, plus - * vector similarity (thresholded) for English — fused by reciprocal rank so a - * page found by either signal can ground the answer. - */ -async function searchDocs(query: string, locale: string) { - const tsConfig = TS_CONFIG[locale] ?? 'simple' - - // Each retrieval path is best-effort and independent: a failure in one still - // lets the other ground the answer (both empty just yields no grounding). - let keywordRows: SearchRow[] = [] - try { - keywordRows = await db - .select(SEARCH_COLUMNS) - .from(docsEmbeddings) - .where( - sql`${docsEmbeddings.chunkTextTsv} @@ plainto_tsquery(${tsConfig}, ${query}) and ${localeFilter(locale)}` - ) - .orderBy( - sql`ts_rank(${docsEmbeddings.chunkTextTsv}, plainto_tsquery(${tsConfig}, ${query})) DESC` - ) - .limit(SEARCH_CANDIDATES) - } catch (error) { - console.error('Ask AI keyword search failed:', error) - } - - let vectorRows: SearchRow[] = [] - if (locale === DEFAULT_LOCALE) { - // Vector retrieval (embedding call + pgvector query) is best-effort: if it - // fails, fall back to the keyword rows already fetched rather than losing all - // grounding for the turn. - try { - const embedding = await generateSearchEmbedding(query) - const vectorLiteral = JSON.stringify(embedding) - vectorRows = await db - .select(SEARCH_COLUMNS) - .from(docsEmbeddings) - .where( - sql`1 - (${docsEmbeddings.embedding} <=> ${vectorLiteral}::vector) >= ${SIMILARITY_THRESHOLD} and ${localeFilter(locale)}` - ) - .orderBy(sql`${docsEmbeddings.embedding} <=> ${vectorLiteral}::vector`) - .limit(SEARCH_CANDIDATES) - } catch (error) { - console.error('Ask AI vector search failed; using keyword results only:', error) - } - } - - // Reciprocal rank fusion across the two rankings, deduped by chunk. - const scores = new Map() - const rowById = new Map() - for (const list of [vectorRows, keywordRows]) { - list.forEach((row, index) => { - scores.set(row.chunkId, (scores.get(row.chunkId) ?? 0) + 1 / (RRF_K + index + 1)) - if (!rowById.has(row.chunkId)) rowById.set(row.chunkId, row) - }) - } - - return [...rowById.values()] - .sort((a, b) => (scores.get(b.chunkId) ?? 0) - (scores.get(a.chunkId) ?? 0)) - .slice(0, SEARCH_LIMIT) - .map((row) => ({ - title: row.title, - url: row.url, - content: row.content, - })) -} - -export async function POST(req: Request) { - if (!isAllowedOrigin(req)) { - return new Response('Forbidden', { status: 403 }) - } - - const now = Date.now() - sweepRateLimit(now) - const retryAfter = rateLimit(getClientIp(req), now) - if (retryAfter !== null) { - return new Response('Too many requests', { - status: 429, - headers: { 'Retry-After': String(retryAfter) }, - }) - } - - let body: { messages: UIMessage[]; locale?: string } - try { - body = await req.json() - } catch { - return new Response('Invalid JSON', { status: 400 }) - } - const { messages } = body - const locale = KNOWN_LOCALES.includes(body.locale ?? '') - ? (body.locale as string) - : DEFAULT_LOCALE - - if (!Array.isArray(messages) || messages.length === 0 || messages.length > MAX_MESSAGES) { - return new Response('Invalid request', { status: 400 }) - } - if (!messages.every(isValidMessage)) { - return new Response('Invalid request', { status: 400 }) - } - if (userInputChars(messages) > MAX_USER_INPUT_CHARS) { - return new Response('Request too large', { status: 413 }) - } - - const modelMessages = sanitizeMessages(messages) - if (modelMessages.length === 0) { - return new Response('Invalid request', { status: 400 }) - } - // Bound what actually reaches the model. Measured AFTER sanitization, so the - // prior searchDocs tool outputs that accumulate in client history (and are - // stripped here) don't count — only user/assistant text the model will see. - if (JSON.stringify(modelMessages).length > MAX_TOTAL_CHARS) { - return new Response('Request too large', { status: 413 }) - } - - const result = streamText({ - model: openai(CHAT_MODEL), - system: SYSTEM_PROMPT, - messages: convertToModelMessages(modelMessages), - stopWhen: stepCountIs(MAX_STEPS), - maxOutputTokens: MAX_OUTPUT_TOKENS, - tools: { - searchDocs: tool({ - description: - 'Search the Sim documentation for relevant content. Use this before answering any question about Sim.', - /** - * The SDK's own schema helper instead of a zod schema: the `ai` - * package's zod-v4 typings lag the workspace zod version, so a zod - * object here fails the tool() overloads whenever the two drift. - */ - inputSchema: jsonSchema<{ query: string }>({ - type: 'object', - properties: { - query: { - type: 'string', - description: 'A focused natural-language search query.', - }, - }, - required: ['query'], - additionalProperties: false, - }), - execute: async ({ query }) => searchDocs(query, locale), - }), - }, - }) - - return result.toUIMessageStreamResponse() -} diff --git a/apps/docs/app/global.css b/apps/docs/app/global.css index d7709776139..1047524cc3d 100644 --- a/apps/docs/app/global.css +++ b/apps/docs/app/global.css @@ -31,11 +31,6 @@ body { --text-md: 16px; } -/* Hide the floating Ask Sim widget while a workflow-preview lightbox is open. */ -body.wp-lightbox-open [aria-label="Ask Sim"] { - display: none; -} - /* Pure white light mode background */ :root:not(.dark) { --color-fd-background: hsl(0, 0%, 100%) !important; @@ -64,7 +59,9 @@ body.wp-lightbox-open [aria-label="Ask Sim"] { --surface-4: #f5f5f5; --surface-5: #f3f3f3; --surface-6: #e5e5e5; + --surface-7: #d9d9d9; --surface-active: #ececec; + --surface-hover: #f2f2f2; --border: #d8d8d8; /** Legacy neutral-border aliases. New work should use --border. */ --border-1: var(--border); @@ -133,7 +130,9 @@ body.wp-lightbox-open [aria-label="Ask Sim"] { --surface-4: #292929; --surface-5: #363636; --surface-6: #454545; + --surface-7: #505050; --surface-active: #2c2c2c; + --surface-hover: #262626; --border: #444444; /** Legacy neutral-border aliases. New work should use --border. */ --border-1: var(--border); @@ -463,16 +462,18 @@ html #nd-sidebar button[data-active="true"] { color: var(--text-body) !important; } -/* Hover state — platform --surface-3 */ +/* Hover state — the app's nav Chip hovers to --surface-active. These rules must + carry !important to beat fumadocs' own sidebar styles, which also means they + override the Tailwind hover utilities on the items; keep the two in step. */ html #nd-sidebar a:not(:has(span.font-mono)):hover:not([data-active="true"]), html #nd-sidebar button:hover:not([data-active="true"]) { - background-color: var(--surface-3) !important; + background-color: var(--surface-active) !important; } -/* Active items don't change on hover */ +/* An active item darkens one surface on hover, matching the app's active Chip */ html #nd-sidebar a[data-active="true"]:not(:has(span.font-mono)):hover, html #nd-sidebar button[data-active="true"]:hover { - background-color: var(--surface-active) !important; + background-color: var(--surface-6) !important; color: var(--text-body) !important; } diff --git a/apps/docs/components/ai/ask-ai-panel.tsx b/apps/docs/components/ai/ask-ai-panel.tsx deleted file mode 100644 index c55b05dca0c..00000000000 --- a/apps/docs/components/ai/ask-ai-panel.tsx +++ /dev/null @@ -1,234 +0,0 @@ -'use client' - -import { type FormEvent, useEffect, useMemo, useRef, useState } from 'react' -import { useChat } from '@ai-sdk/react' -import { ArrowUp, Square, Task, X } from '@sim/emcn/icons' -import { DefaultChatTransport } from 'ai' -import { Streamdown } from 'streamdown' -import { cn } from '@/lib/utils' -import 'streamdown/styles.css' - -interface DocSource { - title: string - url: string -} - -/** Pull the deduped doc sources surfaced by the searchDocs tool out of a message's parts. */ -function getSources(parts: ReadonlyArray<{ type: string; [key: string]: unknown }>): DocSource[] { - const seen = new Set() - const sources: DocSource[] = [] - - for (const part of parts) { - if (part.type !== 'tool-searchDocs') continue - const output = (part as { output?: unknown }).output - if (!Array.isArray(output)) continue - for (const item of output as DocSource[]) { - if (!item?.url || seen.has(item.url)) continue - seen.add(item.url) - sources.push({ title: item.title, url: item.url }) - } - } - - return sources -} - -/** Concatenate the streamed text parts of a message. */ -function getText(parts: ReadonlyArray<{ type: string; [key: string]: unknown }>): string { - return parts - .filter((part) => part.type === 'text') - .map((part) => (part as unknown as { text: string }).text) - .join('') -} - -interface AskAIPanelProps { - /** Active docs locale, forwarded so retrieval is scoped to the reader's language. */ - locale: string - open: boolean - onClose: () => void -} - -export function AskAIPanel({ locale, open, onClose }: AskAIPanelProps) { - const [input, setInput] = useState('') - const scrollRef = useRef(null) - const textareaRef = useRef(null) - - const transport = useMemo(() => new DefaultChatTransport({ api: '/api/chat' }), []) - - const { messages, sendMessage, status, stop, error } = useChat({ transport }) - - const isBusy = status === 'submitted' || status === 'streaming' - - const handleClose = () => { - stop() - onClose() - } - - useEffect(() => { - if (!open) return - textareaRef.current?.focus() - const handleKeyDown = (event: KeyboardEvent) => { - if (event.key === 'Escape') { - handleClose() - } - } - document.addEventListener('keydown', handleKeyDown) - return () => document.removeEventListener('keydown', handleKeyDown) - }, [open]) - - useEffect(() => { - if (!open) return - scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight }) - }, [open]) - - useEffect(() => { - scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight, behavior: 'smooth' }) - }, [messages]) - - const handleSubmit = (event: FormEvent) => { - event.preventDefault() - const text = input.trim() - if (!text || isBusy) return - sendMessage({ text }, { body: { locale } }) - setInput('') - } - - if (!open) return null - - return ( -
-
- - - Ask Sim - - -
- -
- {messages.length === 0 && ( -

- Ask anything about building, deploying, and managing AI agents in Sim. -

- )} - - {messages.map((message, index) => { - const text = getText(message.parts) - const isStreaming = isBusy && index === messages.length - 1 - const sources = message.role === 'assistant' ? getSources(message.parts) : [] - return ( -
- {message.role === 'user' ? ( -
- {text} -
- ) : ( -
- {text ? ( - - {text} - - ) : isStreaming ? ( - '…' - ) : sources.length === 0 ? ( - No answer returned. - ) : null} -
- )} - {sources.length > 0 && ( -
- {sources.map((source) => ( - - {source.title || source.url} - - ))} -
- )} -
- ) - })} - - {error && ( -

- Something went wrong. Please try again. -

- )} -
- - -
-