Commit Graph
5416 Commits
Author SHA1 Message Date
Vikhyath Mondreti 2d41360807 improvement(concurrency): limits configurable, docs updates (#5640)
* improvement(concurrency): limits configurable, docs updates

* remove dead tests

* limits self hosted vars
2026-07-13 13:11:05 -07:00
Waleed e94e514c0a feat(flint): add Flint integration with agent task tools, block, and docs (#5641)
* feat(flint): add Flint integration with agent task tools, block, and docs

* fix(flint): forward explicit publish=false, guard missing taskId, align default params branch with tool fallback

* docs(flint): add manual intro section to integration docs page

* fix(flint): fail get_task on OK responses without a task ID

* fix(flint): drop json-object generation type so the wand emits the pages array
2026-07-13 13:06:47 -07:00
Waleed 04535ee33b feat(buffer): add Buffer integration with posts, channels, and ideas (#5637)
* feat(buffer): add Buffer integration with posts, channels, and ideas

* fix(buffer): return clear tool error when account lookup yields no account

* fix(buffer): default schedulingType server-side so basic-mode blocks never fail validation

* fix(buffer): probe Content-Type for extensionless media URLs so videos are not sent as images

* feat(buffer): add get_ideas and get_idea_groups tools, harden media URL classification

* fix(buffer): guard missing post on PostActionSuccess responses
2026-07-13 12:07:37 -07:00
Waleed 836ceda3a7 fix(landing): complete Organization schema, add CollectionPage/BlogPosting JSON-LD, fix TechArticle rich-result eligibility (#5638)
* fix(landing): complete Organization schema, add CollectionPage/BlogPosting JSON-LD, fix TechArticle rich-result eligibility

- Organization schema (site-structured-data.tsx): add brand and
  contactPoint.url (verified against the real /contact route); all other
  fields were already correct and verified against the Footer's sameAs
  links. foundingDate/legalName/address omitted — not verifiable from
  anything in this repo.
- CollectionPage (blog + library index): buildCollectionPageJsonLd now
  takes the real posts list (same getAllPostMeta() the index page already
  renders from) and emits a mainEntity ItemList of BlogPosting stubs
  instead of omitting mainEntity entirely.
- BlogPosting + TechArticle (post detail template): Google's Article
  rich-result eligibility only recognizes Article/NewsArticle/BlogPosting
  — a bare TechArticle type isn't in that allowlist. buildArticleJsonLd
  now emits a multi-type @type array (["BlogPosting","TechArticle"]) for
  genuinely technical posts, and "BlogPosting" alone for posts that are
  general announcements, via a new `technical` frontmatter flag (defaults
  true; set to false on the series-a funding-announcement post, the one
  post with no technical content). Also fixed a real markup/schema
  mismatch: the article's `speakable.cssSelector` referenced
  `[itemprop="description"]`, but no element carried that itemProp —
  added it to the description paragraph. TechArticle/BlogPosting image
  URLs are now made absolute (previously relative paths, invalid for
  crawlers).
- FAQPage: audited every LandingFAQ usage (/models, /models/[provider],
  /models/[provider]/[model], /integrations, /integrations/[slug],
  /comparison, /comparison/[provider]) — all already emit matching
  FAQPage JSON-LD sourced from the same data passed to LandingFAQ; no
  gaps found, no changes needed.

* fix(landing): match microdata itemType and scope collection JSON-LD to visible posts

Address Greptile/Cursor review on PR #5638:
- itemType now always resolves to BlogPosting (matching Greptile's exact
  suggested fix), since the JSON-LD graph already carries the richer
  TechArticle type via a multi-type array and the microdata path doesn't
  need to duplicate that distinction
- buildCollectionPageJsonLd now receives the same tag-filtered/paginated
  post subset ContentIndexPage actually renders, instead of the full
  unfiltered catalog, via a new shared selectVisiblePosts/paginateContentPosts
  helper in lib/content/index-list.ts (also de-duplicates the pagination
  logic that previously lived only in ContentIndexPage)

* fix(landing): match CollectionPage ItemList order and url to the visible filtered/paginated variant

Address round-2 Cursor Bugbot findings on PR #5638:
- buildCollectionPageJsonLd no longer re-sorts the given posts by date -
  ordering is now solely owned by the caller (selectVisiblePosts), so
  featured-row-first render order matches ItemList position order
- buildCollectionPageJsonLd now takes an optional {tag, page} filter
  descriptor and reflects it in the emitted url, instead of always
  pointing at the bare section index regardless of which filtered/
  paginated variant's posts are actually listed in mainEntity
2026-07-13 11:55:39 -07:00
Waleed 0640c0bbac improvement(hubspot): align tools with API docs and expand coverage with delete, list membership, and search tools (#5635) 2026-07-13 11:30:18 -07:00
Waleed b1bd2b5cab feat(gong): align tools with official API spec + 4 new tools (#5632)
* feat(gong): align tools with official API spec, add ask-anything, brief, unassign, and logs tools

- fix gong_list_flows: query param was flowEmailOwner (typo copied from Gong's endpoint prose); the API requires flowOwnerEmail, so every call failed
- create_call: drop phantom url output (API returns only requestId/callId) and make downloadMediaUrl optional per spec
- list_scorecards: refresh to current spec (numeric IDs, questionType/answerGuide/minRange/maxRange/answerOptions, reviewMethod)
- answered_scorecards: map selectedOptions on answers, correct score range description (1-50)
- surface requestId uniformly across all read tools; totalRecords no longer fabricated from page size
- normalize includeAvatars to a strict boolean param, uppercase aggregationPeriod, encode userId path param
- validate email/phone format before irreversible GDPR purge calls
- new tools: gong_ask_anything, gong_get_brief (AI entity Q&A/briefs), gong_unassign_flow_prospects, gong_get_logs

* fix(gong): require custom-range dates and gate param remapping by operation

- ask_anything/get_brief: entityFromDateTime/entityToDateTime now conditionally required (UI) and validated tool-side when timePeriod is CUSTOM_RANGE
- block params mapper: remaps are gated by the selected operation so stale values from previously configured operations can no longer overwrite fromDateTime/fromDate/workspaceId

* fix(gong): final spec-alignment pass, review fixes, and regenerated docs

- ask_anything/get_brief: send fromDateTime/toDateTime only for CUSTOM_RANGE; declare mcpResult brief section field
- unassign: dedicated optional unassignFlowId subblock so a stale assign-flow ID can never silently narrow an unassign to one flow
- get_logs: logType is a closed enum (AccessLog, UserActivityLog, UserCallPlay, ExternallySharedCallAccess, ExternallySharedCallPlay) - dropdown in block, enumerated in tool description
- get_folder_content: folderId optional per spec; get_call: encode callId path param
- list_trackers: drop saidInCallParts (absent from the spec's KeywordTracker schema)
- get_extensive_calls: correct declared interactionStats item shape to {name, value}
- block inputs: list all remapped subblock params; optional flags on nullable outputs; absolute types re-export
- regenerate Gong integration docs and integrations.json entries (29 operations)
2026-07-13 11:30:05 -07:00
Waleed 0d9f8607c1 feat(library): add n8n alternatives, LangGraph alternatives, and open-source AI agent platform listings (#5633)
* feat(library): add n8n alternatives, LangGraph alternatives, and open-source AI agent platform listings

* fix(content): declare actual OG image dimensions instead of hardcoded 1200x630
2026-07-13 11:24:56 -07:00
Waleed 8ae353828c fix(landing): fix oversized HTML and severe LCP on integration/comparison pages (#5634)
* fix(landing): fix oversized HTML and severe LCP on integration/comparison pages

Google Search Console flagged /integrations, /integrations/slack, and
several others for exceeding Googlebot's 2MB uncompressed-HTML crawl
limit, plus severe LCP on /integrations, /integrations/slack,
/integrations/hubspot, /comparison/flowise, and /integrations/hugging-face.

Root cause 1 - /integrations/slack was 5.2MB (measured on production).
The "Agent templates" section renders every template matching
getTemplatesForBlock(type) with no cap - Slack is referenced as
alsoIntegrations in 445 templates (vs 52-126 for Salesforce/Gmail/
HubSpot), so its detail page embedded hundreds of full template cards
in the initial HTML/RSC payload. Capped to 12, consistent with the
same file's existing related-integrations cap of 4.

Root cause 2 - /integrations was 1.83MB, right at the limit. The client
IntegrationGrid component receives the full Integration[] as props
(needed for instant client-side search), which serializes every
integration's complete operations/triggers arrays - including full
per-operation description sentences - into the initial payload purely
to build a search index. Added IntegrationSummary + toIntegrationSummary
(lib/integrations): the same searchable surface (name, description,
operation names, trigger names) precomputed server-side into one
lowercased string, dropping the full operations/triggers data the
client never actually renders. Measured: 627KB -> 142KB of embedded
integration data (77% reduction).

Verified via a real production build + curl:
- /integrations: 1.83MB -> 1.31MB
- /integrations/slack: 5.2MB -> 355KB (93% reduction)
- /integrations/hubspot: 901KB -> 452KB

Verified via Lighthouse (mobile, devtools throttling, matching what
real users on a typical device experience) against both live production
and the fixed local build:
- /integrations: 47 -> 96 (LCP unmeasured->2.1s, TBT 2,770ms->110ms)
- /integrations/slack: 47 -> 96 (LCP 9.7s -> 1.9s)
- /integrations/hubspot: 72 -> 97 (LCP 9.1s -> 1.9s)
- /integrations/hugging-face: 72 -> 95 (LCP 9.2s -> 2.3s, reproduced
  twice on production before fixing - not a fluke)
- /comparison/flowise: 71 -> 95 (LCP 9.8s -> 2.1s)

The last two aren't Slack-style template-count outliers (4 and 0
alsoIntegrations references respectively) - shrinking the shared
IntegrationGrid client bundle appears to have reduced a shared chunk
loaded broadly across landing pages, benefiting pages beyond the ones
directly touched.

Also checked and confirmed already healthy, no action needed:
/integrations/salesforce, /integrations/gmail, /integrations/amazon-dynamodb,
/comparison/tines, /models/xai/grok-4-20-multi-agent-0309.

* fix(landing): restore full-fidelity search and template priority

Two real regressions from the previous commit, both confirmed by
Greptile and Cursor Bugbot independently:

- lib/integrations: IntegrationSummary.searchText joined every field
  into one string, so (a) it dropped operation/trigger descriptions
  entirely (a search for API-specific terms that only appear in an
  operation's description, not its name, silently stopped matching),
  and (b) a single joined string lets a query span a field boundary
  (e.g. matching across the tail of a name and the head of the next
  field) that the original per-field search never allowed. Reverted to
  a searchFields array - same content as the original per-field index
  (name, description, every operation's name+description, every
  trigger's name), just precomputed server-side instead of shipping
  the full Integration objects. Grid filtering goes back to
  `.some(field => field.includes(q))`, matching the exact original
  matching semantics.
- blocks/registry.ts + integrations/[slug]/page.tsx: capping
  getTemplatesForBlock's result with a plain .slice(0, 12) kept
  whatever 12 templates happened to iterate first in registry
  insertion order - for a high-connectivity integration like Slack,
  that can be entirely alsoIntegrations matches from unrelated blocks,
  silently dropping the integration's own owned templates and any
  marked featured. Added an explicit isOwner flag to
  ScopedBlockTemplate (the registry function already computes this
  internally, just wasn't surfacing it) and sort owner-first,
  featured-second before slicing.

* fix(landing): sort featured templates ahead of owned, not just as a tiebreaker

The previous sort (owner-first, featured as a tiebreaker within each
owner tier) meant an integration with 12+ owned templates filled the
entire cap with non-featured owned templates before any featured
related template (reached via alsoIntegrations) was ever considered -
Slack hits this case. Swapped the sort so featured (owned or related)
ranks first, then owned-but-not-featured, so a curated featured
template can no longer be sliced away by a pile of ordinary owned ones.
2026-07-13 11:23:56 -07:00
Waleed 4b3c688fec fix(landing): fix 404ing OG images for every model and integration page (#5636)
Google Search Console flagged every /models/{provider}/{model}/opengraph-image
and /integrations/{slug}/opengraph-image URL as 404 (~230 pages total: 5
sample model pages plus ~130 more models, plus every integration).

Root cause: the sibling page.tsx for each of these routes sets
`dynamicParams = false`, a segment-level restriction that also blocks
the metadata route (opengraph-image.tsx) from rendering any param
combination it wasn't statically told about - but Next does not share
generateStaticParams between a page and its sibling metadata routes.
Since none of the three opengraph-image.tsx files exported their own
generateStaticParams, every param was "unknown" to that restriction and
404d, for every single model and integration.

Added a matching generateStaticParams to all three files, mirroring
each route's own page.tsx.

Note: the exact URLs in the audit report (bare /opengraph-image, no
suffix) aren't the real ones - Next serves these at a build-generated
hash suffix (e.g. /opengraph-image-15dal5?<hash>), which is what's
actually embedded in each page's <meta property="og:image"> tag. The
underlying bug the report surfaced is real regardless; verified by
requesting the actual hash-suffixed URL each page embeds (previously
404, now 200) for both a model and an integration page, on a real
production build.
2026-07-13 11:21:59 -07:00
Theodore Li f9cc73f534 fix(slack): stop requesting unapproved OAuth scopes that break connect (#5631) 2026-07-13 13:31:09 -04:00
Waleed fc25cfb3c8 fix(docs): fix Core Web Vitals regressions on docs.sim.ai (#5630)
* fix(docs): fix Core Web Vitals regressions on docs.sim.ai

Empirically measured under real trace-based (devtools) CPU/network
throttling against the live site: mobile Performance 59, LCP 9.2s
(TTFB 745ms + 8.4s element render delay).

- sidebar-components.tsx / [lang]/layout.tsx: the docs sidebar renders
  every page in the doc tree as a link at once. Next's default
  viewport-prefetch fired an RSC payload fetch for every one of them on
  initial load - dozens of concurrent requests competing with the page's
  own content for bandwidth. Wired fumadocs' documented `sidebar.prefetch`
  option through to the custom SidebarItem/SidebarFolder components (which
  were bypassing it entirely, using next/link directly with no prefetch
  prop) via the `useSidebar()` context hook.
- video.tsx: `autoPlay` forces browsers to fetch the full video file
  immediately on mount regardless of `preload`. Gated actual src loading
  behind an IntersectionObserver so a page with several of these doesn't
  pull down every video up front (5MB across 3 requests, in this case).
  Single shared component - fixes every doc page that embeds one.
- proxy.ts: the i18n middleware matcher excluded favicon/robots.txt/etc
  but not `icon.svg`, so every request for it got routed through i18n
  negotiation instead of served as a static file, 404ing in production.
- next.config.ts: enable productionBrowserSourceMaps - safe since this
  repo's source is already fully public, real debuggability benefit,
  zero performance cost.
- shiki 4.0.0 -> 4.3.1 (verified: syntax highlighting still renders
  correctly). Attempted a coordinated fumadocs-core/ui/mdx/openapi
  upgrade to latest; fumadocs-openapi's v11 factory function became
  client-only (breaking change beyond its declared peer deps, requiring
  a component-boundary restructure), so only the safe, verified,
  docs-exclusive bumps (fumadocs-core/ui/mdx, shiki) are included here -
  the openapi major bump needs its own dedicated migration PR.

Verified via a real production build (dummy env, all 3974 pages
including API reference render/build cleanly) and a clean (non-stale)
local server: Performance 59 -> 71 measured under real devtools
throttling, RSC prefetch requests 63 -> 11, video requests/bytes 3/5MB
-> 0. A pre-existing React hydration warning (#418) was found and
confirmed present on live production before any of these changes,
unrelated to this diff - documented, not blocking.

* fix(docs): fall back to eager video loading without IntersectionObserver

The lazy-load gate from the previous commit threw before isInView could
ever become true in environments lacking IntersectionObserver (older
browsers, some embedded webviews), leaving videos permanently
source-less instead of falling back to eager loading.

* chore(docs): drop non-TSDoc inline comments

Repo convention is TSDoc-only, no plain // comments.

* fix(docs): accessibility and SEO defects across the docs app

Audited with parallel subagents against the accessibility and SEO skill
checklists, each fix verified by reading the actual code (not assumed):

Accessibility:
- lightbox.tsx: focus was never captured/restored on close, and Tab
  escaped the modal to the page behind it (no focus trap on the single
  focusable element)
- heading.tsx: the per-heading copy-link icon only appeared on hover,
  invisible to keyboard-only navigation (added peer-focus-visible)
- navbar.tsx: active nav tab had no aria-current
- response-section.tsx: the status-code dropdown had no
  aria-haspopup/aria-expanded/role, and no Escape-to-close
- workflow-preview.tsx: same focus-trap gap as lightbox.tsx on the
  expanded-canvas modal

SEO:
- page.tsx: generateMetadata's hreflang/canonical URLs used a naive
  String.replace to strip the locale prefix, which also matched "/en"
  inside unrelated slugs (platform/enterprise, integrations/enrich,
  platform/self-hosting/environment-variables), corrupting those pages'
  canonical and alternate-language URLs. Replaced with a prefix-only strip.
- structured-data.tsx: the SoftwareApplication JSON-LD block compared
  url === baseUrl (no trailing slash) against the homepage's actual url
  (always has a trailing slash), so the condition was always false and
  this structured data never rendered anywhere, including the homepage.
- structured-data.tsx: "Mothership" in the indexed featureList violated
  the constitution's required language (the agent is "Sim", the surface
  is "Chat") - this ships in JSON-LD search engines parse.

* fix(docs): defer the Ask Sim chat widget's heavy deps until opened

The chat panel (useChat from @ai-sdk/react, Streamdown + its CSS) was
mounted unconditionally in the root layout on every single page, so
its full weight loaded and executed even though the widget starts
closed on every page view.

Traced via the LCP breakdown insight under real devtools CPU/network
throttling: the LCP text element (the intro paragraph) had a ~8s
element render delay despite a ~13ms TTFB, and bootup-time attributed
~4.3s of scripting time to a single chunk containing React/ReactDOM's
own runtime plus this widget's eagerly-bundled dependencies.

Split into a lightweight ask-ai.tsx (just the toggle button + open
state) and ask-ai-panel.tsx (the actual chat UI, useChat, Streamdown),
loaded via next/dynamic(..., { ssr: false }) only when the user opens
the widget. Verified: the panel's chunk now has zero network requests
on initial page load.

Measured (mobile, devtools throttling, /introduction):
- Performance: 69 -> 75
- LCP: 8.0s -> 6.4s
- TBT: 260ms -> 130ms

The remaining ~6.4s LCP delay traces to the same shared chunk, now
identified as core React/ReactDOM hydration cost for this page's
sidebar/TOC/breadcrumb tree rather than an isolated bug - a real,
larger initiative (hydration architecture, not a surgical fix),
documented here rather than rushed.

* fix(docs): preserve Ask Sim chat state across close/reopen

The panel split unmounted AskAIPanel entirely on close, discarding
useChat's message state - reopening always started an empty
conversation, unlike the original single-component layout where
useChat lived in a component that never unmounted.

Fixed by keeping the panel mounted (via a hasOpened flag that never
resets) once first opened, and having the panel itself return null
when closed rather than being conditionally removed from the tree by
its parent - hooks still run every render, so useChat's state persists
across visibility toggles. The dynamic import still only fires on the
first open, so the initial-load win is unchanged.

Verified via a real click-through (open, type, close, reopen): input
persists correctly, and the panel chunk still has zero network
requests on initial page load. Performance unchanged at 75.

* chore(docs): lint fixes (import order, formatting)

* fix(docs): fill the Ask Sim UI gap while the panel chunk loads

handleOpen set open=true synchronously, hiding the trigger button
before the dynamically imported panel had a chance to render anything
(next/dynamic renders null by default with no loading option) - on a
slow connection neither the button nor the panel was visible.

Added a loading fallback in the same fixed position so there's no gap
between the button disappearing and the real panel appearing.
2026-07-13 09:10:33 -07:00
Vikhyath Mondreti ef7c8e24b2 feat(platform): settings permissions, admin, billing attribution (#5545)
* fix(invites): preserve active organization for external access

Keep organization activation server-owned so failed membership checks cannot clear a valid session context.

* feat(admin, billing, settings): cleanup settings visibility, billing actor resolution, new admin routes

* address comments

* chore(db): reset pending migrations before staging merge

Remove locally generated migrations so they can be regenerated against the latest staging schema without preserving stale snapshots or numbering.

* regen migrations

* address comments

* chore(db): reset generated migrations before staging merge

Remove this branch's generated migrations so they can be regenerated against the latest staging schema with fresh numbering.

* upgrade global work

* fix lint

* address comments

* legacy callbacks correctness

* address comments

* update

* guardrail attribution
2026-07-13 00:53:49 -07:00
Waleed 73f33dba9d chore(bunfig): restore minimumReleaseAge supply-chain gate with scoped excludes (#5523) 2026-07-12 12:57:08 -07:00
faccc6113d fix(skills): quote argument-hint YAML values so Copilot CLI ≥1.0.65 loads all skills (#5627)
* v0.6.29: login improvements, posthog telemetry (#4026)

* feat(posthog): Add tracking on mothership abort (#4023)

Co-authored-by: Theodore Li <theo@sim.ai>

* fix(login): fix captcha headers for manual login  (#4025)

* fix(signup): fix turnstile key loading

* fix(login): fix captcha header passing

* Catch user already exists, remove login form captcha

* fix: quote argument-hint YAML values so Copilot CLI ≥1.0.65 loads all skills

`argument-hint: [foo]` YAML-parses as a flow sequence (array), not a
string. Downstream slash-command loaders that validate `argument-hint`
as a string — notably GitHub Copilot CLI ≥ 1.0.65 — silently reject
the skill on load, and the command disappears from the CLI menu.

Wrap the value in double quotes so it parses as a string. No behaviour
change on Claude Code.

---------

Co-authored-by: Waleed <walif6@gmail.com>
Co-authored-by: Theodore Li <theodoreqili@gmail.com>
Co-authored-by: Siddharth Ganesan <33737564+Sg312@users.noreply.github.com>
Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com>
Co-authored-by: Theodore Li <theo@sim.ai>
2026-07-12 12:49:29 -07:00
Waleed 5e891da819 fix(editor): allow references in boolean variable values and sync tag selections to canvas preview (#5628) 2026-07-12 12:42:59 -07:00
Waleed 29cf3e7d9f fix(ashby): fail loudly instead of silently dropping malformed socialLinks (#5624)
* fix(ashby): fail loudly instead of silently dropping malformed socialLinks

parseSocialLinksInput returned [] for any non-JSON-parseable input, and
tools.config.params only sets result.socialLinks when the parsed array is
non-empty — so a malformed socialLinks string (user typo, or a wand
response that didn't follow the JSON-array prompt) silently omitted the
field entirely. The Ashby candidate.update call then succeeded without
applying the requested links, with no error surfaced to the workflow
author. Throw a clear error instead, matching the existing
throw-on-invalid-JSON pattern used elsewhere (e.g. blocks/airtable.ts).

* fix(ashby): use getErrorMessage instead of inline error-message extraction

check:utils bans the e instanceof Error ? e.message : fallback pattern in
favor of getErrorMessage(e, fallback?) from @sim/utils/errors.
2026-07-11 22:10:16 -07:00
Waleed b20bbdc111 fix(global-commands): use isContentEditable for the editable guard (#5623)
* fix(global-commands): use isContentEditable for the editable guard

* chore(lint): keep focusable span in editable-guard test with biome-ignore
2026-07-11 22:07:16 -07:00
Waleed 5dec4f3168 fix(ashby): parse alternateEmailAddresses and socialLinks into arrays before dispatch (#5621)
* fix(ashby): parse alternateEmailAddresses and socialLinks into arrays before dispatch

The Ashby create_candidate and update_candidate tools require
alternateEmailAddresses (string[]) and socialLinks ({type,url}[]) as
JSON arrays in the request body, guarded by Array.isArray checks. The
block collected both through long-input text fields but forwarded the
raw string straight through to tools.config.params, so Array.isArray
was always false and both fields were silently dropped on every
create/update call.

Parse them in tools.config.params (execution-time, after variable
resolution) using the same comma-separated-or-JSON-array pattern used
elsewhere in the codebase (see blocks/findymail.ts), and add wandConfig
to both fields so the AI wand can generate well-formed input for them.

* fix(ashby): drop json-object generationType from array-shaped wandConfig fields

generationType: 'json-object' makes the wand API append an instruction
that the response must start with { and end with }, but
alternateEmailAddresses/socialLinks parse a raw JSON array or
comma-separated string, not an object. A wand-generated
{"emails":[...]}-shaped response would get comma-split into invalid
email fragments by parseStringListInput, and an object-wrapped
socialLinks response would get silently dropped by parseSocialLinksInput
returning []. Matches the existing array-field wandConfig pattern in
blocks/findymail.ts, which never sets generationType and relies on the
prompt text alone.

* fix(ashby): remove the non-functional candidateId filter from list_applications

Live-verified against Ashby's application.list endpoint: passing
candidateId (including a nonexistent UUID) returns identical, unfiltered
results either way — Ashby's API silently ignores this body field
entirely. Sending it gave users the false impression of filtering by
candidate while actually returning every application. Removed the param,
the tool type, and the block's filterCandidateId subBlock/wiring/input.
The correct path for a candidate's applications is ashby_get_candidate's
applicationIds field.

* fix(ashby): add subblock-id migration for the removed filterCandidateId field

The subblock ID stability CI check correctly caught that removing
filterCandidateId without a migration entry would silently drop the
value on already-deployed workflows. Added the standard _removed_ mapping,
following the same pattern already used for this block's prior removals
(emailType, phoneType, expandApplicationFormDefinition,
expandSurveyFormDefinitions).
2026-07-11 21:50:30 -07:00
Waleed f9b09ecfd4 fix(webhooks): resolve env var references before deploy-triggered subscription creation (#5619)
* fix(webhooks): resolve env var references before deploy-triggered subscription creation

Provider config fields like an API key can reference an environment
variable via {{VAR_NAME}}. The interactive trigger-save route already
resolved these before calling a provider's createSubscription, but the
async deployment-outbox path (workflow deploy -> saveTriggerWebhooksForDeploy
-> createExternalWebhookSubscription) did not, so the literal unresolved
{{VAR_NAME}} string was sent to the provider as the credential and
rejected. Resolve env vars in createExternalWebhookSubscription itself so
both callers behave the same; the persisted providerConfig keeps storing
the unresolved template, only the outbound call gets the resolved value.

* fix(webhooks): guard against a non-string workspaceId when resolving env vars

workflow.workspaceId as string | undefined was an unchecked cast on a
Record<string, unknown> — if a caller ever passed a workflow-like object
where workspaceId isn't actually a string, workspace-scoped {{VAR}}
references would silently stay unresolved and the provider would receive
the literal template as the credential, reproducing the exact class of bug
this change exists to fix. Replaced with a runtime typeof check that falls
back to undefined (personal-env-only resolution) instead of forwarding an
unvalidated value.
2026-07-11 21:36:53 -07:00
Waleed 1fe94d3c7a feat(sidebar): add Cmd+B shortcut to toggle sidebar collapse (#5618)
* feat(sidebar): add Cmd+B shortcut to toggle sidebar collapse

* fix(sidebar): skip Cmd+B sidebar toggle inside editable fields

* fix(sidebar): don't dead-zone Cmd+B on read-only editors; drop stale Mod+B ownership

* chore(lint): apply biome fixes
2026-07-11 21:31:58 -07:00
Waleed f477faab4b fix(rich-markdown-editor): make drag-reorder of an image actually move it, on real browser payloads (#5617)
* fix(rich-markdown-editor): make drag-reorder of an image actually move it, on real browser payloads

Reported on latest staging (deploy verified via CodePipeline): dragging an image duplicates it
instead of moving it, and a click with a few px of hand jitter — which the draggable <img> turns
into a native drag+drop-on-self — destroys the selection and duplicates too, reading as "I can't
select this image anymore". Reproduced in real Chromium with real mouse input (micro-drag becomes
dragstart, never click) and with the real drag payload shape.

Two compounding root causes, both empirically pinned:
- TipTap's node-view dragstart bypasses ProseMirror's drag serialization entirely (verified in
  @tiptap/core source: onDragStart only sets a drag image and NodeSelects the node — no PM
  text/html, no view.dragging). What the drop actually carries is the BROWSER's native enrichment:
  an image File plus text/html whose <img src> is the ABSOLUTE rendered URL.
- Both hosted-image recognizers (extractEmbeddedFileRef and isInlineRouteSrc) reject absolute
  URLs, so the #5573 skip-check never matched on real drags: the drop fell into the upload branch
  (duplicate; original never moves). Falling through to PM instead would be no better: with
  view.dragging unset its default drop PARSES the html into a copy — persisting the display-layer
  src that share/export tracking don't recognize — and never deletes the original.

Fix, at the mechanism level:
- Normalize clipboard/dataTransfer srcs origin-relative before comparing (toSameOriginPath),
  keyed off window.location.origin deliberately rather than getBaseUrl(): the browser serializes
  against the origin the page is ACTUALLY viewed on, which legitimately diverges from the
  configured NEXT_PUBLIC_APP_URL (localhost dev, previews, apex-vs-www). Cross-origin srcs are
  never treated as ours. Applied to isInlineRouteSrc, hasHostedImageHtml, and findHostedImageAttrs
  (the paste-clone path had the same absolute-URL gap for browser-native "Copy Image").
- handleDrop performs the internal move itself when the drop's html references the
  currently-selected image node (htmlReferencesSrc — TipTap's dragstart guarantees that selection):
  same delete → map → insert shape as ProseMirror's own move, ending NodeSelected. Drop-on-self is
  a no-op that keeps the ring — which is what a jittery click now resolves to.

Empirical before/after (real-Chromium harness driving the real editor + engine): pre-fix the drag
leaves the original in place and uploads a duplicate; post-fix the node moves exactly once, nothing
uploads, and the moved image stays selected. Paste-clone verified for both relative (PM copy) and
absolute (native Copy Image) payloads. 604 unit tests pass including 11 new ones for the
origin-aware helpers.

* fix(rich-markdown-editor): no-op invalid drop points, match external-image identity by absolute URL

Greptile round 1, both real:
- dropPoint can return null (no valid insertion point); the raw coords.pos fallback could make
  tr.insert throw — PM's own null-fallback is only safe because it uses the forgiving
  replaceRangeWith. A null drop point is now a handled no-op: the node stays put, still selected.
- A doc image with a cross-origin src (README badge, CDN image) failed the same-origin identity
  check, so drag-reordering IT still fell into the duplicate path. htmlReferencesSrc now compares
  full ABSOLUTE URLs — identity is the question there, not hosted-by-us membership — while the
  hosted-recognition helpers stay same-origin-scoped. New regression test fails pre-fix.

Also folded the remaining inline comments into the handleDrop TSDoc and gave IMG_SRC_RE /
INLINE_ROUTE_QUERY_KEYS proper TSDoc (production diff is now TSDoc-only).
2026-07-11 20:21:51 -07:00
Waleed a2f868c397 fix(demo): preload the Cal.com booking embed while the visitor fills the form (#5616)
* fix(demo): preload the Cal.com booking embed while the visitor fills the form

The embed script, booker iframe, and its assets only started downloading
after the visitor pressed Continue, so the calendar took several seconds
to appear. Warm the whole path on first form focus via the embed's
documented preload instruction (hidden ?preload=true iframe caches the
booker assets) plus a preconnect to app.cal.com. Nothing Cal.com-related
loads at initial page load, so Lighthouse/LCP are untouched.

* fix(demo): retry embed warm-up on failure, preconnect only on first focus

Reset the preload guard when embed.js fails to load so a later focus can
retry, and move the app.cal.com preconnect from render into the
focus-triggered preload path so initial page load makes zero Cal.com
connections.
2026-07-11 19:51:51 -07:00
Theodore Li 0cc6ed61d3 improvement(emcn): multi-select selectors + Wizard on ChipModal (#5614)
* feat(sub-block): support multi-select in channel/user selector fields

* improvement(emcn): migrate Wizard primitive to ChipModal

* fix(emcn): wizard height sizes whole dialog; restore dialog description
2026-07-11 22:25:41 -04:00
Theodore Li 1b82b9763c fix(custom-blocks): restrict iconUrl to https or internal serve paths (#5613) 2026-07-11 21:50:03 -04:00
Waleed b629292d5b improvement(chat): shimmer active subagent and tool labels instead of spinners (#5612)
* improvement(chat): shimmer active subagent and tool labels instead of spinners

* improvement(chat): address review — focus-visible chevron, single shimmer source, reduced-motion rest color

* improvement(chat): pulse shimmer text under reduced motion so running state stays visible

* improvement(chat): reset background-clip in reduced-motion shimmer fallback

* fix(chat): apply reduced-motion shimmer fallback in dark mode too
2026-07-11 18:41:06 -07:00
Waleed fcf4e02930 fix(landing): repair Lighthouse-flagged CWV audits on production (#5605)
* fix(landing): repair Lighthouse-flagged CWV audits on production

Empirically verified against a live full Lighthouse run of www.sim.ai
(production, pre-fix) plus a local build of the exact deployed commit with
source maps temporarily enabled for root-causing. Distinguished genuinely
failing audits from passing ones already misread as broken.

- fetchPriority missing on every LCP hero image: `priority` generates a
  preload <link> but Next does not auto-add fetchpriority=high to it -
  confirmed via raw deployed HTML diff. Added explicit fetchPriority='high'
  to all 5 priority Image usages (hero, enterprise, blog/library post +
  index cards).
- valid-source-maps failing: production ships no source maps at all
  (productionBrowserSourceMaps defaults false). Enabled it - safe here since
  this repo's frontend is already fully open source, so no incremental
  exposure versus Next's default.
- image-delivery-insight (55.8KB wasted): feature-integrate-ui.png's `sizes`
  hint was a flat 1050px regardless of viewport, so mobile fetched the
  1920w variant for a ~423px real render. Replaced with a responsive sizes
  expression derived from the sibling backdrop image's own (already
  correct) hint, scaled by the callout's documented 125% overhang.
- cache-insight (best-fixable portion): _next/static/* filenames are
  content-hashed and immutable per deploy, but shared one cache rule with
  unhashed /public assets, capping both at 1-day max-age. Split into two
  rules - hashed assets now get 1-year immutable, unhashed assets keep the
  shorter revalidating TTL. Verified via a real build + server that both
  paths now return the correct distinct header.

Investigated and NOT changed (documented, not assumed):
- legacy-javascript-insight (14KB): traced via sourcemap to
  next/dist/build/polyfills/polyfill-module.js - Next's own built-in
  polyfill bundle, not our code or a dependency, and not exposed via any
  next.config.ts option. No browserslist misconfiguration on our end (none
  exists; Next already defaults to its modern target).
- forced-reflow-insight: even with source maps present locally, the
  dominant cost (335-417ms) stayed [unattributed] by Chrome's own profiler,
  and the small attributed slice was non-deterministic between our own
  chunk and a third-party script (HubSpot analytics) across runs - not a
  confident single root cause worth a targeted fix.
- render-blocking-insight / network-dependency-tree / bf-cache: bf-cache's
  actual failure reason is Cache-Control: no-store on the main document -
  the exact root cause already fixed on staging (PR #5522/#5528, the
  PublicEnvScript/unstable_noStore fix) but not yet promoted to main/prod.
  Resolves once that ships, not additional work here.

* fix(landing): convert mothership cover from PNG to JPEG (/blog LCP 6.6s -> 2.8s)

Ran a full Lighthouse sweep across every public page as requested. /blog
scored 73 (LCP 6.6s) while every other page scored 95+ - reproduced
consistently across 3 runs, not noise. Traced via lcp-breakdown-insight:
the LCP image (mothership/cover.png, 241KB even after the earlier palette
compression pass) took 6+ seconds to download on simulated mobile
throttling, well beyond what its size should cost.

PNG is a poor fit for this illustration's subtle gradients versus JPEG's
lossy compression. Verified empirically before converting: same 1920x1080
resolution, visually identical (spot-checked), 241KB -> 65KB (73% smaller).
No other cover in the content set uses PNG and benefits the same way
(checked copilot/cover.png, the only other PNG cover - already optimal at
64KB, converting it yielded no improvement, left unchanged).

Verified fix: /blog score 73->93, LCP 6.6s->2.8s, reproduced across 3 runs.

* fix(landing): correct mobile sizes tier, drop non-functional cache rule

- integrations-callout: account for FeatureCard's max-lg:grid-cols-1 mobile
  stack in the sizes hint, verified against Lighthouse's measured mobile
  render width.
- next.config: remove a custom _next/static cache-control rule that never
  actually fired (confirmed via header-marker test) - Next's own built-in
  default already applies the correct immutable 1yr cache to that path.

* fix(landing): correct sizes underestimate + fix dead .map header rule

- integrations-callout: derive sizes from the section's actual grid math
  (fixed 386px copy column, 40px gap, section gutters) instead of an
  approximated vw fraction. Verified against a static reproduction of the
  layout rendered at each Tailwind breakpoint - the old 110vw mobile tier
  underestimated real render width by ~3% right at the 1023px stack
  boundary, which could cause the browser to pick a too-small srcset
  candidate and upscale.
- next.config: the .map header rule's trailing `$` was read as a literal
  character by Next's path-to-regexp source matcher, not a regex anchor,
  so the rule never matched a real .map URL (confirmed via routes-manifest
  regex + a live header check). Removed the dead anchor and added a
  bounded Cache-Control so a future decision to stop shipping source maps
  isn't undermined by a 1yr immutable cache on already-fetched maps.

* fix(llms): serve well-formed llms.txt, remove Mothership + dead static files

Both the marketing site and docs site's llms.txt validator errors ("does
not appear to contain any links") traced to the same root cause: a static
public/llms.txt shadowed a better-written, already-existing dynamic
app/llms.txt route, and every "link" in the static files (and in the
docs app's auto-generated route) was bare `label: url` text, not Markdown
link syntax - so a strict Markdown-link parser found zero matches even
though URLs were visibly present.

- apps/sim: delete public/llms.txt (dead code, shadowing the properly
  Markdown-linked app/llms.txt route.ts, confirmed via production headers
  showing the static file was what actually served). Fix llms-full.txt's
  Links/Support/Legal sections to use [label](url) syntax, correct a
  stale "Next.js 15" reference, and replace "Mothership" with "Chat" per
  the constitution's language rules.
- apps/docs: same shadowing issue - delete the orphaned public/llms.txt
  (also still said "Mothership"). Fix the auto-generated per-page link
  list in app/llms.txt/route.ts to emit [title](url) instead of
  "title: url" for every documentation page.

* fix(llms): actually include the route.ts fixes from the prior commit

The prior commit (3b2d35c99) only staged the two deleted public/llms.txt
files - these two modified route.ts files (the Mothership/link-format
fixes they were meant to accompany) were left unstaged. No new changes,
just completing that commit's intent.
2026-07-11 17:50:00 -07:00
Waleed 83c532ce36 improvement(files): show loading spinner in file preview content area (#5610) 2026-07-11 17:48:25 -07:00
Waleed 0aa23090e9 fix(files): unwedge post-stream read-only editor; stop bullet Backspace from destroying the image below (#5608)
* fix(rich-markdown-editor): stop Backspace on an empty bullet from destroying the image below it

Reported: clearing an empty bullet with an image after it "nuked the bullet point and the image".
Reproduced: when the emptied bullet is the doc's first block, removeEmptyWrappedBlock's
Selection.near(resolve(start), -1) finds no text position behind it and silently lands a
NodeSelection on the FOLLOWING image — so the user's next keystroke is destructive (a second
Backspace while clearing deletes the image; typing replaces it). Only-first-block explains why it
wouldn't re-repro. The selection left behind is now always a caret: end of the previous textblock
first, else a gap cursor at the deletion point when the neighbour is a leaf (typing there inserts a
new block instead of replacing the image), else the next textblock.

The regression test surfaced two adjacent gap-cursor crashes on Backspace, both reachable on
current staging whenever a gap cursor exists (e.g. between two dividers/images, the
data-gap-between-leaves state):
- our own handler threw RangeError from $from.before(0) on a depth-0 (doc-start) gap cursor
- with that guarded by falling through, TipTap's blockquote Backspace handler crashes on the same
  resolution ($from.node(-1) is undefined) — so a doc-start gap cursor consumes the key instead
  (there is nothing before it for Backspace to act on)

* fix(files): poll the content query while the post-stream reconcile waits, so the editor can't wedge read-only

Reported: images "don't get selected sometimes" (can't grab/drag/resize, doc uneditable) until a
full refresh. Reproduced in a real-Chromium harness driving the actual RichMarkdownEditor + engine:
after an agent stream settles, the reconcile phase exits only when a fetch shows the server content
advanced past the pre-stream baseline — but that exit had no retry. A single refetch racing the
agent's write (or the mothership invalidation never reaching this surface — it's the only place
that invalidates this query) left the editor locked read-only indefinitely: contenteditable=false,
images not grabbable, until refetchOnWindowFocus or a reload happened to run.

Fix: while (and only while) the reducer is in `reconciling`, the content query polls via
react-query's refetchInterval — the same pattern this module already uses for the generated-doc
409 polling, and like it, bounded (45s window; past that the write has almost certainly failed and
refetchOnWindowFocus remains the recovery). The interval is the function form reading the phase
through a ref, re-evaluated by react-query after every fetch, so polling stops the moment a fetch
advances without needing an extra render.

Harness (real Chromium, real editor + engine, in-memory server): pre-fix the editor stays
editable=false with fetches frozen at 1 indefinitely while the server holds the new content;
post-fix it unlocks within one poll (~1.5s), polling stops immediately after finalize, and the
streamed image click-selects. Unit tests drive stream -> settle -> advance through the real engine
and assert the interval flips on/off with the phase (2 of 3 fail pre-fix), plus the bounded window
and a no-polling guard for plain at-rest editing.

* test(files): cover the refetchInterval passthrough against real react-query

Both consumers' test setups (the reconcile unit tests and the browser harness) replace
@/hooks/queries/workspace-files, so the real hook's two changed lines were exercised by nothing but
the type-checker. These render the real useWorkspaceFileContent under a real QueryClientProvider
with a stubbed fetch: no polling by default, polling with a numeric interval, and the function form
re-evaluated so flipping its condition stops the polling — the exact mechanism the reconcile fix
depends on. The two polling tests fail against the pre-fix hook.

* fix(files): degrade reconcile polling to a slow cadence instead of stopping; prove findFrom textOnly never leaf-selects

Greptile round 1:
- Real gap in my bounded window: past 45s the poll stopped outright, leaving the reducer wedged in
  reconciling with only focus-refetch/reload as recovery — the exact failure shape this PR exists
  to remove, just later. Polling now degrades to a 15s cadence instead of stopping, so a write
  landing late (slow job, replica catch-up) is still picked up automatically; react-query pauses
  interval refetches in background tabs by default, so an abandoned doc doesn't poll unattended.
- Refuted with source + an executable test: Selection.findFrom($gap, -1, true) cannot return a
  NodeSelection — prosemirror-state's findSelectionIn skips atoms entirely under textOnly
  (`!text && isSelectable`). New regression test pins the exact scenario (image directly BEFORE the
  emptied bullet): backward search returns null, the gap-cursor/forward-caret branches take over,
  and the image is never silently selected.
2026-07-11 16:56:18 -07:00
Waleed d165712d54 fix(files-upload): enforce workspace authorization on mothership uploads (#5604)
* fix(files-upload): enforce workspace authorization on mothership uploads

The mothership context in POST /api/files/upload skipped the workspace
permission and storage quota checks that every sibling context enforces,
letting a caller write files into a workspace they have no access to.

* fix(files-upload): check mothership quota once against the full batch

Resolve the mothership permission and quota check once per request
(mirroring the existing execution-context pattern) instead of per file:
a per-file quota check let a multi-file batch exceed the caller's quota
since each file's own size fit even when the combined total did not.
Also corrects the missing-workspaceId error message, which named the
chat context instead of mothership.
2026-07-11 14:16:14 -07:00
Waleed 67a2f00266 perf(search): cap Cmd-K result groups so typing isn't blocked by reshuffle (#5597)
* perf(search): cap Cmd-K result groups so typing isn't blocked by reshuffle

Every result group re-rendered its full match set on each keystroke — the
catalog alone is 1,000+ tool operations, plus all workflows/files in large
workspaces — so the deferred re-render that reshuffles results stalls the input
and drops the next character. Add a per-group cap (filterAndCap,
MAX_RESULTS_PER_GROUP=50) applied to every variable-size group. Results are
already score-sorted, so the cap only trims the low-relevance tail while keeping
the DOM and per-keystroke reconciliation bounded. No UX changes.

* perf(search): scope the result cap to active queries, never the browse list

Keep the empty state byte-for-byte identical to before — capping applies only
to the top-ranked matches of an active query (the reshuffling per-keystroke
render that stalls input), never to the full browsable list. No browsable result
a user could otherwise see is hidden.

* fix(search): rank blocks/tools by name so exact name matches win

Blocks and tools were ranked against their full searchValue (name + type + every
command-searchable option label), so an exact name match couldn't earn the
exact-match bonus and paid a length penalty inflated by option text — e.g.
"Agent" lost to "Pi Coding Agent" for the query "agent". Rank by name first via
a new optional secondary accessor on filterAndSort/filterAndCap, falling back to
searchValue only when the name doesn't match, so an exact name match always wins
while a block stays findable by an option label.

* fix(search): treat whitespace-only queries as browse

A whitespace-only query (e.g. a single space) was truthy, so it both filtered
(a space matches the spaces in multi-word labels) and capped large groups to 50
while the palette looked empty. Trim the query at the source in filterAndSort so
every caller treats whitespace-only as browse, and decide the cap on the trimmed
query — whitespace-only input now returns the full, unfiltered browse state.

* fix(search): keep integrations catalog hidden on whitespace-only input

The filteredIntegrations guard used the raw deferredSearch, so a whitespace-only
value passed it while filterAndCap trimmed the same value to browse and returned
the full catalog. Guard on deferredSearch.trim() to match the trimmed-emptiness
semantics — the catalog stays hidden until the user types something meaningful.
2026-07-11 14:15:57 -07:00
Waleed eb12333032 fix(chat-otp): re-check authType before minting deployment auth cookie (#5600)
PUT verify no longer trusts a stale authType at cookie-mint time — it
now re-checks the chat is still email-auth before issuing the cookie,
matching the existing POST guard and the public-file OTP route.
2026-07-11 14:15:07 -07:00
Waleed b73116226a fix(file-viewer): sanitize docx hyperlink hrefs to block javascript: XSS (#5599)
Sanitize anchor hrefs rendered by docx-preview after render, stripping
any scheme outside http/https/mailto (same allowlist already used by
the PPTX renderer). Covers both the workspace file viewer and the
unauthenticated public share page, which reuse the same component.
2026-07-11 14:14:42 -07:00
Waleed e2e29eed30 fix(api): bound request-body reads on speech/knowledge-chunks/help routes (#5601)
* fix(api): bound request-body reads on speech/knowledge-chunks/help routes

Replace unbounded request.json()/formData() reads with the existing
size-limited helpers, and move auth ahead of the body read on the
knowledge chunks route so unauthenticated callers can't force a large
allocation before being rejected.

* fix(knowledge): reject non-string workflowId instead of silently skipping authorization

A truthy non-string workflowId previously fell through the type guard and
skipped the workflow-scoped write authorization entirely. Validate the
type explicitly and fail closed with a 400 instead.
2026-07-11 14:14:11 -07:00
Waleed b486aba07e fix(pricing): route Talk to sales CTA to demo request form instead of signup (#5602)
- pricing page's enterprise card was labeled "Talk to sales" but linked to
  /signup for every card, sending visitors to self-serve signup instead of
  the demo-request flow every other "Contact sales" CTA on the site uses
- resolveCta now keys off the CTA's sales intent to pick the right href
- extracted the /signup and /demo route literals (previously hardcoded
  independently in 6 files) into a single shared apps/sim/app/(landing)/constants.ts
  so no CTA can drift to the wrong destination again
- hoisted the static per-column comparison sections out of render and
  deduped the annual-discount price math in pricing-plans.tsx
2026-07-11 12:40:07 -07:00
Waleed 7c2de1d426 feat(providers): add xAI to hosted key rotation pool (#5574)
* feat(providers): add xAI to hosted key rotation pool

Wires xai into the same hosted-key mechanism as OpenAI, Anthropic,
and Z.ai so Sim can serve Grok models without users bringing their
own key.

* fix(pi): include xai in Pi cloud-mode workspace BYOK read-back

xai was fully wired as a Pi-supported provider but missing from
WORKSPACE_BYOK_PROVIDERS, so a stored workspace xAI key was never
read back for cloud-mode Pi runs.

* fix(byok): add xai settings UI row

xai is both hosted (Pi block hides its inline API key field for
hosted models) and Pi-supported (cloud mode requires a user key),
so without a Settings > BYOK row users had no way to supply an xai
key for Pi cloud runs.
2026-07-11 14:23:27 -04:00
Waleed def2d5299a fix(docs-og-image): match reference cover template typography exactly (#5598)
* fix(docs-og-image): match reference cover template typography exactly

- swap Season Sans for Söhne Kräftig (500) — the reference cover's actual
  brand font, confirmed by letterform comparison; recovered from git
  history since it was removed as an unused static asset
- fix ink/background colors to exact reference hex values
- square caps + miter join on the corner arrow to match the reference's
  sharp corners instead of rounded ones
- recalibrate title font size, line height, and wrap width for the new
  font's metrics

* fix(docs-og-image): estimate CJK glyph width separately to avoid under-wrap

wrapTitleLines budgeted a flat 0.42em/char, tuned for Latin text. Docs
ships ja/zh locales — CJK glyphs render near-square (~1em), so a CJK
title could overflow the fixed-width title box uncaught. Sum per-char
em-width with a CJK-range check instead of counting characters.

* fix(docs-og-image): fall back to character-level wrap for oversized CJK words

wrapTitleLines only splits at spaces, so a space-free CJK run (common
for Chinese titles) still arrived as a single word wider than the
title box and rendered as one overflowing line. Falls back to
character-level chunking for any word that alone exceeds maxWidthEm.
2026-07-11 11:17:54 -07:00
Waleed 591516a9b9 fix(rich-markdown-editor): fix mention chip losing ambient color inside links/h6 (#5594)
* fix(rich-markdown-editor): fix mention chip losing ambient color (same class as #5573)

Auditing the whole "an element's own explicit color always wins over an inherited one" bug class
(previously fixed for strong/em/code/del/s vs. links and h6 in #5573) turned up one more instance:
the @-mention chip's label hardcoded text-[var(--text-primary)], which is redundant with the prose
default anyway (matching the strong/em/code precedent) and silently overrides any ambient color a
mention's container legitimately sets — a link's blue, or h6's dimmer --text-secondary — since a
mention is inline content that can appear inside either (e.g. "###### see @some-file").

Removed the hardcoded color entirely so the label inherits correctly in every context, same fix as
strong/em/code. The icon's own monochrome --text-icon fallback is untouched (icons intentionally
don't follow ambient text color).

New test renders MentionChipView directly and asserts the wrapper carries no explicit text-color
utility class; verified it fails against the pre-fix className.

* fix(rich-markdown-editor): broaden mention-chip color-regression guard beyond the exact old class

Greptile: the test only matched the literal old text-[var(--text-primary)] string — a future edit
swapping it for e.g. text-[var(--text-secondary)] or text-blue-500 would still silently reintroduce
the ambient-color bug and pass this test. Now checks every non-descendant-scoped (excludes the
[&>svg]: icon rule) text-* utility on the wrapper against a color-shaped pattern (arbitrary value,
color-shade pairs, or a named color keyword), so any bare text color slipping back in fails.
Verified against a text-blue-500 regression.

* fix(rich-markdown-editor): close the semantic-Tailwind-color gap in the mention-chip test

Greptile: the color-shaped regex still missed semantic theme tokens (text-primary,
text-muted-foreground, text-chart-1, etc.) since they don't match a shade-suffix or bracket pattern.
Rather than keep enumerating Tailwind's color-naming schemes, flag ANY unscoped text-* utility on
the wrapper — none is legitimate on this chip today, so this can only be a color slipping back in.
Verified against text-primary/text-muted-foreground/text-chart-1 regressions.

* fix(rich-markdown-editor): catch Tailwind's self-targeting [&]:text-* variant too

Greptile: the previous filter excluded ANY class starting with `[&`, which also dropped Tailwind's
self-targeting arbitrary variant (`[&]:text-primary` applies to the element itself, same as a bare
`text-primary`) — only descendant variants like `[&>svg]:text-*` should be excluded. Now explicitly
catches both the bare and `[&]:` forms. Verified against a `[&]:text-primary` regression.
2026-07-11 11:04:47 -07:00
Waleed d14c304787 fix(og-image): match sim.ai OG image colors to live landing tokens (#5592)
The wordmark ink and background were slightly off from the actual
site: #1a1a1a on #f8f8f8 in the static OG asset vs var(--text-body)
(#3b3b3b) on var(--bg) (#fefefe) as rendered on the live landing page.
Recolored the same wordmark artwork in place to match exactly - alpha
reconstructed from the existing two-color image and recomposited onto
the new colors, so the glyph geometry/anti-aliasing is unchanged.
2026-07-11 11:04:18 -07:00
Theodore Li 3a2f4e5c8f feat(custom-blocks): add deploy_custom_block copilot tool (#5532)
* feat(custom-blocks): add deploy_custom_block copilot tool

* feat(copilot): send workspace entitlements to the mothership

* chore(copilot): sync tool catalog — plan-neutral deploy trigger text

* refactor(copilot): extract entitlements registry with add-an-entitlement recipe

* fix(custom-blocks): review fixes — undeploy without enterprise, array bounds, whitespace name

* fix(custom-blocks): enforce per-item field limits from the REST contract

* fix(custom-blocks): enterprise gate applies to first publish only, matching REST

* chore(copilot): sync tool catalog — deploy_custom_block requires name
2026-07-11 13:47:20 -04:00
Theodore Li fca5f10f86 feat(workflow-editor): open block palette on edge drag-release with auto-connect (#5586)
* feat(workflow-editor): open block palette on edge drag-release with auto-connect

* fix(workflow-editor): correct drag-release drop coords, scoping, and container placement

* fix(workflow-editor): correlate drag-release palette selection with a token

* fix(workflow-editor): preserve tool operation preset on in-container drag-release

* fix(workflow-editor): wire drag-release edge from the actual source handle via handleToolbarDrop

* refactor(workflow-editor): collapse drag-release correlation into one store field
2026-07-11 13:40:33 -04:00
Waleed 3d02bbbe62 fix(mcp): coerce corrupted consecutiveFailures instead of crashing the whole server list (#5593)
Root cause: updateServerStatus() only fell back to the default status
config when the whole statusConfig column was null/undefined, not when
it was a real object missing consecutiveFailures (e.g. the column's
'{}' default on server creation). currentConfig.consecutiveFailures
was then undefined, undefined + 1 evaluated to NaN, and
JSON.stringify(NaN) persisted as a literal `null` into the DB the
first time a freshly-created server had a connection failure.

That corrupted value then failed listMcpServersContract's Zod parse
client-side (consecutiveFailures: z.number() rejects null), and since
the response is a single array, one bad server blanked the entire MCP
servers list with "Response failed contract validation" for the whole
workspace — currently affecting 81 servers across 69 production
workspaces.

Two fixes:
- service.ts: normalize the read-back statusConfig so
  consecutiveFailures is always a real number, never NaN, going
  forward.
- contracts/mcp.ts: coerce any non-number consecutiveFailures
  (including the already-corrupted `null` rows) to the schema's
  default of 0 instead of failing validation, so every
  already-affected workspace self-heals on next load with no DB
  migration needed.
2026-07-11 10:37:56 -07:00
Waleed 2ba0b5837d fix(rich-markdown-editor): reliable image selection + serialization/paste polish (#5590)
* fix(rich-markdown-editor): reliable image selection + resize and broken-image polish

- Reactive editability. The editor runs with shouldRerenderOnTransaction:false, so a node view that
  read editor.isEditable once at render kept a stale value after setEditable() toggled (e.g. an agent
  stream settling into the doc), leaving a pasted image showing read-only affordances and code blocks
  stuck on their read-only label until a full refresh. A shared useEditorEditable hook subscribes to the
  editor's update/transaction events so both node views track editability reactively.
- Deterministic click-to-select. A handleClickOn plugin sets the image's NodeSelection on a plain click
  so selecting never depends on ProseMirror's click-vs-drag arbitration; grab-anywhere drag-reorder is
  kept, and modified clicks (Cmd/Ctrl to follow a linked badge) fall through.
- Resize commits once. The width previews in local state during the drag and commits to the node once on
  pointer-up (or pointer-cancel), so a resize is a single undo step and an interrupted drag isn't lost.
- Broken-image placeholder. A src that fails to load renders as a visible box with its alt text and stays
  selectable, instead of collapsing to a bare broken-icon.

* fix(rich-markdown-editor): keep bare URLs and autolinks bare on serialize

The normalizing serializer rewrote a bare URL or <url>/<email> autolink to [url](url) /
[a@b.com](mailto:a@b.com) on every save, churning every README's links. postProcessSerializedMarkdown now
collapses a link back to its bare form when the visible text already equals the destination (a plain
http(s) URL, or an email behind mailto:) — GFM re-autolinks it, so the round-trip is identical with a far
quieter diff. Titled links, explicit links, and any link inside a fenced/inline code region are left
untouched. Idempotent.

* feat(rich-markdown-editor): linkify a selection when a URL is pasted over it

Pasting a single URL (or a bare www. host / email) over a non-empty text selection within one block now
wraps the selection in a link, keeping the visible text. www. gets https://, an email gets mailto:, and
the href is scheme-sanitized (javascript:/data: rejected; mailto: requires a real user@host address).
Collapsed carets, cross-block selections, multi-word pastes, node selections, and code contexts fall
through to normal paste.

* chore(rich-markdown-editor): drop useless String.raw in highlight.ts

biome 2.0's noUselessStringRaw flags HIGHLIGHT_BODY — its pattern has no escape sequences, so String.raw
is equivalent to a plain template literal (byte-identical value; interpolated into the other String.raw
regexes unchanged). Pre-existing on staging; the repo-wide lint gate blocks CI on it.
2026-07-10 23:23:42 -07:00
Waleed 9ee499e9f7 fix(canvas): raw tooltip shows up when hovering block params (#5589)
* fix(canvas): replace native title tooltip with styled overflow tooltip on block params

Hovering a truncated subblock value or block name on the canvas popped
the browser's raw native tooltip with the full untruncated content
(including raw code). Replace the `title` attribute with the
cursor-following styled Tooltip, shown only when the text is actually
clipped.

* fix(canvas): drop unused ResizeObserver in OverflowSpan

useFloatingTooltip's canShow already receives the hovered element, so
measuring overflow via useIsOverflowing was redundant — every canvas
row was paying for a ResizeObserver and resize listener it never used.
2026-07-10 21:58:08 -07:00
Waleed f3582ed197 feat(branding): sim wordmark favicon/OG, docs footer parity, footer peel (#5587)
* feat(branding): sim wordmark favicon/OG, docs footer parity, footer peel

- replace apps/sim favicon and default OG image with the sim wordmark
  logo (OG image widened, logo kept at native size)
- swap the docs navbar logo to the icon-only mark (no wordmark text)
- add a scroll "peel" reveal effect to the landing footer using a
  sticky-positioned illustration, pure CSS, no scroll listeners
- port the same footer (link directory + peel effect) to the docs app
  so both apps are visually consistent; add Academy to Resources
- rebuild the docs OG image template to match the site's existing
  blog/library cover style (wordmark top-left, arrow top-right, title
  bottom-left), working around a Satori text-measurement bug that
  doubled the gap after certain words

* fix(docs): correct OG font, mobile logo, and footer stacking

- switch the docs OG image title font from Geist to the site's real
  brand font (Season Sans), instantiated as a static TTF weight since
  Satori can't parse WOFF2 or variable fonts; served from /static/
  so the i18n proxy's matcher (which excludes static but not fonts)
  doesn't intercept it
- fix DocsLayout's nav.title (fumadocs' own mobile menu slot) to show
  the wordmark instead of the icon mark
- add an isolated stacking context + higher z-index to both the docs
  and sim app footers so fumadocs' sticky z-20 sidebar can't paint
  over the footer content or the peel reveal

* fix(docs): match OG template exactly, fix gradient/origin bugs

- recalibrate the OG image to the reference cover template's actual
  measured values: 1200x675 canvas (was 630), ~26px margins (was
  56-64px), ink #525252 (was #3f3f3f), larger wordmark/arrow/title
  sizing — confirmed by direct pixel measurement of the reference
  cover.jpg, not estimation
- fix SimLogoIcon/SimLogoFull's SVG gradient ids to be unique via
  useId() instead of a fixed string, so multiple instances on one
  page don't collide (Greptile P2)
- fix SIM_SITE_URL to be a hardcoded sim.ai constant instead of
  deriving from NEXT_PUBLIC_APP_URL, which reflects wherever this
  deployment runs, not the fixed public marketing site (Greptile P1)

* fix(docs): route Jira footer link to the docs guide, not sim.ai

Every other integration in the footer's Integrations column links to
its own docs.sim.ai guide; Jira was the only one pointing at the
marketing site's landing page instead, despite docs having its own
/integrations/jira guide. Matches the established pattern.

* fix(docs): fix sidebar-divider grid regression, footer-peel path/positioning, OG sizing, and prune stray comments

- #nd-docs-layout::before divider now spans the full grid explicitly
  (grid-row/grid-column: 1 / -1) instead of being auto-placed into a
  real content cell, which was pushing page content down
- footer-peel.jpg moved under /static/landing/ (was 404ing behind the
  i18n proxy's non-static path matcher) and wrapped in a relative div
  so next/image's fill positioning is valid under the sticky container
- OG route: corrected title font sizes and char-width ratio so long
  titles wrap to 2 lines instead of 3, and resized the corner arrow to
  match the reference cover template's proportions
- swapped the icon-only desktop navbar logo back to the wordmark
- removed stray non-TSDoc comments, folded into TSDoc where the
  explanation was worth keeping

* fix(footer): remove sticky peel reveal, keep clean footer link directory

The peel's "reveal window" relied on position: sticky bottom-detaching
into a containing block whose extra height came from padding-bottom —
that combination doesn't reliably work in WebKit/Safari (sticky never
gets room to engage when the surplus height is padding rather than an
explicit height or content), so the peel stayed permanently covered by
the footer regardless of viewport size. Rather than carry that
unreliable technique further, removing it entirely from both apps and
reverting to the plain footer link directory.
2026-07-10 20:44:55 -07:00
Waleed 1c604152b3 fix(landing): contain sr-only logos heading to stop phantom root scrollbar (#5585) 2026-07-10 18:11:26 -07:00
Waleed da2371adf8 fix(models): restore Anthropic models in landing page compare chart (#5584) 2026-07-10 17:48:53 -07:00
Waleed 8525ba5a2b feat(landing): add Share chip to integration and model pages (#5582)
- add a Share chip (copy link / X / LinkedIn) to integration and
  model detail pages, matching the bordered secondary-pill chip
  already used for View docs / All {provider} models
- rework ShareButton to render as a Chip everywhere (blog, library,
  integrations, models) instead of a bespoke muted-text trigger, and
  switch its copy-link state to the shared useCopyToClipboard hook
2026-07-10 17:21:16 -07:00
Theodore Li 7962236719 improvement(custom-blocks): hardened delete with usage count + per-input required option (#5575)
* improvement(custom-blocks): usage visibility + type-to-confirm delete

* feat(custom-blocks): per-input required option

* improvement(custom-blocks): replace usage tab with delete-confirmation usage count

* fix(custom-blocks): escape LIKE wildcards in usage scan + fresh count on delete modal

* fix(custom-blocks): explicit ESCAPE clause on usage-scan LIKE prefilter
2026-07-10 20:16:53 -04:00
Waleed 8e7e2db35e feat(docs): update favicon, fix icon contrast, add integration intros (#5581)
* feat(docs): update favicon, fix icon contrast, add integration intros

- replace docs favicon/icon assets with new sim logo
- fix light-tile icon contrast in BlockInfoCard so icons like Daytona
  no longer render invisible (white-on-white); matches sim toolbar's
  brightness-based contrast logic
- add missing MANUAL-CONTENT-START:intro sections to 30 integration
  docs pages that lacked context/links

* chore(docs): normalize spacing from generate-docs pass

Ran the docs generator to verify our new intro sections survive
regeneration cleanly. It reformats the blank line before "## Usage
Instructions" to match every other manually-annotated page.

* fix(context-dev): remove prefetch and simplified-brand tools

- remove context_dev_prefetch_domain, context_dev_prefetch_by_email,
  and context_dev_get_brand_simplified tools and their block operation
  entries; these aren't meant for general use
- regenerate docs to drop their sections from context_dev.mdx
2026-07-10 17:12:41 -07:00
Theodore LiandClaude Opus 4.8 f4d47ed826 feat(slack): reusable custom bot credentials, slack_v2 block (preview), redesigned trigger (#5323)
* feat(slack): enable assistant-agent tools via assistant:write scope

Add assistant:write, app_mentions:read, and im:history to the Slack bot
OAuth scopes so the Set Assistant Status / Title / Suggested Prompts tools
(assistant.threads.*) work with users' existing Slack credentials — no new
app or credentials required. Restore the action_assistant trigger capability
(scope assistant:write) in the manifest generator.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WpeT8J5yVCrrNQB9Hzm9uS

* Add slack trigger

* fix channel picker in slack trigger

* improvement(slack-trigger): reorder app type, gate account to sim mode, add channel-id input

* fix(slack-trigger): drop unmapped events from filter, resolve oauth token for reaction text + file downloads

* fix(slack-trigger): empty operation selection fires nothing; resolve token via credential owner not execution actor

* fix(slack-trigger): ignore message edit/delete/system subtypes; prefer channel picker over stale manual ids

* feat(slack-trigger): single-event model with contextual filters and full event catalog

* fix(slack-trigger): apply event/channel/bot filters on custom-app path too

* fix(slack-trigger): don't drop edit/delete events when channel_type is absent

* feat(slack): reusable custom bot credentials, slack_v2 block, interactivity triggers

- Custom bot as a workspace service-account credential (set up once, shared
  ingest URL /api/webhooks/slack/custom/{credentialId}, reused across triggers
  and actions)
- slack_v2 action block: credential-based Custom Bot auth alongside Sim OAuth;
  v1 hidden from toolbar
- Interaction triggers (block_actions / view_submission) with optional
  action/callback id filter; settings.interactivity in generated manifests
- Setup wizard: name + description, full permissions by default with
  ChipDropdown customization; reconnect mode rotates secrets in place
- Centralized service-account token resolution (unknown provider fails loudly)
- Shared Slack webhook fan-out dispatcher for native + custom ingest routes

* chore(api-validation): bump route baseline to 924 after staging merge

* feat(slack): preview-gate slack_v2 and the custom-bot credential surfaces

slack_v2 (block + hosted slack_oauth trigger) ships preview: true — hidden
from all discovery until revealed via block-visibility AppConfig or
PREVIEW_BLOCKS. v1 stays toolbar-visible with the legacy slack_webhook
trigger until v2 GAs. The integrations-page custom-bot setup surface rides
the same flag via isHiddenUnder(slack_v2); placed instances, existing
credentials, and ingest/execution paths are never gated.

* fix(slack): v1 keeps slack_webhook trigger subblocks; handle object-form event channels

- v1 spread had been swapped to slack_oauth's trigger subblocks (shared with
  v2), leaving its slack_webhook deploy path without signing-secret config
  (Bugbot high). v1 now carries the legacy trigger set again; v2 swaps them
  for slack_oauth's.
- resolveSlackEventChannel reads channel.id for channel_created/channel_rename
  payloads, so channel filters no longer drop every rename event.

* fix(slack): default absent appType to custom at deploy; deactivate custom-bot webhooks on credential delete

- appType is hidden and seeded 'custom' by value(), which only covers
  editor-created blocks; defaultValue now persists it via buildProviderConfig
  and the deploy fallback flips to custom (the only exposed mode this ship)
- deleting a slack-custom-bot credential now also deactivates provider='slack'
  webhooks routed by that credential id, not just native slack_app rows

* fix(slack): resolve credential owner for deploy-time team_id lookup

A teammate deploying a trigger wired to a shared Slack credential isn't the
credential owner; refreshAccessTokenIfNeeded only loads tokens for the owning
user. Resolve the account owner first, mirroring the runtime formatInput path.

* chore(slack): reconcile staging merge

- nullable webhook.path coalesced at correlation/payload/tiktok boundaries
- slack dispatch delegates to staging's dispatchResolvedWebhookTarget
  (shared preprocess/deployment/filter/enqueue lifecycle), keeping the
  skip-reason diagnostics; route tests reworked around that seam
- api-validation route baseline 924 -> 926

* fix(slack): workspace-scope bot credentials at deploy; recreate webhooks on routing transitions

- a bot credential id is semi-public (embedded in Slack Request URLs), so the
  custom deploy branch now rejects credentials outside the workflow's workspace
- needsRecreation also compares path/routingKey, so a row from an older routing
  model can't survive redeploy as a stale delivery surface

* test(slack): pin fail-closed behavior for empty/missing event selection

* fix(slack): 409 on custom-bot name collision instead of silently returning the existing credential

The service-account dedupe matches on displayName, which defaults to the Slack
team name — shared by every bot in that workspace. A second unnamed bot create
returned the first credential as success, orphaning the new id already pasted
into the Slack Request URL. Same-id replays stay idempotent; different-id
collisions now fail loudly so the wizard prompts for a distinct name.

* fix(slack): reconnect surfaces Atlassian error codes and persists name/description edits

- PUT credential route now returns the Atlassian provider code (providerErrorCode
  -> code) so reconnect failures map to specific token/domain messages, matching create
- Google/Atlassian reconnect send + seed displayName/description (parity with Slack);
  edits are no longer silently discarded, and empty fields don't clobber existing values

* fix(slack): require bot name; propagate rotated bot_user_id to webhooks on reconnect

- the setup wizard now requires a bot name (canAdvance), so the credential name,
  manifest app name, and uniqueness key all use the user's choice instead of the
  shared Slack team-name fallback that collided for a second bot in one workspace
- reconnect that changes the bot user id (recreated Slack app) now updates the
  bot_user_id cached in each bound webhook's providerConfig, so reaction
  self-drop keeps working instead of letting the bot's own reactions re-enter

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 19:43:52 -04:00