From 69e3e177ac767b32f92be3ce155a9626505c8708 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Thu, 6 Aug 2026 22:07:44 -0700 Subject: [PATCH 1/8] feat(library): Best AI Agent Builder in 2026: Sim Leads for Open-Source, Self-Hostable Teams (#6369) Co-authored-by: Sim Pi Agent --- .../best-ai-agent-builder-2026/index.mdx | 127 ++++++++++++++++++ .../best-ai-agent-builder-2026/cover.jpg | Bin 0 -> 30845 bytes 2 files changed, 127 insertions(+) create mode 100644 apps/sim/content/library/best-ai-agent-builder-2026/index.mdx create mode 100644 apps/sim/public/library/best-ai-agent-builder-2026/cover.jpg diff --git a/apps/sim/content/library/best-ai-agent-builder-2026/index.mdx b/apps/sim/content/library/best-ai-agent-builder-2026/index.mdx new file mode 100644 index 00000000000..9fd992f24df --- /dev/null +++ b/apps/sim/content/library/best-ai-agent-builder-2026/index.mdx @@ -0,0 +1,127 @@ +--- +slug: best-ai-agent-builder-2026 +title: 'Best AI Agent Builder in 2026: Sim Leads for Open-Source, Self-Hostable Teams' +description: 'Compare the best AI agent builders in 2026 across open-source licensing, self-hosting, build modes, integrations, deployment options, and pricing.' +date: 2026-08-01 +updated: 2026-08-01 +authors: + - andrew +readingTime: 9 +tags: [AI Agents, Agent Builders, Open Source, Self-Hosting, Comparison, Sim] +ogImage: /library/best-ai-agent-builder-2026/cover.jpg +canonical: https://www.sim.ai/library/best-ai-agent-builder-2026 +draft: false +faq: + - q: "What is the best open-source AI agent platform?" + a: "Sim is a strong choice for technical builders who want a permissive license, visual and programmatic building, and the option to run agents on their own infrastructure. Its core is available under Apache 2.0." + - q: "Is Sim really open source?" + a: "Yes. Sim's core repository uses the OSI-approved Apache 2.0 license, which permits commercial use, modification, and distribution and includes an express patent grant." + - q: "What is the best AI agent builder for developers?" + a: "Sim fits developers who want visual workflows without giving up APIs, custom logic, or infrastructure choice. A workflow can be built through natural language, a visual canvas, or an API and then published for other applications to use." + - q: "Can I self-host an AI agent platform for free?" + a: "Yes. Sim's open-source core can be self-hosted without a software seat fee. You remain responsible for infrastructure and any model or external service costs." + - q: "Is Sim better than n8n or Zapier?" + a: "Sim is the better fit when permissive licensing, agent-native context, and infrastructure control are priorities. n8n may fit engineering-led deterministic automation, while Zapier may fit teams that prioritize guided SaaS automation." + - q: "Which AI agent platform fits enterprise security reviews?" + a: "The right platform depends on your organization's controls. Sim offers enterprise deployment and governance options, while its open-source core gives technical teams source visibility and infrastructure choice." +--- + +*Last updated: August 2026* + +## TL;DR + +- [Sim](https://www.sim.ai/) is the best AI agent builder for technical teams that want an [Apache 2.0](https://github.com/simstudioai/sim), self-hostable workspace. Its public repository shows its current GitHub community, while Sim reports a community of more than 100,000 builders. +- [n8n suits engineers who want self-hosted workflow automation](https://docs.n8n.io/hosting/) under its [fair-code Sustainable Use License](https://docs.n8n.io/sustainable-use-license/). +- [Zapier suits users who prioritize guided setup and a large app catalog](https://zapier.com/apps), and whose workloads fit its [task-based plans](https://zapier.com/pricing). +- [Make suits operations users building visual automation scenarios](https://www.make.com/en), while [Make AI Agents](https://www.make.com/en/ai-agents) remains a separate, evolving product surface. +- [Gumloop offers a managed, no-code AI automation builder](https://www.gumloop.com/) for users who do not need to operate the platform's infrastructure. +- The deciding factor is architecture. Sim began as an agent workspace, while the other four grew from workflow automation products or managed automation builders. + +For a wider market survey, see this comparison of the [best AI agent platforms in 2026](https://www.sim.ai/library/best-ai-agent-platforms-2026). + +## The best AI agent builder for technical, self-hosting teams + +Sim is the best AI agent builder for technical builders and teams that prioritize open-source licensing and self-hosting control. Sim's [core repository](https://github.com/simstudioai/sim) uses the permissive Apache 2.0 license, displays the project's current star count, and includes self-hosting resources. [Sim reports](https://www.sim.ai/) that more than 100,000 builders use the platform. + +[Mothership](https://docs.sim.ai/mothership) lets users create and operate workspace resources through natural language. The [Sim workspace](https://sim.ai) includes native Tables, Files, and Knowledge Bases, along with [1,000+ integrations](https://www.sim.ai/integrations) and support for [major model providers](https://www.sim.ai/models). As documented in the [Sim product documentation](https://docs.sim.ai/), users can publish workflows for API access, hosted chat, and use by MCP clients. + +Sim fits technical builders at startups and enterprise teams that want to limit vendor lock-in through source access and infrastructure choice. If you mainly need simple SaaS automation and do not plan to self-host, a managed automation platform with guided onboarding may suit you better. + +## Why AI-native architecture beats retrofitted automation + +Every platform in this comparison now advertises AI features: [Zapier Agents](https://zapier.com/agents), [Make AI Agents](https://www.make.com/en/ai-agents), and [n8n's AI workflow tools](https://n8n.io/ai/) sit alongside their established automation products. The difference is when those features arrived relative to each product's foundation. + +Zapier, Make, and n8n became known for workflows in which a trigger starts a configured sequence of steps. That model works well for moving records between SaaS tools. Agents behave differently: they may select tools based on context, carry relevant state into later steps, and produce variable outputs. Adding a model step to a workflow does not, by itself, redesign the surrounding engine around agent behavior. + +Sim started from the agent use case. That shows up in three concrete places: + +- **Context lives in the workspace, not only in glue code.** Tables, Files, and Knowledge Bases are native Sim workspace resources. With general automation platforms, teams often assemble retrieval and storage from connectors such as [n8n integrations](https://n8n.io/integrations/) or [Zapier apps](https://zapier.com/apps). +- **Three build modes cover the whole team.** [Mothership](https://docs.sim.ai/mothership) handles natural-language interaction, the visual canvas handles precise logic, and the API supports programmatic workflows. Teams can choose a mode that matches the task rather than forcing every contributor into the same interface. +- **Workflows deploy as callable tools.** The [Sim documentation](https://docs.sim.ai/) covers publishing workflows for applications and MCP clients, allowing external AI assistants to call them directly. + +The short version: most platforms made AI a feature of an automation product. Sim made agents the foundation and built workspace capabilities around them. + +## Apache 2.0 vs fair-code: what the license actually changes + +"Open source" gets used loosely in this category, so the distinction matters for legal review. For more licensing context, compare the leading [open-source AI agent platforms](https://www.sim.ai/library/open-source-ai-agent-platforms). + +Sim's core is licensed under [Apache 2.0](https://github.com/simstudioai/sim/blob/main/LICENSE), an OSI-approved license. Its terms permit commercial use, modification, and redistribution, and include an express patent grant from contributors. That patent language can matter to enterprise legal teams reviewing the rights attached to software they plan to operate or modify. + +n8n uses a [Sustainable Use License and Enterprise License](https://docs.n8n.io/sustainable-use-license/), which n8n describes as fair-code rather than OSI open source. The published terms restrict some commercial uses, including offering functionality that substantially derives its value from n8n. Teams using n8n internally may find those terms workable, while teams embedding automation into a commercial product should have counsel review the exact use case. A deeper [n8n alternatives comparison](https://www.sim.ai/library/n8n-alternatives) covers the practical tradeoffs. + +Zapier, Make, and Gumloop present their products as vendor-operated services through their respective [Zapier plans](https://zapier.com/pricing), [Make plans](https://www.make.com/en/pricing), and [Gumloop plans](https://www.gumloop.com/pricing). Teams that require source modification or operation of the full platform on their own infrastructure should evaluate those constraints before adopting one. + +## Enterprise requirements + +Open-source adoption often reaches a security and governance review before production. Sim's [pricing and enterprise materials](https://www.sim.ai/pricing) describe options intended for those reviews: + +- **SSO and SAML** for identity provider integration on supported enterprise deployments +- **SOC 2 Type II** and security review materials for vendor assessment +- **BYOK (bring your own key)** options for teams that want to use their own model-provider credentials +- **Access controls** for governing workspace and workflow operations +- **Programmatic administration** for teams integrating provisioning and deployment into internal processes +- **Workspace branding, import, and export** options for managed environments +- **Self-hosting choices** for organizations that need greater control over network and data boundaries + +Exact controls and deployment responsibilities should be confirmed with Sim for the selected plan. Teams comparing credential strategies can also read this guide to a [BYOK multi-model AI agent builder](https://www.sim.ai/library/byok-multi-model-ai-agent-builder). + +## How Sim compares on the criteria that matter + +The ranking weighs ownership, building flexibility, production use, and cost because those factors determine whether a platform fits your technical requirements and operating model. + +- **License and self-hosting** determine whether you can inspect, modify, and run the software on your own infrastructure. +- **Build modes** show whether you can create agents through natural language, a visual editor, code, or a combination. +- **Integrations and models** measure how readily agents can use your existing tools, data, and preferred model providers. +- **Deployment surfaces** define how you can publish a finished agent, such as through an API, chat interface, or callable tool. +- **Pricing model** reveals whether costs depend on seats, tasks, executions, operations, platform credits, model usage, or infrastructure you operate. + +## Ranked alternatives: n8n, Zapier, Make, and Gumloop + +Each alternative serves a distinct use case. Your license requirements, technical skill level, and need for infrastructure control may change the order. + +1. **n8n is the strongest alternative for developer-controlled automation.** Its [workflow editor and nodes](https://docs.n8n.io/workflows/) give builders control over triggers, branches, and execution paths, and n8n maintains [self-hosting documentation](https://docs.n8n.io/hosting/). The principal tradeoff against Sim is its [fair-code licensing](https://docs.n8n.io/sustainable-use-license/) rather than Apache 2.0. Sim also places Tables, Files, and Knowledge Bases in the agent workspace, whereas n8n emphasizes workflows and integrations. Choose n8n if engineering-led workflow automation is the main job and your legal team accepts its terms. +2. **Zapier emphasizes connector breadth and guided onboarding.** Its [app directory](https://zapier.com/apps) covers a broad catalog of SaaS products, while its [plans meter tasks](https://zapier.com/pricing). That means teams should model task volume before committing. [Zapier Agents](https://zapier.com/agents) extends the company's hosted automation ecosystem with agent building, but teams that require source access or self-hosting should look elsewhere. See the broader guide to the [best Zapier alternatives](https://www.sim.ai/library/best-zapier-alternatives) for additional options. +3. **Make gives operations users detailed visual control over multi-step automations.** Its [visual automation platform](https://www.make.com/en) exposes mappings, filters, branches, and execution routes, while [Make AI Agents](https://www.make.com/en/ai-agents) adds agent-oriented building. Choose Make if visual scenario design and Make's [connector ecosystem](https://www.make.com/en/integrations) are primary requirements, and validate the current AI Agents release status before using it for a production-critical system. +4. **Gumloop is a managed, no-code approach to AI-focused automation.** Its [platform](https://www.gumloop.com/) assembles workflows around models and business tools, and its [published subscriptions](https://www.gumloop.com/pricing) use platform credits. Choose Gumloop if speed to a managed workflow matters more than operating or modifying the underlying platform. + +## Comparison table + +| Tool | License and hosting | Build modes | Integrations | Deployment surfaces | Pricing model | Best-fit buyer | +| --- | --- | --- | --- | --- | --- | --- | +| Sim | [Apache 2.0](https://github.com/simstudioai/sim/blob/main/LICENSE); hosted or self-hosted | [Mothership](https://docs.sim.ai/mothership), visual canvas, API | [1,000+ integrations](https://www.sim.ai/integrations) and [major model providers](https://www.sim.ai/models) | [API, hosted chat, and MCP use](https://docs.sim.ai/) | [Free and per-seat hosted plans](https://www.sim.ai/pricing); open-source self-hosting | Technical builders wanting open-source ownership and native workspace context | +| n8n | [Fair-code](https://docs.n8n.io/sustainable-use-license/); [cloud or self-hosted](https://docs.n8n.io/hosting/) | [Visual workflows and code](https://docs.n8n.io/workflows/) | [Nodes and integrations](https://n8n.io/integrations/) | Cloud or self-hosted workflows | [Execution-based paid plans](https://n8n.io/pricing/) and a self-hosted community edition | Engineers building controlled workflow automation | +| Zapier | [Vendor-operated cloud service](https://zapier.com/pricing) | [Zaps and Agents](https://zapier.com/agents) | [App catalog](https://zapier.com/apps) | [Hosted automations](https://zapier.com/) | [Task-based plans](https://zapier.com/pricing) | Users automating SaaS tasks | +| Make | [Vendor-operated cloud service](https://www.make.com/en/pricing) | [Visual scenarios and AI Agents](https://www.make.com/en/ai-agents) | [App connectors](https://www.make.com/en/integrations) | [Hosted scenarios](https://www.make.com/en) | [Credit-based plans](https://www.make.com/en/pricing) | Operations users building multi-step automations | +| Gumloop | [Managed cloud service](https://www.gumloop.com/pricing) | [No-code AI workflow builder](https://www.gumloop.com/) | [Managed app and data connectors](https://www.gumloop.com/) | [Hosted workflows](https://www.gumloop.com/) | [Credit-based subscriptions](https://www.gumloop.com/pricing) | Users wanting managed AI automation without infrastructure operations | + +## Pricing and self-hosting + +Sim offers two separate paths, and it is worth being precise about the difference because they are often confused. + +**The open-source core can be self-hosted without a Sim seat fee.** Clone the [Apache 2.0 repository](https://github.com/simstudioai/sim) and follow its current deployment instructions. Your infrastructure, model usage, and external services remain your responsibility; the open-source software itself is not metered as a hosted Sim plan. + +**The hosted plans are separate.** The current [Sim pricing page](https://www.sim.ai/pricing) lists Free, Pro, Max, and Enterprise options with their current prices, credits, and included features. Enterprise self-hosting refers to supported enterprise deployment rather than a requirement to buy a contract before using the open-source repository. Because prices and allowances can change, use the pricing page for a current cost model rather than relying on a static comparison. + +## Getting started with Sim + +An open-source, self-hostable agent workspace gives you control over where your agents run and how the underlying code evolves. Start building with the hosted product at [sim.ai](https://sim.ai), or clone the [Apache 2.0 repository](https://github.com/simstudioai/sim) to run Sim on your own infrastructure. diff --git a/apps/sim/public/library/best-ai-agent-builder-2026/cover.jpg b/apps/sim/public/library/best-ai-agent-builder-2026/cover.jpg new file mode 100644 index 0000000000000000000000000000000000000000..dcce413a75b2269c0041a214735063e2b5a779c1 GIT binary patch literal 30845 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 literal 0 HcmV?d00001 From e6a17b03e98485dad1902b691c05e5acf71feeb2 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Thu, 6 Aug 2026 22:59:17 -0700 Subject: [PATCH 2/8] improvement(tables): show a tooltip on truncated column headers (#6371) * improvement(tables): show a tooltip on truncated column headers * chore(tables): use absolute import for HeaderLabel --- .../table-grid/headers/column-header-menu.tsx | 15 ++++++----- .../table-grid/headers/header-label.tsx | 27 +++++++++++++++++++ .../headers/workflow-group-meta-cell.tsx | 3 ++- 3 files changed, 38 insertions(+), 7 deletions(-) create mode 100644 apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/headers/header-label.tsx diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/headers/column-header-menu.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/headers/column-header-menu.tsx index e2834ab3c7d..d18e69cbc14 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/headers/column-header-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/headers/column-header-menu.tsx @@ -4,6 +4,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react' import { cn } from '@sim/emcn' import { ChevronDown } from '@sim/emcn/icons' import type { WorkflowGroup } from '@/lib/table' +import { HeaderLabel } from '@/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/headers/header-label' import type { WorkflowMetadata } from '@/stores/workflows/registry/types' import { COL_WIDTH, SELECTION_TINT_BG } from '../constants' import type { ColumnSourceInfo, DisplayColumn } from '../types' @@ -296,9 +297,10 @@ export const ColumnHeaderMenu = React.memo(function ColumnHeaderMenu({ blockIconInfo={sourceInfo?.blockIconInfo} blockMissing={blockMissing} /> - - {column.workflowGroupId ? column.headerLabel : column.name} - + ) : (
@@ -314,9 +316,10 @@ export const ColumnHeaderMenu = React.memo(function ColumnHeaderMenu({ blockIconInfo={sourceInfo?.blockIconInfo} blockMissing={blockMissing} /> - - {column.workflowGroupId ? column.headerLabel : column.name} - +
@@ -412,7 +418,7 @@ function MappingEntry({ controller, group, entry }: MappingEntryProps) { ) : null} {entry.candidatesTruncated ? (

- More options than shown — search by name. + Too many targets to list them all — search covers only the ones shown.

) : null} From d2964af7d21d36f70b8cf0d1b3559181ce91c4c9 Mon Sep 17 00:00:00 2001 From: Waleed Date: Fri, 7 Aug 2026 11:29:20 -0700 Subject: [PATCH 8/8] fix(chat): re-measure the prompt editor when its width changes (#6380) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(chat): re-measure the prompt editor when its width changes The chat input's textarea grows to its full content height under a mirror overlay, but it only re-measured on text change. A width change after typing (window resize, sidebar toggle, resource panel opening) left the textarea at a stale inline height while the overlay rewrapped taller. The spilled lines still painted and scrolled but had no textarea beneath them, so clicks landed on the scroller and never placed a caret. Re-measure on width change only — the measure writes the textarea's height, so reacting to height would feed itself. * fix(chat): measure the observer's first delivery like any other The width can change between the mount-time measure and observe(), so treating the first notification as confirmation of the mount width dropped that change and left the stale height in place. * chore(chat): trim duplicated comments on the prompt editor autosize The failure mode was documented in four places. Keeps one canonical explanation next to the guard and leaves only the per-test whys the test names do not already carry. --- .../prompt-editor/prompt-editor.test.tsx | 213 ++++++++++++++++++ .../prompt-editor/prompt-editor.tsx | 35 ++- 2 files changed, 246 insertions(+), 2 deletions(-) create mode 100644 apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/prompt-editor/prompt-editor.test.tsx diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/prompt-editor/prompt-editor.test.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/prompt-editor/prompt-editor.test.tsx new file mode 100644 index 00000000000..5b7fc95ee03 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/prompt-editor/prompt-editor.test.tsx @@ -0,0 +1,213 @@ +/** + * @vitest-environment jsdom + */ +import { act } from 'react' +import { createRoot, type Root } from 'react-dom/client' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +vi.mock('@/hooks/queries/skills', () => ({ useSkills: () => ({ data: [] }) })) +vi.mock('@/hooks/queries/mcp', () => ({ useMcpServers: () => ({ data: [] }) })) +vi.mock('@/blocks/integration-matcher', () => ({ + getIntegrationMatcher: () => ({ regex: null, byName: new Map() }), +})) +vi.mock( + '@/app/workspace/[workspaceId]/home/components/user-input/components/plus-menu-dropdown/plus-menu-dropdown', + () => ({ PlusMenuDropdown: () => null }) +) +vi.mock( + '@/app/workspace/[workspaceId]/home/components/user-input/components/skills-menu-dropdown/skills-menu-dropdown', + () => ({ SkillsMenuDropdown: () => null }) +) + +import { PromptEditor } from '@/app/workspace/[workspaceId]/home/components/user-input/components/prompt-editor/prompt-editor' +import { usePromptEditor } from '@/app/workspace/[workspaceId]/home/components/user-input/components/prompt-editor/use-prompt-editor' + +/** + * jsdom performs no layout, so the autosize inputs are stubbed: `editorWidth` + * stands for the scroller's content width and `contentHeight` for the height + * the text wraps to at that width. Narrowing raises the content height, exactly + * as rewrapping does in a browser. + */ +let contentHeight = 240 +let editorWidth = 700 +let autosizeCalls = 0 + +/** + * `observe` only registers the target: real deliveries, including the initial + * one, are asynchronous, so tests drive them explicitly. Delivering inside + * `observe` would hide the window between the mount-time measure and the first + * notification — exactly where a width change can be missed. + */ +class FakeResizeObserver implements ResizeObserver { + private static instances: FakeResizeObserver[] = [] + private readonly callback: ResizeObserverCallback + private targets: Element[] = [] + + constructor(callback: ResizeObserverCallback) { + this.callback = callback + FakeResizeObserver.instances.push(this) + } + + observe(target: Element) { + this.targets.push(target) + } + + unobserve(target: Element) { + this.targets = this.targets.filter((t) => t !== target) + } + + disconnect() { + this.targets = [] + FakeResizeObserver.instances = FakeResizeObserver.instances.filter((i) => i !== this) + } + + deliver() { + const entries = this.targets.map( + (target) => ({ target, contentRect: { width: editorWidth } }) as ResizeObserverEntry + ) + if (entries.length > 0) this.callback(entries, this) + } + + static reset() { + FakeResizeObserver.instances = [] + } + + static observerCount() { + return FakeResizeObserver.instances.length + } + + static deliverAll() { + for (const instance of [...FakeResizeObserver.instances]) instance.deliver() + } +} + +function mountEditor() { + ;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true + const container = document.createElement('div') + document.body.appendChild(container) + const root: Root = createRoot(container) + + function Probe() { + const editor = usePromptEditor({ workspaceId: 'ws-1', initialValue: 'a long prompt' }) + return + } + + act(() => root.render()) + + const textarea = container.querySelector('textarea') + if (!textarea) throw new Error('textarea did not render') + + return { + textarea, + unmount: () => { + act(() => root.unmount()) + container.remove() + }, + } +} + +function resizeTo(width: number, wrappedHeight: number) { + editorWidth = width + contentHeight = wrappedHeight + act(() => FakeResizeObserver.deliverAll()) +} + +/** + * Delivers the observer's initial notification at the mounted width, putting the + * editor in the steady state a test can then resize away from. + */ +function settle() { + act(() => FakeResizeObserver.deliverAll()) +} + +describe('PromptEditor autosize', () => { + let originalScrollHeight: PropertyDescriptor | undefined + + beforeEach(() => { + contentHeight = 240 + editorWidth = 700 + autosizeCalls = 0 + FakeResizeObserver.reset() + + originalScrollHeight = Object.getOwnPropertyDescriptor(Element.prototype, 'scrollHeight') + Object.defineProperty(Element.prototype, 'scrollHeight', { + configurable: true, + get() { + if (!(this instanceof HTMLTextAreaElement)) return 0 + autosizeCalls++ + return contentHeight + }, + }) + vi.stubGlobal('ResizeObserver', FakeResizeObserver) + }) + + afterEach(() => { + if (originalScrollHeight) { + Object.defineProperty(Element.prototype, 'scrollHeight', originalScrollHeight) + } + vi.unstubAllGlobals() + }) + + it('sizes the textarea to its content height on mount', () => { + const { textarea, unmount } = mountEditor() + + expect(textarea.style.height).toBe('240px') + unmount() + }) + + it('re-measures when the editor width changes so no text falls outside the textarea', () => { + const { textarea, unmount } = mountEditor() + settle() + expect(textarea.style.height).toBe('240px') + + resizeTo(340, 500) + + expect(textarea.style.height).toBe('500px') + unmount() + }) + + it('re-measures again when the editor widens back', () => { + const { textarea, unmount } = mountEditor() + settle() + + resizeTo(340, 500) + resizeTo(700, 240) + + expect(textarea.style.height).toBe('240px') + unmount() + }) + + /** Distinct from the case above: here the width moves before any delivery lands. */ + it('re-measures on the first delivery when the width changed before it arrived', () => { + const { textarea, unmount } = mountEditor() + expect(textarea.style.height).toBe('240px') + + resizeTo(340, 500) + + expect(textarea.style.height).toBe('500px') + unmount() + }) + + /** The height `autosize` writes re-notifies this observer, so this guard breaks the loop. */ + it('ignores resize notifications that do not change the width', () => { + const { textarea, unmount } = mountEditor() + settle() + const callsAfterSettle = autosizeCalls + + contentHeight = 500 + act(() => FakeResizeObserver.deliverAll()) + + expect(textarea.style.height).toBe('240px') + expect(autosizeCalls).toBe(callsAfterSettle) + unmount() + }) + + it('stops observing after unmount', () => { + const { unmount } = mountEditor() + expect(FakeResizeObserver.observerCount()).toBe(1) + + unmount() + + expect(FakeResizeObserver.observerCount()).toBe(0) + }) +}) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/prompt-editor/prompt-editor.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/prompt-editor/prompt-editor.tsx index db58cdcf461..af7013cb84b 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/prompt-editor/prompt-editor.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/prompt-editor/prompt-editor.tsx @@ -88,7 +88,7 @@ export function PromptEditor({ * container, letting the browser clamp a bottom-pinned transcript upward by * the input's grown height on every multi-line edit. */ - useLayoutEffect(() => { + const autosize = useCallback(() => { const textarea = textareaRef.current if (!textarea) return const scroller = scrollerRef.current @@ -96,7 +96,38 @@ export function PromptEditor({ textarea.style.height = 'auto' textarea.style.height = `${textarea.scrollHeight}px` if (scroller) scroller.style.height = '' - }, [value, textareaRef]) + }, [textareaRef]) + + useLayoutEffect(() => { + autosize() + }, [value, autosize]) + + /** + * The textarea carries an inline pixel height, so a width change (window + * resize, sidebar toggle, chat column reflow) rewraps the text taller while + * the box stays at its old height. The mirror overlay paints the full text + * regardless, so the spilled lines render over the scroller with no textarea + * beneath them — visible, scrollable text that swallows clicks instead of + * placing the caret. + * + * Only width is compared: `autosize` writes the textarea's height, which + * re-notifies this observer, so reacting to height would feed itself. The + * first delivery is measured like any other — the width can change between + * the mount-time measure and `observe()`. + */ + useEffect(() => { + const scroller = scrollerRef.current + if (!scroller) return + let lastWidth: number | null = null + const observer = new ResizeObserver(([entry]) => { + const width = entry.contentRect.width + if (width === lastWidth) return + lastWidth = width + autosize() + }) + observer.observe(scroller) + return () => observer.disconnect() + }, [autosize]) useEffect(() => { if (autoFocus && !readOnly) editor.focusAtEnd()