Aligns with the /integrations/[slug] convention (plural category, singular
item). Adds a 301 redirect (both the bare hub page and every competitor
detail page) so previously indexed URLs keep working, and updates every
internal reference: footer nav, sitemap generation, and all import
specifiers/canonical URLs/breadcrumb JSON-LD within the route itself.
* improvement(cleanup-skill): parallelize analysis, apply fixes sequentially
* improvement(cleanup-skill): add comment-reduction pass; mirror 6 missing skills into .claude/commands
* fix(cleanup-skill): substitute parsed scope into analysis passes instead of literal <scope>
* fix(cleanup-skill): parse fix token anywhere; preserve pass labels through convergence for ordered apply
* fix(cleanup-skill): apply Step 1 proposals content-anchored, re-derive when a prior pass invalidated the snippet
* fix(babysit-skill): correct garbled --reverse explanation across all three copies
* fix(skills): propagate parallel cleanup to cursor/agents copies; disambiguate babysit /ship refs in claude copy
* fix(skills): port url-state + comment passes to cursor/agents; clarify converge pass-label ordering
* feat(skills): canonicalize skills under .agents/skills with generated .claude/.cursor projections
Establish .agents/skills/<name>/SKILL.md as the single source of truth (latest
content reconciled per skill from the three drifted copies), and generate the
.claude/commands and .cursor/commands projections from it via scripts/sync-skills.ts.
Adds skills:sync/skills:check, a CI gate, a pre-commit regen hook, and CONTRIBUTING docs.
Structurally fixes prior drift (e.g. abbreviated .claude ship -> full ship).
* fix(skills): strip leaked XML tags from skill tails; clarify lint:check has no per-file target
Removes stray </content>/</invoke> markup that leaked into add-block,
add-connector, add-hosted-key canonical skills, and reword the cleanup
skill's lint step to note bun run lint:check runs repo-wide via turbo
(no per-path API). Projections regenerated via skills:sync.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjefwescJoHZ6zcc3C17FR
* fix(add-block-skill): restore unknown-output stop in Final Validation
Re-add the "if any tool outputs are still unknown, tell the user instead
of guessing block outputs" step that was dropped when Final Validation
step 5 became the BlockMeta template check.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjefwescJoHZ6zcc3C17FR
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(byok): migrate agent/router/evaluator LLM keys to BYOK by model prefix
* fix(script): address review — cover translate/guardrails, trim provider ids, reject mixed mapping, add key-prefix guard
* fix(script): include pi block in LLM-family set, trim key-prefix value
* fix(script): add router_v2 to LLM-family set, canonicalize provider ids to lowercase
* fix(script): resolve overlapping model prefixes deterministically (longest match wins)
Adds the three fields intentionally omitted from #5638 pending real values:
- legalName: 'Sim, Inc' - matches the entity named throughout the Terms of
Service (apps/sim/app/(landing)/terms)
- foundingDate: '2025'
- address: real registered office (80 Langton St, San Francisco, CA 94103)
* improvement(buffer): explicit mediaType override; error instead of guessing on ambiguous media
* fix(buffer): forward validated mediaType from routes into the media resolver
* fix(buffer): presign uploaded media for 7 days — Buffer fetches assets at publish time
* docs(buffer): document presign lifetime bound by signing credentials
* fix(buffer): always mint fresh presigned URLs from verified storage keys for media
* 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
* 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
* 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
* 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)
* 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
* 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.
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.
* 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.
* 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
* 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>
* 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.
* 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).
* 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.
* 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).
* 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.
* 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
* 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.
* 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.
* 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.
* 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.
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.
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.
* 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.
- 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
* 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.
* 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.
* 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.
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.
* 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