Commit Graph
201 Commits
Author SHA1 Message Date
Waleed aae9ce62e7 feat(smartlead): add Smartlead integration (#6352)
* feat(smartlead): add Smartlead integration

Adds a Smartlead block with 22 tools covering campaigns, sequences, leads,
analytics, and webhooks.

Every request path, parameter, enum, and response mapping was verified against
the live Smartlead API rather than its documentation, which proved unreliable:

- `POST /campaigns/new` (documented) 404s; the real path is `/campaigns/create`
- `GET /campaigns/{id}` and `/sequences` return bare payloads, not the
  documented `{success, data}` envelopes
- `/statistics` returns paginated per-email rows, not the documented aggregate
- `POST /campaigns/{id}/leads` returns import counters under entirely
  different field names than documented
- documented `/leads/{id}`, `/top-level-analytics`, `/all-leads-activities`,
  `/lead-lists/`, and `/lead-tags/` all 404

Enum values (campaign status, track settings, stop-lead settings, webhook event
types, engagement status) were probed value-by-value against the API.

Notes on the API's shape, encoded in the mappers:
- string-encoded numbers (`total_leads: "1"`, `sent_count: "0"`) are normalized
  to numbers so a field never changes type between operations
- `seq_delay_details` is read as `delayInDays` but written as `delay_in_days`
- webhook writes echo `event_type_map`/`category_id_map` objects while the list
  endpoint returns `event_types`/`categories` arrays; both map to arrays
- `track_settings` reads back in a vocabulary it will not accept on write

Statistics rows and lead message-history entries pass through unmapped: no
account could produce a non-empty sample, so no field names were invented.
Email-account tools and a webhook trigger are omitted for the same reason.

Adds a `smartlead-errors` extractor since the API's 400s put the useful text in
`message` while `error` is only "Bad Request".

* feat(smartlead): expand to the core workflow surface and fix review findings

Grows the block from 22 to 47 tools and fixes every defect found in review.

New tools (all executed against the live API end to end):
campaign email accounts (list/add/remove), duplicate, delete, CSV lead export,
webhook delete + delivery summary, lead + mailbox statistics, top-level
analytics by date, lead activities, get lead by id, unsubscribe from campaign,
unsubscribe globally, mark complete, delete from campaign, master-inbox
replies, lead lists (list/get/create/update/delete), email accounts, clients.

The endpoint inventory was rebuilt by extracting method+path from all 212
reference pages, which corrected several earlier conclusions: get-lead-by-id is
`/leads/{id}` (not under `/campaigns/`), lead lists are `/lead-list/`
(singular), and lead activities are `/campaigns/all-leads-activities` with no
campaign segment. More documented paths that 404 in reality: lead tags at
`/crm/leads/tags`, and webhook delete at `/campaigns/{id}/webhooks/{id}` —
deletion actually takes the id in the body.

Shapes the docs got wrong again, caught live: `GET /leads/{id}` wraps the lead
in a single-element `data` array; `DELETE .../leads/{id}` answers with the bare
string `success`, not JSON; duplicate returns `newCampaignId`; create/update
lead list take `listName`, and mark-complete takes `campaign_lead_map_id` where
its siblings take `lead.id`.

Review fixes:
- get_campaign, get_campaign_analytics and get_lead_by_email reported an
  all-null success for a missing resource, because Smartlead answers HTTP 200
  with `{}` (or an empty body) instead of 404. They now fail closed.
- update_campaign_settings silently reset stop_lead_settings and
  send_as_plain_text: their dropdown defaults are materialized at block
  creation, so every settings update carried them. Both now default to
  "Leave unchanged".
- Malformed JSON in Leads/Sequences/Custom Fields resolved to `undefined`,
  which overwrote the raw string the executor falls back on and dropped the
  field silently. Parsing now raises, and is scoped to the operation that
  consumes the field so a stale hidden value cannot fail an unrelated one.
- The four documented import overrides (block/unsubscribe/duplicate/bounce
  lists) had no field, so the block's own skill instructions were unexecutable.
- leadId did not distinguish lead.id from campaign_lead_map_id; passing the
  latter 404s, and list_campaign_leads surfaces it first.
- Path ids are trimmed and escaped; dead code and a hand-rolled id mapper removed.

Unverified and called out rather than guessed: add/remove email accounts to a
campaign (no mailbox could be connected, so only their error shape was seen),
and the row shapes for statistics, message history, inbox replies, email
accounts and clients — every one of those collections was empty on the
verification account, so their rows pass through unmapped.

* fix(smartlead): correct request params and outputs found in re-validation

Three tools sent a parameter Smartlead's validator rejects outright with 400,
so the affected operations failed whenever the field was filled in:

- get_campaign_lead_statistics paginated with `skip`; the endpoint accepts
  `offset` and only echoes it back as `skip`.
- list_lead_activities and list_inbox_replies both sent a campaign filter.
  `campaign_id`, `campaignId`, `campaign_ids` and `email_campaign_id` are all
  rejected, so the filter is gone rather than advertised and broken.

mark_lead_complete reported `next_sequence: null` on every call, including when
a step remained: `status.nextSequence` is an object, not a number. It now maps
to `next_sequence_id` and `next_sequence_delay_in_days` — verified live
returning step 10093171 rather than null.

get_lead_by_id reused the by-email mapper, so it always claimed the lead belongs
to zero campaigns; `GET /leads/{id}` omits `lead_campaign_data` entirely. It now
declares the narrower shape it actually returns.

A stale advanced `clientId` leaked into list_email_accounts: advanced subblocks
serialize without evaluating their condition, and that tool consumes `clientId`
while sitting outside its condition list. The field is now offered for that
operation too, so the value is visible wherever it is sent.

Two dropdowns had defaults that act on their own. `status` defaulted to PAUSED,
so choosing Update Campaign Status and never opening the dropdown paused the
campaign; it now requires an explicit choice. `pauseLead` sent `false` on every
categorization, which risks resuming a paused lead; it now defaults to leaving
the state alone.

Also counts CSV export rows with a quote-aware scan so a newline inside a name,
location, or custom field no longer inflates the count, and fills in the block
output declarations for the fields the 47 tools actually return.

* fix(smartlead): preserve a zero-day next-sequence delay and render enum values in docs

A next sequence scheduled to send immediately reported no delay at all:
`Number(next.delayInDays) || null` mapped a legitimate 0 to null.

Tool descriptions built enum lists with template literals. The runtime value
and the LLM-facing tool metadata were correct, but the docs generator reads the
description statically, so the public page rendered
`${SMARTLEAD_CAMPAIGN_STATUSES.join(...)}` instead of START, PAUSED, STOPPED.
The five affected descriptions now spell the values out.

* fix(smartlead): stop email-account tools from emitting mailbox credentials

Connecting a real mailbox to the verification account made the email-account
response shapes observable for the first time, and they carry the stored
credentials: `GET /email-accounts/{id}/` and the campaign route return
`password` in plaintext, the list route returns it base64-encoded, and both
carry `imap_password`.

Both tools passed rows through unmapped, so those values would have reached
workflow output, execution logs, and model context. They now select fields
explicitly and omit the credentials.

Verified against the live API: the API response contains the password while the
tool output does not, for both tools.

Also fills in the real email-account fields, which were previously an opaque
array — id, sender identity, SMTP/IMAP host and port, verification state and
last error, sending caps, warmup status, and tags.

* fix(smartlead): remove the dead campaign field that could target the wrong campaign

Removing the campaign filters from list_lead_activities and list_inbox_replies
left their `activityCampaignId` subblock, its params mapping, and its inputs
entry behind. Two problems, the second serious:

- On those two operations the field promised campaign scoping the API cannot
  do. Smartlead rejects every candidate key (`campaign_id`, `campaignId`,
  `campaign_ids`, `email_campaign_id`), so the value was silently discarded and
  account-wide results were reported as scoped.
- Worse, the field is `mode: 'advanced'`, and advanced subblocks serialize
  without evaluating their condition. A value left over from listing activities
  therefore fed `campaignId` on all 32 campaign operations through the
  `params.campaignId || params.activityCampaignId` fallback. Configuring List
  Lead Activities with campaign 111, then switching the block to Delete
  Campaign and leaving Campaign ID blank, would have passed required-validation
  and deleted campaign 111.

Both list tools now also say plainly that Smartlead exposes no campaign filter,
rather than advertising one in their descriptions.

Also: route mark_lead_complete's next-sequence id through the shared numeric
coercion, since Smartlead string-encodes numbers inconsistently and its sibling
field already arrives as a string; re-bind the two enum constants that lost
their last consumer so the literal descriptions cannot drift undetected; and
declare the 17 tool output keys the block was missing — `accounts` most
importantly, which is the entire payload of both email-account tools.
2026-08-06 17:40:20 -07:00
mzxchandraandWaleed Latif dc5bab6e54 feat(embeddings): multi-provider Embeddings block on a shared core (#6317)
* feat(embeddings): multi-provider Embeddings block on a shared core

The Embeddings block was OpenAI-only with a bare fetch: no batching, no
retry, no metering, and no hosted-key support. Meanwhile the knowledge-base
indexing path already had a real multi-provider engine. Nothing bridged the
two, so the block could not reach Gemini and the KB engine could not be
reached from a workflow.

Extract the shared core into lib/embeddings/ first, then build breadth on
top of it, so both the KB path and the block resolve models and providers
from one catalog and one set of adapters instead of a third parallel
implementation.

- lib/embeddings/: catalog, client, key resolution, batching, L2
  normalization, and adapters for OpenAI, Azure OpenAI, Gemini, Cohere,
  and Mistral
- lib/knowledge/embeddings.ts becomes a thin KB wrapper with its exported
  signatures unchanged; the 1536-dimension vector invariant does not move
- one tool per provider from a shared factory, behind a single
  /api/tools/embeddings route and contract
- new `embeddings` block type; the `openai` block is left functionally
  untouched and only leaves the discovery surfaces via hideFromToolbar
  plus sunset.replacedBy, so placed instances keep working unmigrated
- openai_embeddings is now an alias of embeddings_openai, so legacy
  instances pick up batching, retry, and metering with no visible change

* fix(embeddings): report an unsupported dimension as a client error

The route validated the model and the provider match up front but left
`dimensions` to be checked inside embed(), where resolveDimensions throws
and the generic catch maps it to 502. A typo in the block's dimension
field, or a reference expression resolving to an out-of-range value, was
reported as an upstream gateway failure rather than bad input.

Resolve dimensions in the route alongside the other boundary checks and
return 400. The throw stays the single source of the message, so the two
call sites cannot drift.

Adds route tests covering auth, the response shape, each boundary
rejection, input normalization, and the 502 path for genuine provider
failures.

* fix(embeddings): only send a dimension when the caller asked to reduce

resolveDimensions() returns the model's native size when no reduction is
requested, and that resolved value was handed straight to the adapter. The
adapters guard on `dimensions !== undefined`, so the field was always
populated and always sent.

Models that support Matryoshka reduction accept their own native size, so
this was invisible for text-embedding-3-*, gemini-embedding-001,
embed-v4.0, and codestral-embed. Models that do not support the parameter
at all reject it outright: every unreduced request to text-embedding-ada-002
and mistral-embed failed with a 400, which is both of the models whose
catalog entry has no supportedDimensions.

Track the caller's explicit reduction separately from the resolved
dimensionality. The resolved value still drives reporting and billing; only
the requested one reaches the wire.

Found by driving the live provider matrix against all four providers.

* test(knowledge): de-flake the sync-engine suite

Every test dynamically imported the module under test, so the first one to
run paid the whole cold-load cost inside its own 10s timeout and failed
intermittently under load.

The dynamic imports were working around a hoisting problem: mockMapTags is
a top-level const read by a vi.mock factory, and vi.mock is hoisted above
it, so a static import of the module under test crashes with a
use-before-initialization error. Declaring the mock through vi.hoisted()
removes that constraint, which is the pattern the testing guidelines
already call for.

One static import replaces 42 dynamic ones. The file drops from ~15s to
~2s and passed 5 consecutive runs.

* fix(embeddings): drop a capability the selected model no longer offers

The per-model Dimensions and Task Type dropdowns each share one subblock
id, and nothing clears a stored subblock value when its dependsOn fields
change — dependsOn only feeds rendering. A choice made for one model
therefore outlives a switch to another.

Picking 3072 on text-embedding-3-large and switching to -3-small left 3072
stored while the dropdown offered at most 1536, and the block forwarded it.
Same for a task type: 'similarity' chosen on Gemini survived a switch to
Cohere, which has no equivalent input type.

The guards only checked that the model declared the capability at all, not
that the value was one it lists. Check membership so a stale value falls
back to the model's native size, or is omitted, instead of being sent and
rejected. The user cannot have deliberately chosen an option the dropdown
stopped presenting.

* feat(embeddings): use the latent-constellation mark for the block icon

Replaces the scatter-plot-on-axes placeholder with a centre node, four
neighbours, and the rays between them — a point and its nearest neighbours
in embedding space, which is what the block actually produces. The axes
mark read as a generic chart and said nothing specific to embeddings.

Nodes are filled so they hold their shape at small sizes. The rays carry
less weight than the nodes to keep the hierarchy, but at 1.6/0.9 rather
than the 1.4/0.75 they were drawn at, so they do not thin out to loose
dots in the 14px block-search row.

Kept byte-identical between the app and docs icon sets.

* fix(embeddings): declare the outputs the legacy openai block returns

openai_embeddings became an alias of embeddings_openai, so the legacy
block's runtime payload gained `provider` and `dimensions`. Its declared
outputs still listed only embeddings/model/usage, so the tag picker never
offered two fields every run demonstrably returns, and downstream blocks
could not reference them.

Declaring them is additive and does not touch execution. Asserts the
legacy block's output keys match the replacement's, since both run the
same tool and neither should expose fields the other lacks.

* fix(copilot): resolve same-id subblock variants before validating

A block may declare one field id several times, each variant conditioned
on another field — the embeddings block declares model, dimensions, and
taskType once per provider, and the image and video generators do the
same. Validation keyed a map by id alone, so whichever variant was
declared last silently became the validator for every write to that
field.

Programmatic edits to an embeddings block were therefore checked against
Mistral's option lists whatever the saved provider: `text-embedding-3-small`
was rejected as not one of mistral-embed/codestral-embed, and dimensions
valid only elsewhere (3072, 768) could not be set at all. Values that
happened to overlap the last variant passed, so automation saw partial
success rather than a clean failure.

Keep every candidate per id and pick the one whose condition holds,
evaluating against the mutation's inputs merged over the block's saved
values so a partial write still resolves. When no condition matches, fall
back to the union of all variants' options rather than guessing.

Conditions still never gate whether a field may be written — that was a
deliberate choice and a hidden field stays writable. They only select
which definition describes the field, and an unresolved condition widens
the accepted set instead of narrowing it.

* fix(copilot): prefer a conditioned variant over an unconditioned catch-all

An unconditioned same-id variant matches every set of values, so it would
shadow a genuinely selected variant purely by being declared first. Prefer
a variant that actually asserted something about the current values.

No block in the registry currently declares a catch-all ahead of a
conditioned variant on a field where it would change validation, so this
is a guard against the pattern rather than a fix for a live case.

* chore(embeddings): scope this branch to the multi-provider block

Two changes made while building the Embeddings block are not part of it and
ship separately, so their files are restored to staging here:

- copilot edit-workflow validation resolving same-id conditional subblock
  variants. The embeddings block surfaced it, but it is a platform fix
  affecting ~20 blocks that declare a field id more than once, and it
  narrows what programmatic edits accept — that deserves its own review.
- the sync-engine test de-flake, which is unrelated test hygiene.

Both are preserved in full on feat/embeddings-full-snapshot.

Note this restores the reported bug where a programmatic edit to an
embeddings block validates model/dimensions against the last-declared
provider variant. The block is unaffected in the editor and at runtime.

* fix(embeddings): honor per-model token limits and bound the JSON input path

Review round 1.

Batching used one 8,000-token constant for every model, inherited from the
knowledge-base engine this branch extracted. `batchByTokenLimit` truncates
any single text above the limit it is given, so that constant both sent
oversized input to models with a lower ceiling and silently dropped content
models with a higher one accept:

- Gemini declares 2,048, so a 3,000-token text passed through whole and the
  provider rejected it, surfacing as a 502. This also affected knowledge-base
  indexing on staging, which uses the same constant.
- Cohere declares 128,000, so anything past 8,000 was truncated for no reason.

Batch against the selected model's own `maxInputTokens` instead. Using the
per-input ceiling as the per-batch budget also keeps every individual text
within it.

The contract bounds the array arm of `input`, but a JSON-encoded array
arrives as a plain string and `normalizeInput` only expands it after
validation — so neither the 1,000-input cap nor the non-empty checks applied
to the reference-expression path the route was written to accept. `"[]"`
also reported success with no vectors. Re-check the normalized list so the
bounds hold for both shapes.

* chore(embeddings): regenerate tool metadata for the new embedding tools

CI's tool-metadata:check gate failed: registering embeddings_openai,
embeddings_gemini, embeddings_cohere, and embeddings_mistral left the
generated tool-ids/metadata/outputs artifacts stale.

* fix(embeddings): project before batching, and keep the sunset block's docs icon

Review round 2.

Projection ran inside callEmbeddingAPI, after batchByTokenLimit had already
measured and truncated the original text. The projector rewrites resolved
secrets to placeholders, which changes length, so batching sized against a
string that was never sent: a lengthening projection then pushed input past
the model's ceiling and the provider rejected it, and a shortening one
discarded document content that would have fit.

Project once up front, then batch the projected text, so truncation measures
what actually goes to the provider. This also keeps projection to exactly one
call per embed(), so no retry can re-project.

Separately, marking the legacy openai block hideFromToolbar dropped it from
the generated docs icon map, which only retains hidden blocks when they are
versioned. integrations/openai.mdx is deliberately kept — docsLink is baked
into every placed instance — so BlockInfoCard lost its icon and fell back to
a text tile. A sunset block keeps its docs page for the same reason a hidden
versioned block does, so the generator now treats it the same way.

The sim-side integrations map still omits it, which is intended: that feeds
the discovery page a sunset block should not appear on, and placed blocks
render from the registry's own icon reference.

* fix(embeddings): override stale block params instead of omitting them

Review round 3.

The generic handler merges the params() result over the original inputs
(`{ ...inputs, ...transformedParams }`), so omitting a key leaves the stale
value in place. The previous round dropped an unsupported taskType or
dimensions by omission, which was therefore a no-op through the executor
path: a reduction or task type chosen for one model still reached the tool
after a model switch.

Rewrite each stale field to an explicit `undefined`, which does override in a
spread.

Same class of bug for `model` itself, which was forwarded whenever present
without checking it belongs to the selected provider. Every provider's model
dropdown shares the `model` id, so switching provider kept the previous
provider's model and failed at the route as a mismatch. It now falls back to
the provider's default unless the saved model actually belongs to it.

Tests assert the merged result rather than the returned object, since the
return shape alone cannot distinguish an omitted key from an overridden one —
which is exactly why the previous fix looked correct and was not.

* fix(embeddings): discount the batch ceiling when the tokenizer is foreign

Review round 4.

Batching measures with tiktoken, which only has encodings for OpenAI models —
every other id falls back to cl100k_base. Gemini's 2048, Cohere's 128k, and
Mistral's 8192 were therefore enforced in OpenAI token units, so an input near
one of those ceilings could still be rejected upstream or trimmed more than
needed.

A true fix needs per-provider tokenizers, which the repo does not have:
estimateTokenCount is a chars-per-token heuristic, and truncation needs a real
encode/decode pair to slice on a token boundary. So the ceiling is discounted
for foreign tokenizers rather than trusted exactly.

The discount is one-sided on purpose. Overshooting means the provider rejects
the whole request; undershooting only trims a text that was already at the
limit, so the margin errs toward the second.

resolveBatchTokenCeiling is a pure function tested directly, rather than
inferred from truncation behavior, so the guarantee holds per model as the
catalog grows.

* fix(embeddings): keep the batch ceiling exact and warn before truncating

Review round 5. Reverts the safety margin from round 4.

The two review findings were in direct tension: round 4 flagged that a
foreign model's ceiling is measured in tiktoken units, and the margin added
to absorb that error reintroduced the round 3 harm — valid content truncated
below the provider's declared limit.

The margin was the wrong trade. It swapped a loud failure for a silent one:
an undercount surfaces as a provider rejection the caller can see and act on,
while shortening an embedding's input produces a degraded vector that is
indistinguishable from a good one at every layer above it. Silent quality
loss in a retrieval index is the worse outcome, and it is also the harder one
to ever notice.

So the declared ceiling is applied exactly, and truncation is no longer
silent: an input above the limit now logs a warning naming the model, the
limit, and whether the count was approximate. hasApproximateTokenCount
records which models are counted with a foreign tokenizer without being used
to shrink anything.

The tokenizer imprecision itself remains, and cannot be fixed without
per-provider BPE the repo does not have — estimateTokenCount is a
chars-per-token heuristic, and truncation needs a real encode/decode pair to
slice on a token boundary.

* refactor(embeddings): drop dead surface and enforce OpenAI's item cap

Audit follow-ups on the multi-provider embeddings work:

- Enforce OpenAI's documented 2048-entry `input` array cap in the OpenAI and
  Azure adapters. Nothing bounded item count on the OpenAI path — batching
  bounds tokens per request, so a batch of many short inputs could exceed it.
- Make the provider item cap single-source. It was declared both on the catalog
  entry and on the adapter, read through a `??`; the adapter is the wire-protocol
  owner, so the catalog copy is gone.
- Have the knowledge-base view call `getKbEligibleModels()` instead of
  re-deriving the same `kbEligible` filter inline.
- Remove dead surface: the unused `EMBEDDING_TASK_TYPES` constant,
  `EmbeddingToolDefinition`, `HOSTED_KEY_PROVIDERS`, and the five request-body
  fields (`workspaceId`, `workflowId`, `executionId`, `userId`,
  `useHostedCostTracking`) the route never reads.
- Trim `@/lib/embeddings` to what callers outside the module use.
- Drop the route's manual request-id plumbing; `withRouteHandler` supplies it.
- Fix two comments that had drifted onto the wrong declaration.

* fix(embeddings): normalize reduced Cohere output; correct OpenAI token ceiling

Second validation pass against provider documentation.

- Cohere: normalize locally when `output_dimension` reduces below native.
  Cohere documents the parameter as Matryoshka truncation but never states that
  it renormalizes, and an unnormalized vector silently skews cosine similarity.
  `l2Normalize` is idempotent, so this is a no-op if Cohere already returns unit
  vectors and a correctness fix if it does not. Covered by a test that fails
  without it.
- OpenAI: raise the per-input ceiling from 8191 to the 8192 the API reference
  documents, so a maximal input is no longer truncated by one token.
- Share the OpenAI response type with the Azure adapter instead of declaring an
  identical copy, mirroring how the mail providers share `_nodemailer`.
- Rewrite the Gemini item-cap comment to say the 100-item limit is observed
  rather than documented, which is what Google's reference actually supports.

Docs: add a manual intro to the Embeddings page covering providers, models,
inputs, outputs, and comparability rules. The generated Input tables are empty
because `createEmbeddingTool` builds params programmatically and the docs
generator only reads literals, so the manual section carries that reference.

* fix(embeddings): split per-input and per-request token limits; close provider gaps

Four gaps found in the validation pass.

Gemini token counts were estimated, not measured. `BatchEmbedContentsResponse`
carries `usageMetadata.promptTokenCount`; without reading it the client fell back
to tiktoken, which has no Gemini encoding and silently used `cl100k_base` — the
wrong tokenizer on a count knowledge-base runs bill against.

`maxInputTokens` was doing two jobs: the per-input ceiling that decides
truncation, and the per-request budget that decides how many inputs share a
batch. These are different provider limits, and conflating them meant Cohere
packed batches against its 128k per-document ceiling while OpenAI's documented
300,000-token request cap went unenforced. They are now separate fields.

Truncation moves out of `batchByTokenLimit` and into `embed`, so it happens once,
against the per-input ceiling, and always logs. The request budget is floored at
that ceiling — a budget below it would truncate inputs the provider accepts.
Batch sizes are unchanged everywhere except Gemini, which rises from 2048 to the
8192 the other providers already used.

codestral-embed now offers its documented 3072 maximum. Its API default is 1536,
so the offered sizes straddle the default; the catalog invariant relaxes from
"native size first" to "native size present", which is what the block relies on.

The Mistral API-key field no longer differs from the other three. Sim stocks
`MISTRAL_API_KEY` — `mistral_parse` already hides its key field on hosted — so
one field with `hideWhenHosted` replaces the conditional pair.

Docs: correct the API-key row, which described the old Mistral-only behavior.

* refactor(embeddings): derive block options from the catalog; use shared helpers

Findings from a four-angle quality review.

Reuse: `splitByItemLimit` and `processWithConcurrency` were reimplementations of
`chunkArray` (`@sim/utils`) and `mapWithConcurrency`
(`@/lib/core/utils/concurrency`), so `lib/embeddings/batching.ts` is gone. That
helper's doc forbade a throwing mapper; embedding legitimately wants a failed
batch to fail the call, since a partial vector set is not a usable result, so the
contract is reworded to cover both intents rather than forked.

The block no longer hand-copies the catalog. Its model, task-type, and dimension
dropdowns are derived from `EMBEDDING_MODELS`, which deletes roughly 150 lines of
literals that had to be kept in step by a drift test. The comment claiming this
was impossible was wrong: `generate-docs.ts` only reads `subBlocks` looking for
an `id: 'operation'` entry, which this block does not have. Verified by
regenerating — `embeddings.mdx` and `integrations.json` come out byte-identical.

Single-sourced two maps that were stated twice: BYOK provider ids (which encode
the non-obvious gemini -> google mapping) and the per-provider default model.
The route previously took its default from `getModelsForProvider(provider)[0]`,
which silently depended on catalog key order.

Azure's `endpoint` and `apiVersion` are required on their own context type
instead of optional on the shared one, so the adapter can no longer be built
without them and emit an `undefined/...` URL.

Also: contract enums now `satisfies` the catalog unions so they cannot drift,
the barrel exports only what callers outside the module use, the redundant
`requestedDimensions` field is a parameter, the bare `getEmbeddingModelInfo()`
call is a named `assertKbEmbeddingModel`, and the route checks payload size
before scanning entries rather than copying the body first.

* docs(embeddings): correct comments that drifted from the code

A comment pass over the feature found four that no longer matched what they sat
on, all introduced by earlier rounds of this work.

The contract's `satisfies` note promised that adding a catalog provider could
not leave the wire enum stale. It cannot deliver that: `satisfies` proves every
listed member is valid, not that the list is exhaustive, so an addition stays
silently absent. Reworded to say what it does and does not catch.

The client cited Gemini as a provider that omits usage, which the Gemini adapter
now contradicts — it reads `usageMetadata.promptTokenCount`. Every adapter
defines `parseTokens`, so the fallback is about a response lacking a usage block,
not about a particular provider.

`l2Normalize` documented only Gemini, though Cohere now calls it for a different
and stronger reason, and "normalizes in place" read as mutation when the function
returns a copy.

The route's new size-guard comment claimed it avoids copying the payload; nothing
there copies. The real reason is that summing lengths gates before the per-entry
character scan.

Also: split the derived-sub-block TSDoc so both constants carry hover text, gave
the payload cap its own doc, dropped one comment that restated a signature, and
tightened two long blocks without losing a fact.

* fix(docs): generate tool inputs for factory-built tools

The four embeddings tools rendered header-only Input tables. `extractToolInfo`
finds a tool's `params` by regex over the tool's own file, and these files hold
nothing but a `createEmbeddingTool({...})` call — the params live in the
factory's module. There was already a fallback for a same-file `...spread` base,
so this adds the cross-module equivalent: follow the factory's import and read
`params` from there.

Two things surfaced once the tables populated.

`hosting` was not in the set of keys that terminate the `params` capture, so the
non-greedy match ran past it to `request:` and swallowed the whole hosting block.
Every tool with a `hosting:` section between `params:` and `request:` was
publishing `pricing` and `rateLimit` as if they were user-facing inputs — this
drops those rows from eight unrelated integration pages as well.

The shared apiKey description was a template literal, which the regex emitted
verbatim as `${name} API key`. It is now a static string, matching how every
other tool in the repo declares one.

Docs: the Embeddings page keeps a prose intro in its MANUAL-CONTENT block like
other integrations, with the hand-written input/output tables removed now that
the generated ones are correct. The sunset `openai` page loses its
`encodingFormat` row — page generation skips hidden blocks, so that page is
frozen and would otherwise keep advertising a parameter the aliased tool no
longer accepts.

---------

Co-authored-by: Waleed Latif <walif6@gmail.com>
2026-08-06 15:25:17 -07:00
Waleed dcaa118752 improvement(docs): restructure sidebar, align chrome, rename Mothership to Chat (#6296)
Sidebar: 11 separator groups become 5, with each module a collapsible folder
that auto-opens on the active page. 61 always-visible rows drop to 16. Groups
mirror the app's own nav (Chats/Workspace/Workflows) rather than inventing a
taxonomy; Enterprise and Self-Hosting are hoisted out of Platform.

Chrome: register the `hover-hover` variant, without which every @sim/emcn hover
state silently compiled to nothing; restore the sidebar's Geist font stack; add
11 emcn tokens that were falling back to currentColor; adopt the named type
scale; align row geometry, hover tokens and group labels with the app.

Rename: mothership/ -> chat/ with redirects for the old URLs. Asset paths,
the @mothership.sim.ai domain and the `mothership` log-trigger enum value are
deliberately left alone -- they are CDN objects, a real domain, and a live
product value.

Also removes the page-type badge, drops the "Next" heading from the ToC, and
lets FAQ rows open independently so expanding one no longer shifts the page.
2026-08-05 13:49:17 -07:00
Waleed c14818b7bb improvement(emcn): normalize the chevron geometry and consolidate consumers (#6258)
* improvement(emcn): normalize the chevron geometry and consolidate consumers

The sidebar folder arrow read as much larger than the icons beside it. It was
measurably so: `ChevronRight` was a triple outlier in the icon set.

| | ChevronRight | house standard |
|---|---|---|
| viewBox | `0 0 6 10` | 24-based — 149 of 173 icons |
| glyph fill | 80% of the box | 54% median |
| strokeWidth | 1.2 | 1.55 — 153 of 173 |
| cap/join | square/miter | round/round — 175/177 |

Because the box was tight-cropped, a square `size-[16px]` scaled the glyph to
12.8px tall where a standard icon shows ~8.6px — about 50% larger, with a
relatively ~85% heavier stroke. The lucide icon it replaced was a 24-box at 50%
fill, which is why this only appeared after the migration.

`chevron-down`, `chevron-right` and `chevron-left` are rebuilt as the exact
mirror and transpose of `chevron-up`, which already sets the house standard, so
their optical weight is identical to that sibling by construction rather than by
eye.

Changing the viewBox is not safe on its own: 36 call sites sized these to the
old tight aspect (`h-[7px] w-[9px]`, `h-[6px] w-[10px]`, ...), which would
letterbox against a square box. All of them move to `size-[14px]`, the documented
default and the app's dominant size (212 uses). Every one of the 94 chevron call
sites is now square-sized.

Two of those were only reachable through indirection and would have regressed
silently: `STYLES.chevron` in the terminal's structured output, and the
sidebar-section chevron inside a multi-line `cn()`. The dropdown submenu chevron
carried no size at all and was relying on the icon's intrinsic 6x10 — it would
have jumped to 24x24.

Also unifies `folder-input`, which carried both 1.55 and 2 within one icon.

Docs consolidation, same theme — it was forking shared components:
- `SidebarChevron` was a private inline copy of the old 6x10 chevron; it now
  wraps the shared `ChevronRight`.
- `ThemeToggle` inlined lucide's sun and moon at strokeWidth 1.5; both now come
  from `@sim/emcn/icons` at the house 1.55.

Docs inline `<svg>` files drop from 9 to 7; the remainder are the brand icon set,
the logo, OG-image generation and bespoke hand-positioned shapes.

Left alone: 16 icons whose stroke or box still differs. They are fill-based
glyphs and brand marks (`sim`, `wordmark`, `folder`, `more-horizontal`, ...)
where changing the stroke means redrawing the icon — that wants visual review,
not a sweep.

Verified: 23/23 type-check (--force), biome, api-validation, 18361 tests, and
production builds of both apps.

* docs(emcn): correct the chevron geometry left in two comments

ChipChevronDown's TSDoc still described centring a 10x6 glyph, and the
enterprise sidebar's chip-parity comment still cited a 6x10 chevron. Both
now read 14px, matching what the components actually render.
2026-08-04 12:38:10 -07:00
Waleed 3d8b2ed050 improvement(docs): inherit the platform border and font-weight scales (#6252)
* improvement(docs): inherit the platform border and font-weight scales

#6241 consolidated the app's neutral border tokens and flattened its
font-weight scale. `apps/docs` was carrying an untouched copy of the
pre-migration values, so the two have visibly drifted — the docs `@theme`
block already declares it "mirrors apps/sim/tailwind.config.ts", so the
drift is against stated intent rather than a deliberate divergence.

Borders — same consolidation as the app:
- `--border` #dedede -> #d8d8d8 (light), #333333 -> #444444 (dark)
- `--border-1` and `--border-muted` become aliases of `--border`, so the 19
  existing `var(--border-1)` consumers pick up the unified colour without
  being touched
- `--divider` is retired; its single consumer moves to `--border`

Font weights — the arbitrary values the app dropped:
- `font-[480]`/`font-[470]`/`font-[500]` -> `font-medium`, `font-[430]` ->
  `font-normal`, `font-[600]` -> `font-semibold`

The navbar's active tab and its invisible width-reserving ghost both used
`font-[480]`; they move to `font-medium` together, so the anti-layout-shift
trick still holds.

Deliberately NOT ported: the app's `--border-width` hairline (0.5px on hi-dpi).
The app wires it through `borderWidth.DEFAULT` in a Tailwind v3 JS config; docs
is Tailwind v4 CSS-first, which hardcodes `border: 1px` in the utility with no
theme key, so matching it means overriding a Tailwind utility. That is a
site-wide visual change and wants its own PR with visual review.

Also unchanged: the inline SVGs. `components/icons.tsx` (328) is the brand and
integration set, `sim-logo` is a brand mark, and the handful of remaining
shapes are bespoke and positioned by hand. Docs already consumes
`@sim/emcn/icons` in the 15 places where a shared icon is the right call, and
imports zero lucide.

* improvement(docs): finish the platform token sweep

Follow-on within the same PR. A full comparison of every custom token docs
rolls against the platform found three more classes of drift.

Text scale — #6241 retuned these and docs kept the old values:
- `--text-body` #3b3b3b -> #434343 (light), #cdcdcd -> #c1c1c1 (dark)
- `--text-muted` #707070 -> #7a7a7a (light), #787878 -> #6e6e6e (dark)
- `--text-icon`  #525252 -> #5a5a5a (light), #a0a0a0 -> #969696 (dark)

Docs and the app now agree on all 62 shared token names, with zero divergent
values.

Missing shared-component tokens. Docs renders `@sim/emcn` (Badge, Chip,
ChipLink) and `@sim/workflow-renderer` (block, subflow and note views), but
never defined 14 of the tokens those components reference — an undefined
`var()` silently falls back to `currentColor`, so the failure is invisible
until the branch that uses it renders. Several are live: `--warning` on an
edited subflow, `--caution` on inline code inside a note, `--text-placeholder`
on an empty note, `--border-success` on a successful run edge. Added with the
app's values, along with `--text-icon-muted` (new in #6241) and the four Badge
palettes docs lacked (teal, cyan, pink, blue-secondary), so any variant renders
correctly rather than being one prop away from black.

Type scale — docs declares micro/xs/caption/small/base/md in `@theme` but 20
call sites bypassed it with identical raw values (`text-[13px]`,
`text-[0.8125rem]`, `text-[12px]`, `text-[15px]`, `text-[10px]`). Each now uses
the token; every value is byte-identical, so this is a rename, not a restyle.
The class reordering in the same files is biome's `useSortedClasses` reacting
to the rename — verified as a pure permutation, with the class multiset
unchanged in every file.

Deliberately left alone:
- `#33C482` / `#2FB3FF` — brand-mark SVG fills and default props in demo data,
  not styling.
- The Ask AI button's inverted `#383838`/`#575757`/`#e0e0e0`/`#cfcfcf`. The
  platform's `--surface-inverted`/`--surface-inverted-hover` hold *different*
  values, so adopting them would restyle the control rather than tokenize it.
  Worth doing, but as a visual change with review.
2026-08-04 10:54:02 -07:00
Emir KarabegandWaleed Latif 9b9da81a27 improvement(platform): drop lucide-react for the in-house icon set, flatten the type and border scales, and retire scheduled tasks and workflow references (#6241)
* border styling

* improvement(platform): migrate off lucide-react, flatten the font-weight scale, and retire scheduled tasks and workflow references

* chore(platform): drop the dead schedule client layer and repair stale rule and skill docs

Follow-up cleanup for the platform commit, which removed the workspace
scheduled-tasks surface and migrated off lucide-react. Both left dead tails
that type-check clean, so nothing flagged them.

Six mutation hooks in hooks/queries/schedules.ts lost their only consumer when
the scheduled-tasks page was deleted: useDisableSchedule, useResumeSchedule,
useDeleteSchedule, useExcludeOccurrence, useUpdateSchedule, useCreateSchedule.
They are removed along with the three contract objects that served only them —
disableScheduleContract, excludeOccurrenceContract, deleteScheduleContract.

disableScheduleBodySchema and excludeOccurrenceBodySchema are deliberately
kept: both are members of scheduleUpdateSchema, the discriminated union the
live PUT /api/schedules/[id] route parses. Dropping them would collapse the
union and 400 the disable and exclude_occurrence actions.

The schedule-calendar tree and its utils stay unmounted for later reuse. Its
TSDoc now says so, since it has no importer and would otherwise read as dead
code on the next sweep.

The add-enrichment skill templated an import from lucide-react, a dependency
the platform commit deleted, so running it produced an unresolvable import. It
now points at @sim/emcn/icons, matching all five shipped enrichments. The
emcn-design-review skill and several rule files still pointed at
apps/sim/components/emcn/**, which moved to packages/emcn/**.

Also corrects the documented Chip variant list — it advertised a ghost variant
that never existed and omitted border — repoints the sim-url-state date-parser
example at an inline snippet now that its source file is gone, and normalizes
the one strokeWidth the icon migration left at 1.5 in bubble-chat-delay.

* fix(platform): mark the resource chrome as client components

`skills/page.tsx` is a Server Component, and this branch moved its
`IntegrationTabsHeader` import onto the `@/app/workspace/[workspaceId]/components`
barrel. That barrel re-exports `SortDropdown` from `resource-options`, which
calls `useState`, so the server graph now reaches a client-only module and
`next build` fails. `resource-header` has the same latent problem (`useState`,
`useEffect`, `useRef`).

Both files are genuinely client components, so they get the directive rather
than the page dropping the barrel import — local feature barrels are the
convention here.

Also drops a stale `lucide-react` mention now that the dependency is gone.

* chore(scheduled-tasks): remove the scheduled-task logic

Scheduled tasks are retired. This removes the `sourceType = 'job'` half of
`workflow_schedule` from the application, leaving the workflow Schedule
trigger (`sourceType = 'workflow'`) untouched.

Gone:
- the job orchestration layer (`lib/workflows/schedules/orchestration.ts`)
  and the agent-job runner in `background/schedule-execution.ts`
- the job claim/dispatch half of the schedules execute tick
- POST /api/schedules (job creation) and the job branches of
  GET /api/schedules and PUT/DELETE /api/schedules/[id]
- the copilot job tools and handlers, the `scheduledtask` resource type and
  chat-context kind, and the VFS `jobs/` materialization
- the scheduled-task analytics events and the job variant of the
  schedule-disabled email

Kept on purpose: `scheduled-tasks/components/schedule-calendar/**` and
`scheduled-tasks/utils/**`, which the agents module will reuse.

`packages/db/schema.ts` is deliberately untouched — the columns stay for now
and come out in a follow-up with a proper expand/contract migration.

The generated copilot catalog and VFS snapshot types are regenerated from
the matching copilot PR, which removes the tools and the `jobs` snapshot
field at the source.

Verified: 23/23 type-check, biome, api-validation, production build, and the
full vitest suite (18361 passing; the one failure in
executor/handlers/pi/cloud-review-tools.test.ts predates this branch).

* fix(sidebar): derive the settings and switcher widths from SIDEBAR_WIDTH

This branch moved `SIDEBAR_WIDTH.DEFAULT` from 248 to 238 but left two
hardcoded `248px` chrome widths behind, so both sat 10px wider than the live
sidebar:

- the workspace-switcher menu, which is meant to line up with the sidebar
  column it drops out of
- the standalone settings sidebar, whose own comment says to keep it in step
  with the in-workspace chrome

Both now read `SIDEBAR_WIDTH.DEFAULT` directly rather than repeating the
number, so the next change to the constant cannot leave them stale again.

* fix(schedules): stop the API accepting actions it no longer handles

Adversarial pass on the scheduled-task removal found a real regression in
PUT /api/schedules/[id].

Removing the job-only `update` and `exclude_occurrence` handlers left them in
`scheduleUpdateSchema`, so those bodies still parsed. The handler chain is
`disable` first and then an unguarded fall-through to reactivate, so an
`action: 'update'` request would have silently REACTIVATED the schedule
instead of being rejected.

Both actions are dropped from the discriminated union, so `parseRequest` now
rejects them with a 400. Their bodies, response types and the orphaned
`createScheduleContract` (its POST route is gone, and nothing imported it)
go with them.

* chore(landing): retire the scheduled-tasks marketing surface

The feature is gone from the product, so the marketing pages stop selling it.

- deletes the `/scheduled-tasks` landing page and its calendar-loop hero, and
  the `LandingPreviewScheduledTasks` panel
- drops the view from the landing preview: the `SidebarView` member, the nav
  entry and its now-unused Calendar icon, the callout label, both render
  branches, and the staged chat copy in `workflow-data`
- removes the navbar and footer links and the sitemap entry
- removes the route from `LANDING_ROUTES`, the COEP exemption list that must
  list every `app/(landing)` route

`/scheduled-tasks` is indexed, so it 301s to `/workflows` rather than starting
to 404 — that is the surface that still carries scheduled execution via the
workflow Schedule trigger.

Left alone deliberately: `demo-scheduler` is the Cal.com booking embed for the
demo page, unrelated to this feature, and the scheduling library article is a
generic SEO piece that never pitched it.

* perf(chat): stop the resource picker fetching schedules it no longer shows

Dropping the `scheduledtask` group from the add-resource dropdown left
`useWorkspaceSchedules` behind, so the picker still issued a workspace
schedules request whose result never reached a group.

Worse than a wasted request: `schedulesPending` was still in the hydration
gate, so the whole picker waited on that response before it could settle, and
`schedules` was still a `useMemo` dependency, re-running the group build when
it resolved.

The hook and its route stay — `/api/schedules?workspaceId=` still correctly
lists workflow schedules, unlike `createScheduleContract`, whose route this
branch removed.

* chore(scheduled-tasks): drop the leftovers the removal stranded

An independent audit of the branch turned up dead code and stale docs that the
compiler cannot see — nothing behavioural, but all of it rots silently.

- README still sold the feature: the "Scheduled tasks" tile, the prose listing
  it as a workspace surface, and the now-unreferenced screenshot. The landing
  surface went in c61770a8c; this tile was missed.
- `resource-content.tsx`: `SCHEDULE_STATUS_LABEL`, `formatScheduleInstant` and
  `ScheduledTaskField` were orphaned when the schedule render branch went.
- `computeNextRunAt`: zero callers, including tests — its only consumer was the
  removed agent-job runner.
- `applyScheduleUpdate`'s `allowCompleted` option: no call site passes it, and
  its comment described self-completion, which no longer exists. The guard stays
  (legacy `sourceType='job'` rows still carry `status='completed'` until the DB
  follow-up); it is simply unconditional now.
- Three TSDoc blocks still described a create-job route and "opening a
  scheduled-task artifact".

Type-check re-run with --force, since a cached turbo replay is not a check.

---------

Co-authored-by: Waleed Latif <walif6@gmail.com>
2026-08-04 10:28:00 -07:00
mzxchandraandWaleed Latif 87aeca6f0c feat(zoho-desk): add Zoho Desk integration (#6157)
* feat(zoho-desk): add Zoho Desk integration

Add a full Zoho Desk integration: tools, block, icon, and a webhook trigger.

Tools (tools/zoho_desk): list/get/update tickets, list/add comments,
list/get threads, get contact, list organizations, and download attachments
as UserFiles via an internal route. Registered in tools/registry.ts.

Block (blocks/blocks/zoho-desk.ts): operation dropdown, OAuth credential,
an organization selector backed by GET /organizations, per-operation fields,
and BlockMeta templates. Wires the Zoho Desk trigger.

OAuth (zoho-desk provider): authorize/token at accounts.zoho.com with
access_type=offline + prompt=consent; the Desk REST base is derived from the
token response api_domain and persisted so calls honor data residency instead
of assuming desk.zoho.com. Every call sends Authorization: Zoho-oauthtoken and
the orgId header.

Trigger + webhook handler (triggers/zoho_desk, lib/webhooks/providers/zoho-desk.ts):
Sim creates and tears down the Zoho Desk webhook subscription. Inbound events
are verified with JWT RS256 (X-ZDesk-JWT) against the data-center JWKS, ACKed
via the durable queue to meet Zoho's 5s deadline, and fail loudly on
Free/Standard editions that cannot create webhooks.

* fix(zoho-desk): OAuth PKCE, DC scope-marker parsing, SSRF, and e2e fixes

OAuth: forward code_verifier in the custom getToken (PKCE is enabled, so the
exchange must echo the verifier or Zoho rejects the request with invalid_request).
Surface Zoho's error/error_description, which it returns in the JSON body with
HTTP 200, instead of collapsing every failure into "no access token".

Data-center base parsing: better-auth persists Zoho's scopes comma-joined with no
spaces, so the greedy \S+ marker regex swallowed the whole scope list into the
host. Stop the capture at a comma or whitespace in both read sites (token route
and webhook handler), so apiDomain resolves to the real Desk host.

Attachment SSRF: replace the permissive host regex (which accepted attacker
domains like zoho.attacker.com) with a strict Zoho-apex suffix allowlist.

Block: guard Number() pagination so a non-numeric typo can't send NaN; add the
ignoreSourceId -> sourceId loop-guard header to update_ticket (matching add_comment).

Organizations route: surface fetch/Zoho failures with a real status instead of a
200 with an empty list, so the org selector no longer fails silently.

* fix(zoho-desk): webhook creation, attachment naming, and HTML content handling

Webhook trigger (verified end-to-end against a live Enterprise org):
- Omit ignoreSourceId; Zoho rejects a non-Zoho UUID with INVALID_DATA. Drop
  the generateId() fallback and its providerConfig persistence.
- Answer Zoho's create-time notification-URL probe via the existing pending
  webhook verification mechanism (GET/HEAD matchers) so subscription creation
  no longer 405s.
- mapZohoWebhookError now surfaces Zoho's real errorCode / message / field
  errors instead of a catch-all edition message, and attaches an HTTP status so
  4xx flow through NonRetryableDeploymentError while 429/5xx stay retryable.
- Propagate the real status through deploy.ts so failed creates don't retry-loop.

get_attachment polish:
- Return the downloaded file's name under `name` (ToolFileData key) instead of
  `filename`, and derive it (explicit -> Content-Disposition -> URL segment ->
  fallback) so attachments are no longer stored as "untitled".
- Gate the add_comment-only `contentType` param so it isn't sent to get_attachment.

HTML content handling (Zoho content fields emit raw HTML):
- Add a Zoho-local html-to-text converter mirroring the Outlook dual-field
  pattern: when contentType is 'html', derive a plain-text `contentText`
  alongside the untouched raw `content` + `contentType`; plainText mirrors.
- Apply to comments (list/add), threads (list/get), the ticket description
  (descriptionText), and the webhook trigger payload.

Trigger org selector: Organization is now a credential-scoped combobox that
lists the connected account's Zoho Desk organizations.

* fix(zoho-desk): review round - DC-base derivation, org-loader resilience, batched-event visibility

- deriveZohoDeskBaseFromApiDomain: preserve an already-regional desk.zoho.<tld>
  api_domain instead of falling back to the US (.com) data center, and map the
  DC TLD from any zoho(apis).<tld> host - keeps Desk calls in the right data
  center for residency.
- fetchZohoDeskOrganizationOptions: wrap the token/org fetch in try/catch and
  degrade to an empty list (the org field is a free-text combobox, so manual
  entry still works) instead of hard-failing the selector on token/DC/network
  errors.
- formatInput: warn (not silently drop) if Zoho ever delivers more than one
  event in a single payload.

* fix(zoho-desk): harden attachment download against redirect-based SSRF/token leak

Replace the raw fetch in the attachment route with secureFetchWithValidation
(the same guarded fetch the copilot file-download tool uses). The download URL
is user/LLM-influenced and Zoho may redirect, so auto-following redirects could
send the OAuth token / orgId to an untrusted or internal host. The guarded fetch
pins the resolved IP, blocks private/reserved targets on every hop, drops the
Authorization header if a redirect leaves the origin (stripAuthOnRedirect), and
enforces the 50MB cap while streaming. The strict Zoho apex allowlist still
gates the initial origin as defense in depth.

* fix(zoho-desk): only add the edition hint when Zoho's error indicates it

mapZohoWebhookError appended the "requires Professional edition or higher"
guidance to every 403, but a 403 can also mean a wrong org, a missing scope, or
a bad token. Gate the hint on Zoho's own errorCode / message matching the
permission/edition pattern instead of the bare status, so unrelated 403s surface
Zoho's real reason without the misleading suffix. Adds a test for the
non-edition 403 path.

* fix(zoho-desk): stop duplicating /api/v1 when resolving a relative attachment href

A relative attachment href that already starts with `api/v1` (as Zoho's hrefs
often do) was concatenated onto getZohoDeskApiBase (which ends in /api/v1),
producing `/api/v1/api/v1/...` and a failing download. Extract a tested
resolveZohoAttachmentUrl helper that uses absolute hrefs as-is and strips a
leading slash + `api/v1/` prefix from relative ones before joining, so the path
is correct for absolute, root-relative, and api/v1-prefixed hrefs alike.

* fix(zoho-desk): reject an empty update_ticket PATCH with a clear error

update_ticket built its PATCH body from optional fields via filterUndefined, so
a call with no fields set sent `{}` and surfaced an opaque Zoho failure. Guard
the body builder to throw an actionable "provide at least one field" error
before the request. Adds a test for the empty and populated body paths.

* fix(zoho-desk): fall back to the credential Desk domain in webhook JWT verify

verifyAuth chose the JWKS host from providerConfig.apiDomain and otherwise
defaulted to the US host (desk.zoho.com), so a non-US webhook row missing
apiDomain would verify against the wrong JWKS and reject legitimate events. When
apiDomain is absent, resolve it from the OAuth credential's __zoho_domain__ scope
marker (mirroring deleteSubscription). The persisted-apiDomain fast path stays
DB-free to respect the 5s delivery deadline. Adds tests for both paths.

* fix(zoho-desk): apply the Zoho host allowlist to the organizations route

The organizations route built its URL from the client-supplied apiDomain and
attached the OAuth token without the https-Zoho-host allowlist the attachment
route already enforced, so a session-access caller could point the server at an
arbitrary origin and leak the token. Extract the shared isZohoHost allowlist and
an assertZohoUrl guard into tools/zoho_desk/utils (two consumers now), guard the
organizations URL before fetching, and refactor the attachment route to reuse
the shared helper. Adds tests for the allowlist and guard.

* fix(zoho-desk): propagate provider 4xx in the stable webhook prepare path

The v2 stable deploy preparation flattened every registration failure (except
path conflicts) to HTTP 500, so a provider-attached permanent 4xx - e.g. Zoho's
edition/validation failures from createSubscription - retried instead of failing
the deploy terminally. Propagate the attached status (`?? 500`), matching the
legacy save path's status-aware mapping so both deploy paths route 4xx through
NonRetryableDeploymentError.

* fix(zoho-desk): make createSubscription config failures non-retryable

createSubscription threw plain Errors (no status) for missing orgId, event type,
or credentials, and for a Zoho success with no webhook id - so the deploy outbox
mapped them to 500 and retried permanent configuration failures. Attach a 4xx
via statusError (400 for missing config/credentials; 422 for the no-id anomaly,
where a retry risks duplicate webhooks) so they fail the deploy terminally like
the mapped Zoho API 4xx responses. Tests assert the 400 status on the guard paths.

* fix(zoho-desk): enrich prevState with contentText symmetrically with payload

formatInput derived plain-text contentText only on payload, so an update event
for a comment/thread left prevState as raw HTML while payload carried
contentText - inconsistent shapes for before/after comparisons. Apply
withDerivedContentText to prevState too. Test asserts both are enriched.

* docs(zoho-desk): regenerate integration docs

Regenerate zoho_desk.mdx from the current tool definitions: removes the stale
add_comment `ignoreSourceId` input row (the field was dropped because Zoho
rejects arbitrary values) and adds the derived `contentText` / `descriptionText`
plain-text fields on comments, threads, and tickets.

* fix(zoho-desk): validate the persisted Desk base against the strict host allowlist

deriveZohoDeskBaseFromApiDomain trusted any host matching `desk.zoho.[a-z.]+`,
so a crafted api_domain like `desk.zoho.com.attacker.com` passed and was
persisted as the credential's `__zoho_domain__` REST base - later receiving the
OAuth token on every Desk tool/webhook call. Gate the derivation on the strict
isZohoHost apex allowlist (which rejects that lookalike), extracted with
assertZohoUrl into a dependency-free host-allowlist module so the auth
token-exchange path validates hosts without pulling in the tool utilities. The
attachment and organizations routes now import the shared guard from there.

Also: formatInput now emits the normalized null trigger shape for an empty/
malformed event array instead of leaking a raw `[]` to downstream steps. Tests
cover the empty-array shape and the lookalike-host rejection.

* fix(zoho-desk): correct API field names, scopes, and host validation

Validation pass against Zoho's published Desk API surfaced six defects that
typecheck, lint, and the existing suite all passed over, because each one fails
silently against the live API rather than erroring.

Wire-name mismatches (Zoho ignores unknown keys, so all three were silent):
- update_ticket sent `customFields`; the ticket PATCH body names it `cf`.
  `customFields` exists only as a deprecated alias on other Desk resources and
  on the separate validate-field-updates endpoint, so updates reported success
  and applied nothing.
- ZOHO_DESK_TICKET_PROPERTIES and ZOHO_DESK_CONTACT_PROPERTIES advertised a
  `customFields` output; both resources return `cf`. The declared field always
  resolved undefined and the real one was undeclared.
- list_tickets sent `departmentId`; the query param is `departmentIds`, so the
  department filter was dropped and every department's tickets came back.

Content handling:
- deriveZohoContentText matched `contentType === 'html'`, but Zoho spells the
  discriminator per resource: comments use `html`, threads use the MIME form
  `text/html`. Every thread's `contentText` was therefore raw markup - the exact
  opposite of the field's purpose. Now normalized across both spellings,
  parameterized values, and casing, with regression tests.

Scopes (least privilege):
- Desk.tickets.ALL -> Desk.tickets.READ + Desk.tickets.UPDATE. No tool creates
  or deletes a ticket; ALL additionally granted ticket DELETE.
- Dropped Desk.search.READ (no search tool exists) and Desk.webhooks.READ /
  .UPDATE (the provider only creates and deletes), plus their orphaned
  SCOPE_DESCRIPTIONS entries.

Host validation - the webhook provider was the only token-carrying path not
anchored to the Zoho apex allowlist, including the JWKS fetch, where an
unrecognized host would have stood in as the JWT issuer:
- createSubscription, deleteSubscription, and verifyAuth now route their base
  through a shared allowlist check.
- getZohoDeskApiBase validates rather than trusting injection precedence.
- The organizations route uses secureFetchWithValidation with
  stripAuthOnRedirect, matching the attachment route it had diverged from.

Block and trigger:
- The trigger's department field is renamed `triggerDepartmentIds`; sharing the
  `departmentIds` id let a value typed as a list_tickets filter become the
  webhook subscription's filter when switching modes.
- `isPublic` no longer serializes onto all ten operations, matching the existing
  gating for `contentType`.
- from/limit reject negatives and fractions instead of forwarding them.
- update_ticket gains description, resolution, and classification (all already
  declared as outputs), and a departmentId input so a ticket can be moved.

Accuracy corrections to user-facing text, all against the published parameter
tables: `from` is 0-based (0-4999, default 0), not 1-based; per-endpoint limits
are tickets 1-100/10, comments 1-100/50, threads 1-200/100; sortBy lists Zoho's
actual allowed values; the two `include` sets genuinely differ per endpoint;
status and priority accept comma-separated lists.

Also: path IDs are trimmed via requireZohoDeskId so a pasted trailing space
fails with a clear message instead of a %20 404; comment `commenter` and thread
`status`/`isDescriptionThread`/`visibility`/`canReply` are now declared;
ZOHO_CLIENT_ID/SECRET added to the oauth test env; docs page gains a
MANUAL-CONTENT intro covering capabilities, the Professional-edition webhook
requirement, and the US-data-center limitation.

Not verified from documentation, needs a live account before merge:
- the OAuth scope for the attachment content sub-path (Zoho publishes none, and
  there is an unanswered SCOPE_MISMATCH report against it)
- 12 of the 17 offered webhook event ids (5 are confirmed); Ticket_Delete is
  documented but not offered
- the ticket `descriptionContentType` key, and the POST /api/v1/webhooks body
  shape, neither of which appears in any reachable Zoho reference

* chore(zoho-desk): regenerate tool metadata

The param and description corrections in the previous commit changed the
generated tool surface, so tool-metadata:check failed in CI. Regenerated;
the diff is two Zoho-only lines.

* fix(zoho-desk): stop posting null for untouched update_ticket fields

`filterUndefined` strips only `undefined`, but an untouched subBlock never
arrives as `undefined`: the workflow serializer initializes every subBlock value
to `null` (stores/workflows/utils.ts) and extractBlockParams writes those nulls
straight into tool params, with nothing between the serializer and request.body
filtering them.

Reproduced against the real serializer and block with only `status` set:

  basic     {"subject":null,"status":"Closed"}
  advanced  {"subject":null,"status":"Closed","priority":null,...,"cf":null}

`subject` leaks even in basic mode because it declares no `mode`, so
shouldSerializeSubBlock never drops it. Zoho documents subject as a writable
field, so every status-only edit either failed the PATCH or blanked the ticket's
subject; in advanced mode the whole update surface nulled out, including `cf`.

Two things hid this. The empty-PATCH guard was unreachable from the block (the
body always carried at least `subject`), and the existing test called buildBody
with fields *absent* rather than null - the shape the block never produces - so
it could not fail on the real path.

Replaces filterUndefined with a local omitUnset that drops undefined, null, and
'' (a cleared input means "leave unchanged", not "set to empty"). Adds three
tests using the real serializer shape, all verified to fail before the fix.

Also fixes the same null-blindness in the block's param mapping, where
Number(null) === 0 injected from=0 on every operation, and corrects the shared
limit placeholder, which claimed max 100 while list_threads allows 200.

* feat(zoho-desk): add Self Client service-account credential

Adds a second way to connect Zoho Desk, alongside the interactive OAuth flow: a
Zoho Self Client, pasted as client id + client secret + organization id. Built
on the existing client-credential-accounts framework rather than a new credential
path, so it behaves like the Zoom Server-to-Server and Box CCG accounts already
in the repo - a short-lived token minted on demand, no refresh token.

Two Zoho behaviors the generic framework does not cover:
- `scope` must be COMMA-separated on Zoho's token endpoint; a space-separated
  list is rejected as an invalid scope. The list comes from
  getCanonicalScopesForProvider('zoho-desk'), so the Self Client and the OAuth
  flow can never drift apart on scopes.
- Zoho reports OAuth failures in the JSON body, frequently with HTTP 200
  (e.g. {"error":"invalid_client"}), so the success body is inspected for an
  `error` field before the token is read - a status-only check would accept a
  failed mint.

deriveZohoDeskBaseFromApiDomain moves out of auth.ts into the dependency-free
host-allowlist module so the minter and the OAuth path share one derivation
instead of duplicating it, and the mint response's api_domain now flows through
to tools as `apiDomain` (the SA branch of the token route previously returned
none, so SA calls would have assumed desk.zoho.com).

Docs: hand-authored zoho-desk-service-account.mdx following the existing
*-service-account.mdx pages, registered in meta.json and in the generator's
keep-list so stale-page cleanup does not delete it.

Known limitation, documented in the descriptor helpText and the docs page:
webhook triggers still require an OAuth connection. Webhook provisioning resolves
credentials through getCredentialOwner/refreshAccessTokenIfNeeded, which is
OAuth-account-only for every provider in the repo - not a Zoho-specific gap.

Unverified from documentation, needs a live Zoho org before merge:
- the `ZohoDesk.` soid prefix. Zoho documents only the syntax
  {servicename}.{zsoid} with a single CRM example; no first-party doc states the
  Desk prefix. normalizeZohoDeskSoid passes through any value already containing
  a '.', so an operator can paste a corrected full soid without a code change.
- whether zsoid is the same identifier as the Desk orgId header value.
- whether the client-credentials endpoint accepts Desk.webhooks.CREATE/DELETE
  for a Self Client.
- whether the mint response populates api_domain for Desk (documented for CRM);
  if absent the derivation falls back to the US Desk host.

* fix(zoho-desk): derive descriptionText for ticket-shaped payloads

Cursor Bugbot: webhook ticket events reached workflows as raw HTML with no
plain-text sibling. `withDerivedContentText` only looked at `content` /
`contentType`, but ticket resources carry their body on `description` /
`descriptionContentType`, so trigger output disagreed with get_ticket.

The helper now derives both, which also removed two inconsistencies on the tool
side: get_ticket had its own inline copy of the derivation (now one shared
implementation that cannot drift), and update_ticket returned its PATCH response
raw despite the shared output map declaring descriptionText.

`descriptionContentType` remains the one field name unconfirmed in any Zoho
reference. It degrades safely - an absent key makes deriveZohoContentText return
the value unchanged, so descriptionText mirrors description rather than breaking,
exactly as get_ticket already behaved - and it is now one helper to correct if
Zoho names it differently.

* feat(zoho-desk): let the service account pick its data center

Zoho's accounts server is per region, and the integration pinned every call to
the US host. For the interactive OAuth flow that is currently unavoidable -
better-auth's authorize/token URLs are static per provider - but the service
account mints its own token, so the region can simply be chosen. This makes the
Self Client the only way a non-US Zoho org can connect.

Adds an optional `dataCenter` field to the client-credential framework. Optional
matters: ClientCredentialAccountFieldId and ClientCredentialAccountFields are
shared with Zoom, Box and Salesforce, whose descriptors and minters are
unchanged. Blank keeps the previous behavior (US), so existing credentials are
unaffected.

Only us/eu/in/au are offered - the four regions where both the accounts server
and the Desk REST host are confirmed. CA is deliberately absent: Zoho's accounts
docs say accounts.zohocloud.ca while Zoho's own Desk SDK says accounts.zoho.ca,
and the two cannot both be right. JP/SA/CN/UK lack a confirmed Desk host.

The Desk base is now derived from the selected region rather than inferred from
the mint response, which also removes a dependency on `api_domain` being
populated for Desk (Zoho documents it for CRM only). When `api_domain` IS present
and disagrees with the region, it wins - it is authoritative about where the
token actually works - and the mismatch is logged so a mis-selected region is
diagnosable. deriveZohoDeskBaseFromApiDomain gains a `try` variant returning
undefined so an untrusted api_domain can no longer masquerade as an authoritative
US answer and silently override a correct region.

A wrong region fails loudly rather than silently: the minter runs as verification
on both create and reconnect, so the credential is never persisted in a broken
state. Because Zoho reports it as `invalid_client` - a Self Client only exists on
its own region's accounts server - the operator hint for that code now names the
data center as a candidate cause.

Copy is scoped per path rather than blanket "US only": the OAuth service
description, trigger setup instructions, and the docs intro now say which path
each limitation applies to, and the service-account page documents the four
regions with a sign-in-domain to region-code table.

* fix(zoho-desk): strip ticket description HTML, classify body-reported refresh failures

Final validation pass findings.

descriptionText never stripped anything. It was gated on a
`descriptionContentType` discriminator that Zoho does not send: the Ticket_Add
webhook sample ships `"description": "<div>Description</div>"` with no such key,
and the ticket GET/PATCH response field lists have no content-type sibling
either. So get_ticket, update_ticket, and every webhook ticket payload emitted
descriptionText as a byte-identical copy of the raw HTML, while the declared
output promised stripped text.

The tests did not catch it because they fabricated the shape - both fixtures
constructed `descriptionContentType: 'html'`, a key Zoho never emits, proving the
branch works without proving it is ever taken. Ticket descriptions are HTML by
convention, so the strip is now unconditional (html-to-text is a near-identity on
genuinely plain text), an explicit descriptionContentType is still honored if
Zoho ever adds one, and the fixtures now use Zoho's real shape with no
content-type key anywhere.

A body-reported refresh failure was unclassified. Zoho answers a revoked refresh
token with HTTP 200 and `{"error":"invalid_client"}`; refreshOAuthToken only
checked `data.ok === false` (a Slack-ism), so the request fell through to the
"no access token" guard and returned no errorCode. isTerminalRefreshError could
therefore never recognize invalid_client as terminal, the credential was never
marked dead, and every later execution retried a refresh that cannot succeed -
with the user shown "No access token in refresh response" instead of a reconnect
prompt. The body is now classified before the status is trusted, matching what
the token exchange and the service-account mint already did. That guard also
stopped logging the whole response body, which carries live tokens on a partial
success.

Also: an unrecognized dataCenter now fails with a named error instead of quietly
resolving to US and surfacing as an opaque invalid_client (blank still means US);
the webhook JWKS cache is bounded, since its key derives from a providerConfig
field that SYSTEM_MANAGED_FIELDS protects from diffing but not from being
written; and the attachment `size` output no longer asserts bytes, a unit Zoho
documents as KB.

* feat(zoho-desk): canonical selectors and BlockMeta skills

The block picked its organization with an ad-hoc `combobox` + `fetchOptions`.
Only five blocks in the repo did that, and the other four are core blocks
(agent/credential/function/logs) - no other OAuth integration used it. Every
other resource a user has to identify was a bare short-input taking an opaque
numeric id.

Zoho Desk now uses the same machinery as the other 25 selector providers:
hooks/selectors/providers/zoho-desk/selectors.ts registered in the selector
registry, consumed from the block as basic selector + advanced manual input
sharing one canonicalParamId, for organization, update-ticket department, and
the list-tickets department filter. The trigger's org field moves to the same
selector. zoho-desk-org-options.ts is deleted rather than left beside the new
path, so blocks/ has zero fetchOptions usages outside the core blocks.

Wire params are unchanged (orgId, departmentId, departmentIds, assigneeId,
ticketId, contactId) - this is a UI change, not an API change.

The organizations route now resolves the credential server-side. It previously
had the browser fetch an access token and POST it back, which an earlier audit
flagged as the one place a Zoho token left the server; the new selector-credential
resolver keeps it server-side for both the OAuth and service-account credential
types and re-anchors every outbound host to the Zoho apex allowlist.

No agents selector: the endpoint is documented but its OAuth scope is not, and
the nearest evidence points at Desk.agents.READ, which we do not request. Adding
it would force every existing Zoho Desk user to reconnect for a convenience
field, so assigneeId stays a manual input until the scope can be confirmed
against a live org.

Adds the skills array BlockMeta was missing - 227 of 300 blocks declare one and
this did not. Seven skills, each grounded in a use case Zoho or the ecosystem
actually advertises (auto-triage, SLA escalation, digest, AI draft reply,
customer context, engineering handoff, knowledge-gap report) and each exercising
only tools in tools.access. CSAT surveys, ticket creation, dedup and keyword
search were deliberately left out: the integration has no tool for them, and a
skill implying an unsupported action is worse than a shorter list.

* feat(zoho-desk): agents selector and free-text trigger organization

Three improvements that were previously deferred only to avoid forcing existing
users to reconnect or orphaning saved workflows. This integration is unmerged and
has no users, so the constraint does not apply and the better option wins.

assigneeId was the last field still asking for an opaque numeric id. It is now a
canonical selector pair backed by a new zoho_desk.agents selector, which required
adding the Desk.agents.READ scope - the reason it was skipped before. Route
follows the departments one exactly: auth before parseRequest, host anchored to
the Zoho apex allowlist, secureFetchWithValidation with stripAuthOnRedirect, and
a page drain capped at 20 pages with 204 treated as end-of-list.

Scope caveat: Zoho publishes no explicit scope line for the list-all
GET /api/v1/agents. Every other endpoint in the Agents module documents
Desk.agents.READ (get by id, get by email, roles/{id}/agents), and it is the only
agents-module scope Zoho defines, so that is the basis. Inference across a module
rather than a direct quote - worth one live call before merge, same as the
existing attachment-scope note.

The trigger regained free-text organization entry, lost when the org field became
a selector. The earlier concern - that a manual value would land under its raw
subBlock id and never reach the provider - turned out not to hold: buildProviderConfig
already collapses canonical pairs and writes the active member under the canonical
key. The real gap is narrower and does exist: when canonicalModes pins the group
to basic while only the manual field has a value, the collapse deletes the
canonical key even though the required-field check passes, so the deploy succeeds
and then fails at subscription time. resolveConfigOrgId closes that, with a test.

The block/trigger `orgId` id overlap stays shared, now with a comment. Two earlier
audits disagreed; renaming turns out to be the wrong call. buildCanonicalIndex has
an explicit guard for trigger-mode reuse and blocks.test.ts codifies it as a valid
pattern, orgId means the same portal in both modes (unlike departmentIds, which is
correctly distinct), and a separate triggerManualOrgId would put two advanced
members in one canonical group - getCanonicalValues takes the first non-empty, so
a stale tool-mode value could silently supply the trigger's organization.

* fix(zoho-desk): make the attachment cap reachable, unbreak selector paging

Final audit round.

The 50 MB attachment ceiling could never be hit. This route returns the file as
base64 inside its JSON body, and the executor reads internal tool responses
through readToolResponseBody, capped at 10 MB. Base64 inflates 4/3, so ~7.5 MB
of raw bytes is the real ceiling - and the old limit meant a larger attachment
was downloaded, encoded and serialized in full (peaking near 250 MB of live
allocation, with nothing bounding concurrent downloads) purely to be rejected
afterwards. The cap is now the reachable size, so the limit enforces itself while
the bytes are still streaming, and an overflow returns 413 with the actual
ceiling instead of a generic 500. Raising it properly means uploading in the
route and returning a file reference, as the WhatsApp media route does - not a
bigger constant.

Selector paging assumed a 0-based `from`. Zoho's docs contradict themselves:
the pagination section says "range 0-4999, default 0" while the listing examples
read as 1-based ("from=5 and limit=50 retrieves records 5 to 54"). Under the
1-based reading, stepping by exactly the page size re-fetches the boundary record
and the dropdown shows a duplicate per page. Rather than pick a base that cannot
be confirmed without a live tenant, the department and agent drains dedupe by id,
which is correct under either reading.

The organization list was unpaginated, and Zoho's listing APIs default to ten per
page. An account with more accessible portals silently got a truncated dropdown,
and since every other selector and every tool call is gated on orgId, a missing
portal was unreachable except through the advanced manual field. Both the
selector route and list_organizations now request the documented maximum.

Docs: regenerated so the trigger table includes manualOrgId, and two
service-account claims are hedged to match what the code already says it cannot
verify - that zsoid equals the Desk orgId header value, and that every tool works
under the requested scopes (Zoho publishes no scope for the attachment content
sub-path).

Also: status and priority move out of advanced mode - they are the fields most
often changed on a ticket update; the custom-fields wand prompt now ends with the
required "Return ONLY" clause; and the shared-orgId rationale comment cites the
mechanism that actually applies (buildCanonicalIndex dedupe plus the first-non-
empty rule in getCanonicalValues) rather than a blocks.test.ts branch that never
evaluates this pair.

* fix(zoho-desk): five-audit round - serializer trigger-advanced leak, scopes, paging

Five independent audits (OAuth/scopes, tools-vs-docs, block/selectors,
blast-radius, /validate-trigger). Findings, most severe first.

A trigger-mode field was a live tool-mode required param. `shouldSerializeSubBlock`
excluded `mode: 'trigger'` but not `'trigger-advanced'`, so the trigger's required
`manualOrgId` validated on every tool operation. Reproduced against the real
serializer: with the Organization field pinned to advanced, running
List Organizations failed with "Missing required fields: Organization ID" - a
field that operation does not even render, and which the user could not clear
without switching operations. Fixed in the serializer rather than locally,
because the Google Sheets/Drive/Calendar pollers have the identical shape.

`limit=200` on /organizations was an undocumented parameter I added by
extrapolating from /departments and /agents. Zoho documents NO parameters for
that endpoint and its sample is a bare GET; the other siblings cap at 100 and
Zoho answers out-of-range with 422. Since orgId gates every tool and both other
selectors, a 422 there would have made the whole integration unreachable. Reverted
to Zoho's documented shape.

`descriptionText` was HTML-stripping plain text. The previous round made the strip
unconditional after finding Zoho sends no `descriptionContentType`, but Zoho's REST
samples show plain descriptions while only the webhook payload is HTML - and the
webhook path runs this over contact/account/department bodies too. html-to-text is
not identity on plain text: it decodes entities and deletes tag-shaped content
("a < b > c", XML snippets). Now sniffs for markup first.

`omitUnset` made every documented field-clear impossible. Zoho's own PATCH sample
uses `"classification": ""` and `"productId": ""` to clear. Dropping `''` meant no
scalar field could be cleared. Now drops only undefined/null - the serializer-null
case it was written for - and forwards `''`.

status/priority leaked between operations. One shared subBlock served both the
list_tickets filter and the update_ticket value, and subBlock values survive an
operation switch, so a filter of "Open,On Hold" could be PATCHed onto a ticket and
an update value could silently filter a later list. Split per operation.

Auth: `invalid_code` added to TERMINAL_ERRORS - it is Zoho's code for a revoked
refresh token, so without it the previous round's refresh fix never actually
dead-flagged the credential it was written for. The shared refresh body-error
branch now also requires `!data.access_token`, so no provider can have a
successful refresh misclassified. The token route now uses the validating
`extractZohoDeskBaseFromScope` instead of a private regex with no https/allowlist
check - that value is injected into every tool call. Scope list falls back to the
requested scopes when Zoho omits `scope`, which would otherwise flag every
credential as needing reconnect. The Self Client mint no longer sends
`aaaserver.profile.READ`, a scope that grant never uses.

Trigger: `includePrevState` now set for every *_Update event, not just tickets -
it defaults to false, so prevState was permanently null for contact/agent/task/
article updates while the trigger advertised it. `departmentIds` is only sent for
events Zoho documents as accepting it, and the field is conditioned accordingly.
Empty filters serialize as `null`, matching Zoho's examples, rather than `{}`.
JWKS fetch bounded to 1.5s - jose's default is 5000ms, exactly Zoho's whole
delivery deadline, and Zoho publishes no retry. The create-time validation POST
fallback is now matched by the pending-verification probe. Ticket_Delete added.

All 17 webhook event ids, the POST /api/v1/webhooks body contract, and the JWT
claim/JWKS specifics are now confirmed verbatim against Zoho's webhook
documentation - previously 12 of 17 events and the entire subscription contract
were unverified.

* revert(zoho-desk): back out both shared lib/oauth changes

Reverting two changes to shared OAuth code because their premise is inferred
rather than proven, and neither meets the bar for touching a path every provider
runs.

`refreshOAuthToken` body-error branch. The premise was that Zoho reports refresh
failures with HTTP 200 and an `error` body. That is documented and empirically
confirmed for the authorization-code EXCHANGE (see the comment on getToken in
auth.ts), but I never confirmed it for the REFRESH grant specifically - and if
Zoho returns a proper 4xx there, the existing `!response.ok` path already
classifies it via extractErrorCode, making the branch dead code that every one
of the ~34 providers still executes on each refresh. A shared branch whose only
justification is an unverified inference about one provider is not worth its
blast radius.

`invalid_code` in TERMINAL_ERRORS. Same problem, worse downside: the code is
sourced from a Zoho community post rather than official docs, TERMINAL_ERRORS is
consulted for every provider, and a false positive marks a credential dead for an
hour. Not adding it simply preserves today's behavior (retry rather than
dead-flag), so reverting costs nothing that was previously working.

Both are cheap to reinstate, correctly scoped, once a live Zoho account shows
what a revoked refresh token actually returns.

Kept: the token-redaction on the "no access token" warn, which is an unambiguous
improvement independent of Zoho.

Also kept, deliberately, is the serializer `trigger-advanced` exclusion - that one
rests on a reproduced bug rather than an inference, and it aligns the serializer
with the convention the rest of the codebase already follows (blocks.test.ts
treats `trigger` and `trigger-advanced` identically in six places, as does the
copilot block-metadata tool, and blocks/types.ts documents trigger-advanced as
"the advanced side of a trigger field").

* fix(zoho-desk): carry the stored data center through a credential reconnect

A reconnect rebuilds the service-account secret blob from the submitted fields
only, and the connect modal never prefills - correctly, since for every other
field in this family the stored value is a secret the admin must retype. The
data center is the first non-secret member of that set, so it was being silently
dropped: rotating a client secret on an EU/IN/AU credential moved it back to the
US accounts server, where the next mint fails with an opaque invalid_client.

performUpdateCredential now reads the stored dataCenter out of the existing blob
when the caller does not supply one. The read is failure-tolerant - an
undecryptable or unparseable blob yields undefined rather than throwing, so it
can never block a reconnect, and the provider default applies as before.

Raised independently by three reviewers; I twice argued it was acceptable because
the mint fails loudly rather than corrupting silently. That was true and beside
the point - the operator still had to guess why.

* fix(zoho-desk): delta-audit findings - prevState scope, status leak, HTML sniffer

An audit of the commits the earlier five audits never saw. All four findings are
in code written as fixes for those audits, which is where this branch has
repeatedly introduced new problems.

`includePrevState` was sent for Ticket_Comment_Update. The previous commit gated
it on an `_Update` suffix and claimed Zoho supports it on every update event.
Zoho's webhook doc lists the attribute on Ticket/Contact/Agent/Task/Article update
events but NOT on Ticket_Comment_Update, which documents only `departmentIds`.
That made it an undocumented filter key on a live subscription create - the same
class of risk the same commit reverted `limit=200` for, so it failed that commit's
own stated bar. Now an explicit set rather than a suffix rule.

The status/priority split did not stop the leak it was written for. The mapping
used `operation === 'list_tickets' ? filterValue : updateValue`, whose bare else
covers all eight other operations - so a stale Update Ticket status was forwarded
into get_ticket, list_comments and the rest. Harmless on the wire (those tools
ignore it) but exactly the stale-value pattern the neighbouring gates exist to
prevent. Both fields are now scoped to the two operations that declare them.

The HTML sniffer destroyed plain text. `/<[a-z!\/][^>]*>/` fires on any `<`
followed by a letter with a later `>`, so realistic ticket bodies lost content:
"if x<y then z>0" became "if x0", and "replace <username> with the real name"
lost the placeholder. It now requires a real element - a paired tag, a
self-closing tag, a comment/doctype - or an entity, and the entity arm covers hex
references it previously missed. Regression tests verified by reverting to the
loose pattern and watching them go red.

The reconnect data-center carry-forward is scoped to client-credential providers.
As written it added a DB read plus a decrypt to every service-account reconnect
for every provider - Slack, Atlassian, all token-paste providers - to carry a
field only Zoho has.

Also: the JWKS cache-bound TSDoc had been orphaned onto the wrong constant by an
earlier insertion, and `cooldownDuration` was dropped since it restated jose's
default while only `timeoutDuration` needed justifying.

* test(zoho-desk): cover the webhook subscription filter rules

The subscription filter logic had no test coverage at all, and it is where the
last two rounds both found bugs - includePrevState on an event Zoho does not
document it for, and departmentIds sent to events that accept no filters.

Adds six cases against the real createSubscription: includePrevState is set for
each of the five documented update events and NOT for Ticket_Comment_Update,
departmentIds is kept for a filterable event and dropped for one that is not, and
an event with no filters serializes as null rather than an empty object.

Verified the guard bites: reverting PREV_STATE_EVENTS to the `endsWith('_Update')`
rule turns the Ticket_Comment_Update case red.

The Ticket_Comment_Update assertion checks the with-departments case as well as
the bare one - asserting only `not.toHaveProperty` on the bare filter would pass
vacuously, since that filter is legitimately null.

---------

Co-authored-by: Waleed Latif <walif6@gmail.com>
2026-08-01 21:55:00 -07:00
Vikhyath MondretiandClaude f78367c4e8 feat(logfire): add Pydantic Logfire block, tools, and docs (#6075)
Four read-token tools over Logfire's query API: structured span/log search,
raw SQL against records/metrics, full-trace fetch by ID, and read-token
introspection. Plus the block, icon, registry wiring, and generated docs.

Requests go to /v2/query with the region resolved from the token's
pylf_v{n}_{region}_ prefix, overridable by an explicit region or a self-hosted
host (public HTTPS only, per the tool executor's URL policy). Structured
filters are emitted as escaped SQL literals, using DataFusion contains() so
%/_ in user input stay literal.

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-29 18:07:39 -07:00
Waleed 6d48444525 fix(docs): render native block icons instead of the two-letter fallback (#5981)
* fix(docs): render native block icons instead of the two-letter fallback

The Table and Logs pages (and every other native resource block) showed a
two-letter text fallback because the generated icon map never contained them.
Four separate causes in scripts/generate-docs.ts:

- The icon-map allowlist had drifted behind NATIVE_RESOURCE_BLOCK_TYPES, the
  set the docs writer uses. Key the exception off that set so the map cannot
  fall behind the pages that consume it.
- extractIconNameFromContent only matched identifiers ending in `Icon`, so
  Logs (`icon: Library`) resolved to nothing. Match any identifier, excluding
  bare JS literals.
- The map imported everything from `@/components/icons`, so an icon sourced
  from `@sim/emcn/icons` could not resolve. Imports are now grouped by the
  module each icon is actually imported from.
- Trigger-only pages (slack_app, twilio) and hand-written pages (a2a) had no
  entry at all. Seed provider icons from the trigger definitions.

Also fixes three regen bugs found while verifying the output:

- A comment reading "this becomes `hideFromToolbar: true`" in slack.ts was
  matched as the property itself, so a clean regen dropped Slack from the
  integrations catalog and reduced slack.mdx to a 29-line stub. Property
  probes now run against comment-stripped source.
- 16 hand-written *-service-account guides were unregistered, so the stale-doc
  cleanup deleted them on every regen. Registered them, and cleanup now refuses
  to delete any page holding MANUAL-CONTENT (this also restores the intros on
  file.mdx and twilio.mdx).
- Trigger outputs referenced as a constant (`outputs: SLACK_TRIGGER_OUTPUTS`)
  resolved to nothing, dropping whole Output tables. Constants and sibling
  modules now resolve, which also restores 319 lines on clickup.mdx.

Removes the language selector from the docs navbar.

Regenerated docs are included; remaining content deltas are tool-definition
drift since the last regen.

* improvement(docs): drop the preview-gated slack_app page, document managed_agent

- slack_oauth is reachable only through the preview-gated slack_v2 block, so
  documenting it published an unreleased surface under its own slack_app page.
  Triggers whose every hosting block sets `preview: true` are now excluded from
  the docs and the icon map. Triggers no block claims are untouched, so
  standalone webhook providers keep their pages.
- Adds the MANUAL-CONTENT intro to managed_agent.mdx, matching the other
  integration pages. Verified it survives a regen.

* fix(docs): stop truncating quoted descriptions, tighten the cleanup guard

Review findings from round 1.

- parseSubBlockObject read string properties with a single `['"]…[^'"]+…['"]`
  character class, which ends the match at the first quote of either kind. Any
  description holding an apostrophe inside a double-quoted string was cut
  mid-word ("Your app", "Found in your Zoom app"). Matches the opening quote to
  its own closing quote now, reusing the alternation the tool-description
  extractor already used. Restores full text across calendly, gmail,
  google_sheets, hubspot, intercom, whatsapp, and zoom.
- The stale-doc cleanup guard tested for a bare `MANUAL-CONTENT-START`
  substring, so a stray or unterminated marker would pin a stale page that has
  nothing recoverable. It now gates on what extractManualContent actually
  returns.
2026-07-27 14:07:25 -07:00
Bill LeoutsakosandBill Leoutsakos bd61603701 feat(tiktok): unhide integration (#5978)
* feat: unhide TikTok integration

* test: remove TikTok visibility assertion

---------

Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local>
2026-07-27 11:47:15 -07:00
Waleed ff1d061897 feat(tools): Rocketlane integration — 64 tools across projects, tasks, phases, fields, time tracking, spaces, and invoices (#5709)
* feat(rocketlane): Rocketlane integration — 64 tools across projects, tasks, phases, fields, time tracking, spaces, and invoices

* fix(rocketlane): allow clearing optional fields on update, require a user reference for time-off and placeholder assignment

* chore(rocketlane): trigger fresh review round

* fix(rocketlane): require an owner reference when creating a project, matching the API contract
2026-07-16 10:10:19 -07:00
Waleed 3a632936ab feat(clickup): ClickUp integration — 23 tools, OAuth + API-token auth, attachment upload (#5702)
* feat(clickup): add ClickUp integration with OAuth + API-token auth, 23 tools, block, and attachment upload

- 23 tools covering tasks (create/get/update/delete/list/search), comments
  (create/get/update/delete), attachment upload, tags, members, custom
  fields, and the workspace/space/folder/list hierarchy
- OAuth provider wiring (authorization-code flow, non-expiring tokens) plus
  clickup-service-account token-paste credential (personal pk_ API tokens),
  with a shared clickupAuthorizationHeader helper (pk_ tokens sent bare,
  OAuth tokens as Bearer)
- File upload follows the internal-route pattern: contract-validated
  /api/tools/clickup/upload-attachment builds the multipart form and
  returns UserFiles
- ClickUp block with per-operation subBlocks, canonical file param,
  BlockMeta templates/skills, and gradient brand icon
- Generated integration docs page + hand-written service-account guide

* fix(clickup): apply validation-audit fixes across tools, block, and upload route

- Map documented task fields that were dropped: markdown_description,
  subtasks, watchers, custom_fields, time_spent, folder, space — making
  the include_subtasks / include_markdown_description options observable
- Expand verified filters: assignees/tags/due-date ranges on get_tasks and
  search_tasks, include_closed on search_tasks; add due_date_time /
  start_date_time flags and update-task assignee add/remove
- Guard update_comment against an empty body and require comment text in
  the block; prefer markdown_content over content on create_list and make
  markdown reachable for lists in the UI
- Drop the unverified 'required' field from custom-field outputs; read
  both err and error keys from ClickUp error bodies; correct notify_all
  wording
- Upload route: 100MB size cap, shared attachment mapper with full
  documented response fields (version, thumbnails), base-URL constant

* fix(docs): restore clickup-service-account guide and shield it from doc generation

The generator prunes integration pages it does not derive from blocks;
add the hand-written ClickUp API-token guide to
HANDWRITTEN_INTEGRATION_DOCS so regeneration cannot delete it.

* fix(clickup): address review findings — dedupe catalog entries, config-time list parent validation, upload memory cap, unique icon gradient ids

- Remove duplicated clickup entries in docs meta.json and integrations.json
  introduced by a double docs regeneration
- Add a Location dropdown for Get Lists / Create List so the folder ID or
  space ID is conditionally required at configuration time instead of
  failing at run time
- Pass the 100MB cap into downloadServableFileFromStorage so oversized
  files abort during download instead of after full buffering
- Use useId()-derived SVG gradient ids for ClickUpIcon in both icon files

* chore(clickup): format integrations.json entry per biome

* improvement(clickup): final validation-pass refinements across tools and block

- create_task: add doc-backed sprint points param (parity with update)
- get_tasks/search_tasks: expose include_markdown_description
- update_task legacy numeric priority in list responses mapped instead of
  dropped; create_comment omits absent response fields instead of
  emitting sentinel ''/0 values
- order_by only sent when explicitly chosen (Default sentinel); comment
  text no longer UI-required for update_comment (resolve-only and
  assignee-only updates are valid per the tool contract, which still
  rejects an empty body)
- add_tag_to_task sends no request body per docs; upload tool tolerates
  non-JSON error responses

* fix(clickup): tolerate nested user wrapper in member mapping

The task/list member endpoints document a flat member object; accept the
workspace-members-style nested { user: {...} } wrapper as well so both
shapes map correctly.

* fix(clickup): map size-limit errors from download/compile to a 400 upload-size response

downloadServableFileFromStorage enforces maxBytes on both the raw download
and the resolved (compiled) artifact via PayloadSizeLimitError; catch it in
the route so oversized content returns the intended 400 instead of
bubbling to the generic 500 handler.

* feat(clickup): add custom field values, checklists, and time tracking (15 tools, 38 total)

- Set/remove custom field values on tasks (PUT/DELETE /task/{id}/field/{field_id});
  block value input parses JSON for structured field types, plain values pass through
- Checklist CRUD: create/rename/reorder/delete checklists and create/update/
  delete checklist items (assign, resolve, nest), mapped from the documented
  {checklist} response shape
- Time tracking: list entries in a date range (assignee/location filters,
  task-tag and location-name includes), create/update/delete entries, start/
  stop timers, and read the currently running timer; entries mapped from the
  documented data envelope with negative-duration running semantics
- Block gains 15 operations with conditionally-required fields, timestamp
  wand configs, tri-state billable/resolved dropdowns, and a single-location
  filter selector matching the API's one-location-filter rule

* fix(clickup): new-tools audit fixes — POST for set custom field value, tolerant time-entry envelopes, richer mappings

- Set Custom Field Value uses POST per the live reference OpenAPI (the
  llms mirror shows PUT; the reference console spec is authoritative)
- delete_time_entry maps the documented array envelope; create_time_entry
  tolerates both data-wrapped and flat echo bodies
- Time entries surface task_tags and task_location so the include switches
  are observable; checklists carry date_created
- Custom field value input parses any JSON literal (numbers, booleans,
  arrays, objects) and passes plain text through
- Update Time Entry supports duration edits; single-assignee time ops get
  their own field so a comma-separated list can't silently NaN out

* fix(clickup): send explicit date-time flags whenever a date is set

The due/start date-time switches previously only transmitted true; a
timed date could never be flipped back to date-only. The flag is now sent
as an explicit boolean whenever the corresponding date is provided and
omitted otherwise.

* improvement(clickup): final per-tool audit polish — checklist item children, tolerant comment date

- Checklist items surface the documented children array of nested item IDs
- create_comment tolerates a string-typed date in the response

* fix(clickup): reject empty update_task bodies with a clear local error, matching sibling update tools
2026-07-16 00:38:59 -07:00
Vikhyath Mondreti 54b35a4f0e improvement(deployments): bugfixes for run-block, airtable + external sub management (#5680)
* improvement(webhooks): external subscription management

* ui/ux

* remove test file

* fix tests

* address comments

* address comments

* update to grain v2 api

* improvement(grain): hide auto-registered webhook URL on v2 triggers

* Revert "improvement(grain): hide auto-registered webhook URL on v2 triggers"

This reverts commit c89660cc3e.

* address comments

* address comments

* address rollback

* fix grain v2

* fix more comments
2026-07-15 18:01:53 -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 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
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 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
Waleed 6e87d74a93 feat(jupyter): add Jupyter integration (contents, kernels, sessions) (#5527)
* feat(jupyter): add Jupyter integration (contents, kernels, sessions)

- 16 tools covering Contents, Kernels, Kernelspecs, and Sessions REST APIs
- File upload/download via UserFile, following the Box upload pattern
- Block with operation dropdown, token auth, and 8 catalog templates
- Registered tools + block, generated docs, bumped API validation baseline

* fix(jupyter): address Greptile review — SSRF guard, upload path ambiguity, silent notebook fallback

- Route uploads through validateUrlWithDNS + secureFetchWithPinnedIP (matches Grafana/1Password pattern) instead of a raw fetch to the user-supplied server URL
- Replace the upload path/trailing-slash heuristic with an unambiguous directory + filename split
- create_file no longer silently writes an empty notebook when notebook content is malformed JSON — it now errors clearly

* fix(jupyter): request content=1 when listing directory contents

Without it, Jupyter Server returns directory metadata with content:
null, so jupyter_list_contents always reported an empty items array.

* fix(jupyter): reject path-traversal segments in Jupyter content paths

encodeJupyterPath now rejects '.'/'..' segments across the whole path
(shared by all 16 tools, not just upload); the upload route returns a
clean 400 when it's hit.

* fix(jupyter): close remaining path-traversal and redirect-credential gaps

- extract the traversal check out of encodeJupyterPath into a shared
  assertion, and apply it to body-only path fields (rename newPath,
  copy copyFromPath, session path) that never flowed through URL
  encoding and so skipped the check
- pass stripAuthOnRedirect to the upload route's secureFetchWithPinnedIP
  call so a malicious Jupyter server can't redirect the PUT to another
  origin and receive the caller's token

* fix(jupyter): also reject percent-encoded traversal segments

A segment like %2e%2e wouldn't match the literal '..' check. Now
decodes each segment before comparing, in addition to the literal
check, so an already-encoded traversal attempt is caught too.

* fix(jupyter): route all 15 remaining tools through an internal proxy for HTTP/private-host support and no redirects

The generic external tool executor blocks plain-HTTP and non-localhost
private-IP hosts by default, so every non-upload Jupyter operation
could fail against typical self-hosted setups (LAN IP, docker
hostname, or even literal localhost on a hosted deployment) even
though the upload route worked via its own internal route.

Added /api/tools/jupyter/proxy (DNS-pinned, allowHttp, maxRedirects: 0)
that mirrors the upstream Jupyter response verbatim, matching the
established pattern for self-hosted-arbitrary-host integrations
(Grafana, 1Password) instead of the generic executor path. Each tool's
request block now posts to the proxy instead of building a direct
external URL; transformResponse and outputs are unchanged since the
proxy response mirrors upstream status/body exactly.

Also switches the upload route from stripAuthOnRedirect to
maxRedirects: 0 — stronger, since it stops the uploaded file body
(not just the token) from ever reaching a redirect target.

* fix(jupyter): validate proxy path at the trust boundary, reject path separators in upload filename

- The proxy route now independently validates the incoming path field
  for traversal segments instead of only relying on tool-side
  validation before the request reaches it — the route is a shared
  internal boundary, not something only our own tool code can call
- The upload route's fileName can come from an advanced override or
  the legacy fileContent path and could itself contain '/' or '\',
  silently nesting the upload deeper than the directory param
  specified. Now rejected outright before joining.

* fix(jupyter): decode the whole path before splitting, not per-already-split segment

A segment like foo%2f..%2fsecret has no literal slash, so splitting on
literal '/' first and decoding each piece in isolation treats it as
one opaque segment and never notices the '..' hiding behind the
encoded slash. Decode the full path once, then split and check every
segment the target server's own single URL-decode pass would see.
2026-07-08 19:35:57 -07:00
Waleed 10b6bb33e9 feat(google-appsheet): add Google AppSheet integration (#5376)
* feat(google-appsheet): add Google AppSheet integration

- 4 tools (find/add/edit/delete rows) against the AppSheet Action API
- API key auth via Application Access Key (no OAuth/scopes needed)
- Block with operation dropdown, region selector, and Selector expression support
- Generated docs

* improvement(google-appsheet): harden response parsing, add wand config and skills

- Guard against empty/non-JSON AppSheet response bodies (Delete may return no body)
- Add wandConfig to the Selector field for AI-assisted expression generation
- Add 3 skills grounded in attested AppSheet/Zapier automation patterns
- Tighten json output descriptions to describe inner shape

* fix(google-appsheet): validate region against allow-list, encode appId, validate rows shape

- Reject unrecognized region values instead of interpolating them into the
  request host (a caller could otherwise redirect the Application Access
  Key to an arbitrary domain)
- URL-encode appId, not just tableName, in the Action endpoint path
- Reject non-array Rows input in tools.config.params instead of forwarding
  a single object to the AppSheet Action API
- Drop the mismatched json-object generationType on the rows wand config
  (that enricher appends "must start with { and end with }", which
  conflicts with the JSON-array shape the field expects)
- Add utils.test.ts covering region validation and response-body parsing

* docs(google-appsheet): add manual intro/getting-started section

Match the MANUAL-CONTENT convention used by other integration docs
(Airtable, Ahrefs, Google PageSpeed) — an overview of the service, what
the Sim integration lets agents do, and how to get an Application
Access Key.

* docs: sync generated integration docs with current source

Regenerate docs for integrations whose tools/blocks changed upstream
without a matching docs regen (ahrefs, algolia, amplitude, brex, clerk,
gong, hex, langsmith, loops, onepassword, sendgrid, sharepoint,
similarweb, supabase, tailscale, trello, vercel, wordpress), plus the
integrations.json catalog.
2026-07-02 11:58:30 -07:00
Waleed bcf6a804f9 improvement(emcn): extract design system into shared @sim/emcn package (#5257)
Moves apps/sim/components/emcn into a shared @sim/emcn package consumed directly by apps/sim and apps/docs. cn/keyboard/use-copy-to-clipboard move into the package; all imports become direct @sim/emcn (icons via @sim/emcn/icons, CSS via file path). ChipModal email validation is now prop-driven (quickValidateEmail stays in apps/sim, injected via validate). Docs drops its local chip/chip-dropdown/dropdown-menu mirrors and consumes @sim/emcn.
2026-06-28 22:50:48 -07:00
Waleed 66315f19e7 improvement(docs): flatten the academy learn/chapters panels (#5253)
* improvement(docs): flatten the academy learn/chapters panels

The "What you will learn" and "Chapters" panels were filled, bordered
cards — the only boxed elements on the page. The docs design system is
explicitly flat: global.css strips fumadocs cards/callouts/card-grids to
transparent/divider-based, and the right-rail "On this page" TOC is small,
muted, and borderless.

- WhatYouWillLearn (inline): flat divider list like the FAQ, with a small
  panel label at the app's text scale instead of a page-h2-scale title
- VideoChapters (right rail): borderless, matching the TOC — small muted
  label + flat hover rows, no card chrome

* improvement(docs): drop the repeated per-row play icon from the chapters list

The CirclePlay glyph repeated on every chapter row read as noise — a column
of identical icons down the rail. The "On this page" TOC this list mirrors
has no per-row icons; the timestamps already signal video chapters and the
hover highlight signals they're seekable. Rows are now text + time only.

* improvement(docs): drop the under-label rule on the learn callout

A full-width rule under the small "What you will learn" label read as an
awkward heading underline and blurred into the inter-item dividers. The label
is now a quiet muted marker (matching the Chapters label and the TOC heading),
with dividers only between items — so it never competes with the bold item
titles or looks like an underlined heading.
2026-06-28 14:35:39 -07:00
Waleed 3143a15dde feat(uptimerobot): add UptimeRobot v3 integration (#5229)
* feat(uptimerobot): add UptimeRobot v3 integration

- 24 tools across monitors, incidents, maintenance windows, alert contacts,
  public status pages, and account (UptimeRobot v3 REST API, Bearer auth)
- Block with operation-scoped subBlocks, status-page logo/icon file uploads
  via internal multipart routes, and BlockMeta templates + skills
- Registered tools/block, added icon, generated docs
- Updated add-integration/add-block/validate-integration docs links to /integrations

* fix(uptimerobot): address review — heartbeat URL, file/JSON edge cases

- Block: URL is not required for HEARTBEAT monitors (no URL)
- buildMonitorBody: throw on malformed assignedAlertContacts/customHttpHeaders
  JSON instead of silently dropping the field
- PSP route: error (400) when a supplied logo/icon cannot be resolved to a
  stored file instead of silently omitting the image
- PSP route: guard success-path JSON parsing; return a controlled 502 on a
  non-JSON provider response instead of an uncaught 500

* fix(uptimerobot): spec-conformance audit fixes

- pause/start monitor: send Content-Type: application/json (v3 spec requires it
  on these POSTs even with an empty body)
- update maintenance window: drop autoAddMonitors (not in UpdateMaintenanceWindowDto);
  gate the block field to create only

* fix(uptimerobot): rename monitor timeout param to avoid reserved name

The tool runner treats a top-level `timeout` param as the outbound HTTP-client
timeout (ms), so a monitor check-timeout of e.g. 30s would abort the API call in
30ms. Rename the input to `checkTimeout` (block subBlock, tool params, inputs,
numeric coercion) and map it to the API body's `timeout` key in buildMonitorBody.

* fix(uptimerobot): reject empty/non-object PSP responses

A successful PSP create/update must return the PspDto object; an empty or
non-object body now returns a controlled 502 instead of mapping a phantom
status page (id: 0, empty name, null images) back to the workflow.

* fix(uptimerobot): validate core PSP fields before mapping

Reject successful PSP responses that lack a positive numeric id and non-empty
friendlyName (a {} or metadata envelope) with a controlled 502, instead of
mapping a phantom status page.
2026-06-26 18:38:50 -07:00
Waleed 35acc42d2b feat(downdetector): add Downdetector outage-monitoring integration (#5228) 2026-06-26 18:25:31 -07:00
Waleed 954de0559b improvement(docs): align components with the platform design system (#5227)
* improvement(docs): align components with the platform design system

Bring the docs app's chrome in line with the main Sim design system,
validated against the canonical emcn source in apps/sim.

- ask-ai: fix undefined tokens (--text-base x6, --text-link) that broke
  the send-button fill and link/text colors; send button now matches the
  canonical primary fill (text-primary/text-inverse, dark:bg-white); use
  --shadow-medium and chip gap rhythm
- not-found: replace the hand-rolled brand pill with <ChipLink variant='brand'>
  and swap fumadocs tokens for platform tokens
- search-trigger: compose the exported chip chrome constants instead of
  re-spelling them (single source of truth)
- what-you-will-learn, video-chapters: fumadocs fd-* tokens -> platform tokens
- workflow-preview: add --wp-highlight token; route the #33b4ff highlight,
  #ef4444 error dots, and toggle/slider green through tokens
- video-placeholder: tokenize the status pill (bespoke illustration art
  intentionally left as-is)

dropdown-menu, faq, theme-toggle, and page-type-badge were deliberately
left at their canonical values (14px row icons, rounded-md badge) after
validation showed those match the platform, not the chip-pill, standard.

* improvement(docs): neutral primary chip for nav CTA + fix cluster spacing

Align the docs navbar with the main app, which reserves green for
accents/status and uses a neutral high-contrast CTA in nav.

- add a canonical `primary` chip variant (inverse fill: dark in light
  mode, white in dark mode), mirroring the emcn chip's primary action
- "Get started" and the 404 "Go home" now use variant='primary' instead
  of the green brand surface
- retire the now-unused `brand` chip variant (no parallel path left behind)
- fix navbar right-cluster spacing: gap-2 to match the landing navbar and
  drop the asymmetric ml-1 on the CTA
2026-06-26 17:24:09 -07:00
Will ChenandClaude Opus 4.8 d20deedf99 improvement(docs): add Academy learning surface (#5213)
Adds the Academy section to the docs: video-first lessons (self-hosted MP4 on
Vercel Blob), organized into Workflows, Agents, Tables, Files, and Knowledge
Bases, each linking back to the reference docs. Lessons use a course layout
(hero video with chapter seek, "what you'll learn", block diagrams).

Docs only — no runtime or auth changes. The content may move to a separate CMS
or its own site (academy.sim.ai) later; the docs are a starting point.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 15:44:14 -07:00
Waleed 371cc94851 feat(thrive): add Thrive Learning integration (47 tools + block) (#5214)
* feat(thrive): add Thrive Learning integration (47 tools + block)

Add a full Thrive Learning (LMS) integration covering the public REST API:
users lifecycle, audiences with members/managers, assignments and enrolments,
completions, content and activity records, CPD, tags, and skills. Uses HTTP
Basic auth (Tenant ID + API key) with a region selector for the v1/v2 hosts.

* fix(thrive): surface malformed JSON errors, drop redundant limit param

- parseThriveArray/parseThriveJsonObject now throw a descriptive error on
  malformed JSON instead of silently sending an empty/omitted value
- additionalFields parse errors are surfaced to the caller
- remove the redundant 'limit' query param (perPage already covers paging and
  the API prioritises perPage over limit) from the five list tools and block

* fix(thrive): use a single status dropdown instead of reused canonicalParamId

The block test forbids reusing a canonicalParamId across different operation
conditions. Replace the two canonical status subblocks (search_users vs
list_enrolments) with one 'status' dropdown whose options are labelled by
context, fixing the canonical-pair validation failures.

* fix(thrive): split status into context-specific user/enrolment dropdowns

Addresses review feedback that one shared status dropdown mixed user
lifecycle values (active/inactive/expired/new) with enrolment values
(archived/complete/open/...). Use separate userStatus and enrolmentStatus
dropdowns (no canonicalParamId) remapped to the tool's 'status' param so each
operation only offers valid options.
2026-06-25 15:26:13 -07:00
Siddharth GanesanandVikhyath Mondreti e5f3965ed1 feat(mship): add parallel subagents, improve streaming performance (#5122)
* feat(subagents): add support for parallel subagents

* fix(subagents): address parallel-subagent bugs

* progress on streaming refactor

* improvement(subagents): update comment to reflect new go feature flag

* debug mode progress

* remove debug logs

* fix(validation): add escape annotation

* improvement(code): remove dead fallbacks

* fix subagent lane fallback issue

* fix(mothership): increase default redis event limit to 100k from 5k

* fix(mothership): streaming invariant projection enforcement

---------

Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
2026-06-18 11:27:49 -07:00
Waleed 8b93e43037 improvement(integrations): validate BigQuery/Forms/PageSpeed + regenerate integration docs (#5109)
* improvement(integrations): validate BigQuery/Forms/PageSpeed + regenerate integration docs

- BigQuery: mark null-defaulted outputs optional (get_table type/numRows/numBytes/creationTime/lastModifiedTime/location, list_datasets location, list_tables type, query totalBytesProcessed)
- Google Forms: add response pagination (pageToken + filter params, nextPageToken output), fix pageSize visibility, advanced-mode pagination subBlocks + filter wandConfig
- PageSpeed: add a 7th BlockMeta template (competitor benchmark)
- Regenerate integration docs; add manual intro sections to new datagma/dropcontact/enrow/icypeas/leadmagic pages

* fix(docs-gen): preserve apostrophes in tool descriptions when generating docs

The doc generator extracted tool descriptions with a character class that
excluded both quote types (['"]([^'"]...)['"]), so a double-quoted description
containing an apostrophe (e.g. "Find someone's email") was truncated at the
apostrophe — the generated docs/catalog showed stubs like "Find someone".

Anchor extraction on the actual opening quote (single/double/backtick), matching
the existing extractDescription helper, in both buildToolDescriptionMap and
extractToolInfo. Regenerated docs restore full descriptions across all affected
integrations (Apollo, Ahrefs, LeadMagic, Findymail, OpenAI, Slack, etc.).

* fix(docs-gen): resolve tools defined in a sibling file + scope params per tool

The doc generator located a tool's definition only by filename convention
(decompress.ts / index.ts), so file_decompress — which lives in compress.ts
alongside file_compress — fell back to index.ts and rendered an empty Input
table. It also read the params block from the first tool in a multi-tool file,
so every tool in such a file inherited the first tool's inputs/outputs.

- getToolInfo: when no candidate file declares the exact tool ID, scan the whole
  tool-prefix directory for the file that does.
- extractToolInfo: read the params block scoped to the specific tool, falling
  back to the full file for tools that inherit params via spread.

Regenerated docs eliminate ~50 empty/incorrect input tables across integrations
(clickhouse, rb2b, reddit, file, etc.); param-less OAuth-only tools correctly
keep an empty input table.
2026-06-16 23:05:30 -07:00
Waleed 940506ad09 feat(square): add Square integration with 34 commerce operations (#5053)
* feat(square): add Square integration with 34 commerce operations

Add a Square integration (API-key auth via personal access token) covering
payments, refunds, customers, locations, orders, invoices, catalog, and
inventory. Catalog image upload routes through an internal API endpoint using
the shared UserFile handling pattern. Adds a dedicated square-errors extractor.

* fix(square): correct catalog image part name and address review feedback

- Fix catalog image upload: Square's multipart part for the binary is `file`,
  not `image_file` (per the live API cURL examples); this would have caused
  upload failures
- Catalog image route: check response.ok before parsing, drop the unreachable
  legacy base64 path, derive MIME from the uploaded file
- Block: split the search query field per operation so placeholders match each
  endpoint's schema; parse each JSON field individually so errors name the field
- Round out coverage: complete_payment version_token; customer nickname/birthday;
  batch inventory states/updated_after/limit

* fix(square): correct canonical file param usage and revert query split

- Read the catalog image file from the canonical `params.file` (the basic/advanced
  inputs are collapsed before the params function runs) instead of the raw
  uploadFile/fileRef ids, which no longer exist at that point — fixes the
  Canonical Param Validation test and a latent upload bug
- Revert the per-operation query split: canonicalParamId is only valid for
  basic/advanced pairs under one condition. Use a single query field with a
  schema-neutral placeholder and a wand prompt that covers each search operation

* chore(square): trigger fresh review

* fix(square): single-location invoice search and guard numeric coercion

- SearchInvoices: Square's invoice filter accepts only one location, so take a
  single locationId (string) instead of an array and wrap it as
  query.filter.location_ids: [locationId]
- Block: fail locally with a clear "<field> must be a valid number" error when
  amount/limit/version/orderVersion are non-numeric instead of forwarding NaN

* fix(square): accept real booleans for autocomplete/includeRelatedObjects

Coerce these from both the dropdown's string values and actual booleans
(which can arrive via connected blocks or templated inputs), so true is not
silently flipped to false.

* fix(square): validate parsed JSON field shapes (array vs object)

parseJsonField now enforces the expected shape so a valid-but-wrong-type value
(e.g. a JSON string where an array is expected for locationIds/objectTypes/
paymentIds/catalogObjectIds/states, or a non-object for order/invoice/etc.)
fails locally with a clear message instead of a confusing Square API error.
2026-06-15 09:55:14 -07:00
Waleed 57b58fd117 feat(context-dev): add Context.dev web + brand data integration (#5048)
* feat(context-dev): add Context.dev web + brand data integration

* feat(context-dev): add brand variants, products, fonts, styleguide, images, prefetch

Expands coverage to all relevant Context.dev endpoints (22 tools): brand by
name/email/ticker, simplified brand, transaction identifier, single + catalog
product extraction, fonts, styleguide, image discovery, and prefetch utilities.
Shared brand output schema and transform helper; verified against the live API.

* fix(context-dev): wire includeFrames, split crawl/extract maxPages, derive screenshot MIME

Addresses review feedback:
- includeFrames is now a block subblock + param for scrape_markdown/scrape_html
- crawl and extract use separate Max Pages fields (crawl 1-500, extract 1-50) so a
  crawl value can no longer be forwarded to extract beyond its limit
- screenshot file MIME type and extension are derived from the returned URL instead
  of being hardcoded to PNG
2026-06-14 20:35:20 -07:00
Waleed 3db7161b1c feat(integrations): add Vanta integration with compliance, evidence file, people, vendor, vulnerability, and risk tools (#4993)
* feat(integrations): add Vanta integration with compliance, evidence file, people, vendor, vulnerability, and risk tools

* fix(integrations): use write-only scope for vanta document submit and stream-cap document downloads

* fix(integrations): vanta review feedback - unique svg ids, mime type for base64 uploads, auth check consistency

* fix(integrations): bound vanta base64 fileContent size at the contract level

* improvement(integrations): cache vanta tokens and retry once on revocation to avoid concurrent token races

* improvement(integrations): deduplicate in-flight vanta token exchanges

* fix(integrations): hash vanta client secret in token cache keys

* improvement(integrations): expose vanta upload mime type in block and align fileContent bound with the 100MB cap

* chore: resolve api-validation baseline after rebase onto staging (816 + 3 vanta routes)
2026-06-12 10:56:27 -07:00
Waleed c3b98eb1c4 feat(integrations): add Daytona integration with sandbox lifecycle, code execution, and file tools (#4987)
* feat(integrations): add Daytona integration with sandbox lifecycle, code execution, and file tools

* fix(daytona): address review feedback and harden edge cases

- Pre-check file size via userFile.size before downloading from storage in the upload route
- Tolerate empty response bodies in delete/start/stop sandbox tools
- Preserve explicit timeout 0 for run_code and execute_command
- Default missing exitCode to -1 so unknown state is distinguishable from success
- Reject blank sandbox IDs, clamp list limit to 1-200, trim destination path
- Clarify that toolbox operations require the sandbox ID (not name)
- Bump contract route baseline to 812 for the new daytona upload route

* fix(daytona): cap download size at 100MB and preserve sandbox identity on empty lifecycle responses

* improvement(notion): black icon on white background to match brand

* fix(daytona): reject oversized base64 uploads before decoding

* improvement(landing): move integration last-updated below CTAs and de-emphasize it

* fix(daytona): forward explicit zero cpu/memory/disk values in create sandbox
2026-06-11 19:13:45 -07:00
Waleed 8e960368d4 feat(integrations): add Quartr integration with company, event, document, audio, and live event tools (#4986)
* feat(integrations): add Quartr integration with company, event, document, audio, and live event tools

* fix(quartr): address review findings — null-safe query building, string boolean toggles, accurate descriptions, NestJS validation error extractor
2026-06-11 18:26:40 -07:00
Waleed 9ab64e55e3 feat(integrations): add Convex integration with function execution and data export tools (#4981)
* feat(integrations): add Convex integration with function execution and data export tools

* fix(convex): separate List Documents page cursor from deltas cursor and surface HTTP errors in transforms

* fix(convex): rename List Documents pagination cursor to pageCursor end-to-end for unambiguous chaining

* fix(convex): validate deployment URL with shared SSRF guard

* improvement(convex): polish from final validation pass — reject query strings in deployment URL, validate object args, fix sync skill wording

* docs(convex): note streaming export plan requirement on data-export tools (verified via live E2E)
2026-06-11 17:52:58 -07:00
Waleed 5ab6d0d1ab feat(integrations): add Brex integration (#4983)
* feat(integrations): add Brex integration with expenses, receipts, transactions, team, budgets, and payments tools

* fix(brex): reject whitespace-only expense IDs in receipt upload instead of silently falling back to receipt match

* fix(brex): trim receipt name in contract so whitespace-only overrides are rejected

* fix(brex): align spend limit balance shape, enum descriptions, and pagination metadata with Brex API specs

* improvement(brex): validate pre-signed upload URL with DNS pinning and harden API key input

* fix(brex): correct shared limit placeholder to reflect the 100-item cap on list expenses

* fix(brex): normalize timezone-suffixed timestamps for transactions date filters (Brex rejects offsets)
2026-06-11 17:47:11 -07:00
Waleed 493c64fe08 feat(latex): add LaTeX integration with PDF compilation tool, block, and docs (#4972)
* feat(latex): add LaTeX integration with PDF compilation tool, block, and docs

* fix(latex): surface extracted TeX errors on all failed compile responses

* improvement(latex): add compile timeout, cap upstream error code, reject empty resource payloads

* feat(latex): add package search/lookup and font listing tools

* improvement(latex): address review findings — path traversal guard, http(s)-only resource urls, timeout abort handling, typed pdf output, NaN-safe maxResults, empty-query rejection, third-party disclosure

* fix(latex): fail compile/package lookups that return no payload

* improvement(latex): guard lookup tool responses against upstream errors

* improvement(latex): reject whitespace-only package names at the block boundary

* improvement(latex): make relatedPackages fallback explicit
2026-06-11 13:53:11 -07:00
Waleed 5028ef3b16 feat(temporal): add Temporal integration with workflow, schedule, and task queue tools (#4976)
* feat(temporal): add Temporal integration with workflow, schedule, and task queue tools

* improvement(temporal): send requestId on all dedupe-capable write operations
2026-06-11 13:53:00 -07:00
Waleed 354653b82f feat(integrations): add Trigger.dev integration (#4974)
* feat(integrations): add Trigger.dev integration

* feat(integrations): cover the full Trigger.dev management API surface

Adds batch retrieve/results, run result/events/trace/tags, queue list and
concurrency override/reset, deployments, TRQL query + schema, waitpoint
tokens, env var import, and timezones (44 tools total). Marks
deduplicationKey required on create schedule and switches list-run filters
to the documented comma-separated form, both per the OpenAPI spec.

* fix(executor): strip dots from normalized block names so dotted names resolve

References split on '.', so a block named "Hunter.io 1" could never be
resolved (the first path segment cut the name at the dot). normalizeName now
strips dots on both the tag-generation and lookup sides, which cannot break
any previously working reference. Aligns the inline normalizers in
connection drag-drop, deploy modal, output select, and tag dropdown, and
renames Cal Com to Cal.com now that dotted display names work.

* improvement(integrations): address Trigger.dev review feedback

Reads the delete schedule response instead of hardcoding success, and adds
explicit plaintext-secret warnings to the env var read operations and docs.

* fix(integrations): scope Trigger.dev block params to the selected operation

Advanced sub-block values with non-empty content are serialized regardless
of their operation condition, so values left over from a previously selected
operation could leak into another operation's API call (e.g., a trigger-task
idempotency key filtering the waitpoint list). The params function now routes
every shared or remapped param by the selected operation and clears it
otherwise. Also derives delete env var success from the response like
delete schedule, via a shared helper.

* improvement(executor): dot-free names keep reference ownership on legacy collisions

Name uniqueness is enforced at the normalized level on create and rename, so
new collisions cannot be created. For legacy workflows that already contain
names differing only by dots, the resolver now lets the dot-free name own the
reference key regardless of block order, so previously working references
never change targets.
2026-06-11 13:52:47 -07:00
Waleed e5d655b1be feat(persona): add Persona identity verification integration (#4967)
* feat(persona): add Persona identity verification integration

* feat(persona): complete API coverage — inquiry lifecycle, account update/redact, report listing, template discovery

* improvement(persona): explicit HTTP error handling in all transforms, fix tags wand generation type

* chore(persona): align docs path and docsLink with staging structure

* improvement(persona): fail-closed file access gate on import route, explicit error when one-time link missing

* improvement(persona): validate report template ID before request

* improvement(persona): reject unexpected import response bodies instead of returning an empty importer

* improvement(persona): require session token on resume per API contract
2026-06-11 11:43:31 -07:00
Waleed aa39974d7b improvement(docs): align docs UI with the platform emcn design system (#4962)
* improvement(docs): align docs UI with the platform emcn design system

* improvement(docs): build language dropdown options in render scope
2026-06-10 19:40:31 -07:00
Theodore Li 1ff445ae80 feat(codepipeline): add AWS CodePipeline integration with tools and block (#4945)
* feat(codepipeline): add AWS CodePipeline integration with tools and block

* fix(codepipeline): address review feedback on input coercion and error statuses

* chore(hooks): restore use-inline-rename onSave type accidentally swept into previous commit
2026-06-10 14:51:24 -04:00
Vikhyath MondretiandCursor 6abcf82db2 feat(workflows): sim trigger, logs v2 block, toolbar renaming (#4941)
* feat(workflows): sim trigger, logs v2 block, toolbar renaming

* fix(review): bound rule queries, canonical logs params, watched-workflow SQL scoping

Code-review fixes: read the canonical workflowIds param in logs_v2 (the
serializer deletes the source pair ids), aggregate failure-rate in the DB
and switch rule windows to the indexed startedAt column, clamp rule config
to the legacy contract bounds, push no_activity watch scoping into SQL
before the LIMIT, fix the generated sim icon-map key, normalize docs
wording, and drop dead exports.

Co-authored-by: Cursor <cursoragent@cursor.com>

* address comments

* fix(review): integer rule rounding, success-gated workflow labels, display module hygiene

Second-pass review fixes: round integer rule fields so fractional input
never reaches SQL LIMIT, gate workflow-name readiness on a successful
non-placeholder load in both editor and preview (errored loads mislabeled
valid workflows as deleted), lazily read the variables store in preview
rows, move the filter-field JSON preview into the shared display module
and unexport its single-consumer helpers, and align >= boundary copy
(failure rate, error count, cooldown window) with implementation.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore: sync lockfile after staging merge

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(workspace-events): keyset-paginate the no_activity subscription scan

A fixed LIMIT 500 with no ORDER BY silently starved subscriptions beyond
the cap once the global count exceeded it. The poll now pages by webhook
id so every subscription is visited each cycle; pagination bounds memory,
not total work.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(workspace-events): keyset-paginate the watched-workflow scan

The 500-row LIMIT silently and deterministically excluded high-id
workflows from no_activity coverage in watch-everything subscriptions on
large workspaces. The scan now pages by workflow id, mirroring the
subscription scan; per-workflow checks move into a helper so the
pagination loop stays flat.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(workspace-events): skip no_activity subscriptions on the execution-completion path

no_activity is poller-owned and can never fire from a completed execution,
but it passed into the rule branch and cost a pointless cooldown point-read
per subscription on the hottest path. Early-continue alongside the
workflow_deployed guard.

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs(sim-trigger): note failure-based alert conditions evaluate on failed runs

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(blocks): recategorize Data Enrichment as a core block

It's a Sim-native capability (registry enrichments over a managed provider
cascade, like Search), not a third-party integration. Moves it to Core
Blocks in the toolbar, out of the integrations catalog, and relocates its
docs page to blocks/ with the icon-map allowlist keeping the docs card icon.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(blocks): recategorize MySQL, PostgreSQL, SFTP, SMTP, SSH as integrations

External-system connectors with host/credential auth belong under
Integrations, not Core Blocks — consistent with MongoDB, Redis,
ClickHouse, and the other datastore integrations. They already carried
integrationType and /tools docsLinks; the regenerated docs pages turn
those previously-dangling links into real pages, and the blocks join the
integrations catalog and icon maps.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-10 11:04:12 -07:00
+1 39418f9615 improvement(mothership): v0.2 (#4923)
* CONTRACTS

* updates

* Prompt caching

* Fix regression

* updates to prompt caching

* Prompt caching trace

* VFS updates

* Changelog and plan

* VFS update

* Improvement/mothership (#4775)

* ff

* System role in cache

* Fixes

* Add dynamic user info

* Improve tool search

* improvement(platform): workspace UI/UX overhaul + integrations catalog

Rework the workspace around the AI-workspace model: a Mothership home, a
top-level Skills route, connected-credential and integration-detail pages,
and a polished sidebar/settings surface. Replace the notifications store
with a unified toast system (provider-level dismiss/pause, countdown ring).

Integrations & catalog:
- Add a BlockMeta layer (tags + catalog templates) scoped to catalog-visible
  integrations; every catalog integration carries >=7 grounded templates.
- Rework the taxonomy: each block declares category tools|blocks|triggers.
  3rd-party services are 'tools'; first-party primitives (postgres, mysql,
  knowledge, file, search, stt/tts, image/video generators, thinking, etc.)
  are 'blocks'. Versioned blocks follow the upgrade paradigm (old hidden,
  latest in toolbar/docs).
- Generate integrations.json + tool docs canonically from block configs.

Architecture & cleanup:
- Consolidate block data extraction behind a single latest-version strategy
  (getCanonicalBlocksByCategory; version-consistent getBlockMeta).
- Unify version-suffix handling in @sim/utils/string (stripVersionSuffix /
  isVersionedType, with tests); registry, generate-docs, tools/utils, and
  integrations all route through it.
- Repair latent broken barrels, remove dead code, fix BlockMeta-related type
  errors and 5 broken docs links.

Behavior-preserving for block execution and the toolbar's tool/block listing.

* refactor(platform): remove forms, templates, and creators features

Remove three standalone features and their supporting code:
- Forms: form-deployment pages, API routes, execution path, and docs.
- Templates: the template gallery (landing + workspace) and template APIs.
- Creators: creator-profile routes and contracts.

Add a super-user permissions module (lib/permissions/super-user) and an
organizations API contract; update the audit/db/testing packages, billing,
and the session/theme providers accordingly.

* New doc setup

* Fixes

* Fixes

* Logs tools

* test(workflows): update archiveWorkflow update count after forms removal

The forms feature was removed, dropping the form-table update from
archiveWorkflow. Update the stale assertion from 8 to 7 tx.update calls.

* Add finalization on error

* Fixes

* change dev CI to bun run db:push

* Updates

Please enter the commit message for your changes. Lines starting

* Contrat update

* improvement(nested): subagents

* updates

* upgrade

* improvement(knowledge): polish tag filter dropdowns (#4816)

* improvement(logs): object storage backed tracespans (#4787)

* improvement(logs): obj storage backed tracespans

* fix storage write context

* fix tests

* address comments

* address comments

* chore(db): remove migration 0219 to regenerate after staging merge

Drops the 0219_robust_shard SQL, its snapshot, and the journal entry so the
trace-spans/cost schema migration can be regenerated on top of the latest
staging migration chain (avoids a number collision with staging's migrations).

Co-authored-by: Cursor <cursoragent@cursor.com>

* improvement(billing): accurate per-member usage via shared ledger helper

Per-member/per-user usage in the org-member routes now adds the usage_log
ledger to the currentPeriodCost baseline (which is no longer incremented),
via a shared getOrgMemberLedgerByUser helper to avoid repeating the
subscription→period→ledger lookup across the admin and member-facing routes.

Co-authored-by: Cursor <cursoragent@cursor.com>

* regen migrations

* update migration

* address comments

* more code cleanup

* incorrect type cast

---------

Co-authored-by: Cursor <cursoragent@cursor.com>

* improvement(providers): harden OpenAI-compatible providers + add tests (#4796)

* improvement(providers): harden OpenAI-compatible providers + add tests

* fix(vllm): let tool-loop errors propagate instead of returning silent partial success

* fix(litellm): force tool_choice 'none' on final structured-output call

The deferred final call used tool_choice 'auto', so the model could emit
another tool_calls round instead of the structured answer, leaving content
stale. Use 'none' (matching vLLM/Fireworks) on both the streaming and
non-streaming final calls so the model must return the structured response.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(providers/ollama): drop tools from post-tool streaming call

Ollama ignores tool_choice (not in its supported fields), so vLLM/Fireworks'
tool_choice:'none' guard is a no-op here. Omit tools from the final streaming
payload instead so the summarization turn can't emit dropped tool calls.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(litellm): spread payload into deferred final call so reasoning_effort carries over

The non-streaming deferred finalPayload hand-picked fields and dropped
reasoning_effort (and any future payload field), diverging from the streaming
path which spreads ...payload. Spread payload here too for consistency.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(providers/ollama): restore enrichment TSDoc block

Keeps parity with sibling Chat Completions providers (cerebras/mistral/xai).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(fireworks): restore TSDoc on utils helpers

Restore the TSDoc blocks on supportsNativeStructuredOutputs,
createReadableStreamFromOpenAIStream, and checkForForcedToolUsage —
TSDoc is the codebase documentation standard and should not have been
stripped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(litellm): remove inline rationale comments (codebase uses TSDoc)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(providers/ollama): drop orphaned enrichment TSDoc

The block documented a function that now lives in trace-enrichment.ts, so it
documents nothing in this file.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* chore(copilot): deprecate mcp server (#4797)

* chore(copilot): deprecate mcp

* update error codes

* deprecate copilot api v1 route

* feat(integrations): hosted API keys for Findymail, Prospeo, and Wiza (#4777)

* feat(integrations): hosted API keys for Findymail, Prospeo, and Wiza

Add hosted-key support across all credit-consuming Findymail, Prospeo, and Wiza operations so Sim provides the key when a workspace has not brought its own. Register the three BYOK providers, consolidate Wiza's two-step reveal into a single polling wiza_individual_reveal op, and hide the API key field on hosted Sim for hosted operations.

* fix(integrations): harden Wiza reveal polling, soften enrichment getCost guards

Address Greptile + Cursor Bugbot review on #4777: return explicit failures from the Wiza individual_reveal poller instead of throwing (thrown errors were swallowed into a false queued success), short-circuit when the initial reveal is already terminal, tolerate transient 5xx/429 during polling, and return 0 (not throw) from Findymail getCost when the contacts/employees array is absent.

* chore(integrations): biome formatting after wiza merge resolution

* fix(wiza): type isTerminalReveal param structurally for next build typecheck

* feat(enrichments): add Findymail, Prospeo, Wiza to work-email waterfall

* feat(enrichments): add Wiza + Prospeo phone reveal to phone-number waterfall

* feat(enrichments): opportunistic identifiers + LinkedIn URL input across work-email & phone cascades

* fix(tables): reduce column header chevron size and fix sidebar shadow bleed (#4800)

* feat(slack): add install + privacy section to integration landing page (#4799)

* feat(slack): add install + privacy section to integration landing page

Adds a hand-authored, slug-keyed landing-content module (separate from the generated integrations.json so it survives regeneration) and renders an install walkthrough + privacy-policy link on integration pages when present. Also refreshes generated docs (data-enrichment entry, icon mappings, tool mdx).

* fix(landing): render privacy section independently, align CTA analytics label

* docs(landing): clarify the Slack install button is behind sign-in

* refactor(landing): bake integration landing content into generated json via docs-gen

Moves landing content (install walkthrough + privacy) out of a render-time augment and into the generation pipeline: generate-docs reads the pure-data content map and writes landingContent into integrations.json, so the page reads a single source (integration.landingContent). Canonical types live in integrations/data/types.ts.

* improvement(enrichments): align enrichments sidebar with design system (#4801)

* improvement(enrichments): align enrichments sidebar with design system

* fix(enrichments): consistent close button pattern and fix url link hover

* fix(misc): upgrade path change for new better-auth version, billing issue for workflow block agent usage (#4803)

* fix(misc): upgrade path change for new better-auth version, double-billing for workflow block agent usage

* fail loudly if stripe sub id missing

* fix(copilot): seq migration (#4804)

* chore(db): drop redundant idx_webhook_on_workflow_id_block_id index (#4809)

Removed because (workflow_id, block_id) is a left-prefix of idx_webhook_on_workflow_id_block_id_updated_at_desc, which fully covers it. The dropped index was non-unique and enforced no constraint.

* perf(copilot): read chat transcripts from copilot_messages (R+1 cutover) (#4808)

* perf(copilot): read chat transcripts from copilot_messages, not JSONB

Flip user-facing chat reads from the legacy copilot_chats.messages JSONB
array (5.7GB, 99% TOAST) to the normalized copilot_messages table via a
new loadCopilotChatMessages helper ordered by seq NULLS LAST, created_at,
id — the verified canonical order. Both chat-detail getters
(getAccessibleCopilotChat, getAccessibleCopilotChatWithMessages) now drop
the messages column from their metadata select (no more whole-array
detoast on every load) and assemble the transcript from the table after
authorization. This cascades to the copilot + mothership GET endpoints
and to resolveOrCreateChat's conversationHistory (the LLM payload).

The normalize/effective-transcript pipeline is source-agnostic
(copilot_messages.content == a JSONB array element), so transcripts are
byte-identical. Dual-write and the JSONB column stay in place as the
internal-logic source and fallback; removing JSONB writes is a later step.

Prod integrity verified before cutover: 0 messages missing, 0 NULL-seq,
0 dup keys/seq, 0 orphans, order-parity vs JSONB = 0 mismatches.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(copilot): cover auth-deny on a found row skips the messages query

Address PR review: exercise the `if (!authorized) return null` contract —
when the chat row exists but authorization fails, the getter returns null
and never issues the copilot_messages read.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* fix(tables): right-align run/stop in embedded toolbar; workflow cells format like normal cells (#4806)

* fix(tables): right-align run/stop in the embedded table toolbar

Add a right-aligned `trailing` slot to ResourceOptionsBar and move the embedded
mothership table's run/stop control into it, so Filter + Sort stay left-aligned
and run/stop sits opposite on the right. No-op for the search-bearing consumers
(logs, resource list), which don't pass `trailing`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(tables): workflow-output cells format values like normal cells

Workflow-output columns short-circuited in resolveCellRender and rendered their
value as plain text, so a sim-resource URL / external URL / JSON / date produced
by a workflow never got the chip, favicon link, or typed formatting a normal
cell gets. Factor value formatting into a shared `resolveValueKind` helper used
by both the workflow-value branch and the plain-cell branch; the workflow branch
keeps the typewriter reveal for plain streaming text via a `typewriter` flag.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(tables): detect resource/URL links on workflow output regardless of column type

Workflow output columns default to `json` (columnTypeForLeaf), so routing their
values through the type-based formatter (a) gated chip/URL promotion behind
`column.type === 'string'` — a URL produced by a json-typed output never became
a chip — and (b) JSON.stringify'd plain string values, adding quotes and losing
the typewriter reveal. Detect links (sim-resource chip / favicon URL) on the
value string directly for workflow outputs, falling back to the plain `value`
kind; plain cells keep the type-based formatting. Addresses Greptile P2 on #4806.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(icons): repair broken integration icon rendering (#4810)

* fix(icons): repair broken integration icon rendering

Two distinct bugs left integration icons broken on the /integrations page
(visible at 32-40px, hidden at the toolbar's 16px):

1. Corrupted SVG paths (Notion, Greptile, Granola, Calendly, Grafana, Bedrock):
   over-minified data dropped elliptical-arc flag digits (e.g. `A1 1 0 5.9 7`
   instead of `A1 1 0 0 0 5.9 7`); Granola's cubic stream was truncated. Browsers
   abort path parsing at the first invalid arc flag, so each rendered as a fragment
   or blank. Replaced with correct path data from canonical sources, preserving each
   icon's existing fill/gradient and bgColor.

2. Invisible glyph (Bright Data): its icon uses fill='currentColor' but bgColor was
   '#FFFFFF', and every surface forces text-white on the glyph - white-on-white.
   Changed bgColor to Bright Data's brand blue (#3d7ffc) so the white glyph reads,
   matching the white-glyph-on-brand-chip convention.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(icons): restore Calendly dual-tone brand colors

Addresses review feedback: the previous fix replaced the broken Calendly icon
with a monochrome #006BFF path, dropping the cyan #0ae8f0 accent from the
original dual-tone mark. Restored the two-tone logo (blue + cyan) using clean,
valid path data, cropped to a tight square viewBox so it fills the chip.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* improvement(icons): enlarge icons, fix Zoom contrast and Quiver chip

- Zoom: glyph was blue-on-blue (#0B5CFF on #2D8CFF chip); switched to
  currentColor so it renders as a white glyph on the blue chip.
- Quiver: chip bgColor #000000 -> #FFFFFF to match the icon's near-white box,
  and enlarged the mark slightly (viewBox crop).
- Enlarged (tightened viewBox, verified no clipping): RevenueCat, Prospeo,
  Granola, Firecrawl, Enrich.so, and the AWS icons (RDS, DynamoDB, SQS,
  CloudFormation, Athena, CloudWatch, SES, Bedrock, S3).
- ZoomInfo left unchanged: it is a full red rounded-square logo that already
  fills its frame, so a crop would clip it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(icons): use Bright Data wordmark on white chip; repair Circleback

- Bright Data: replaced the flame glyph with the official two-tone 'bright data'
  wordmark (provided asset), centered in a symmetric viewBox. Reverted the chip
  bgColor from #3d7ffc to #FFFFFF since the blue wordmark is invisible on a blue
  chip (the wordmark is designed for a light background).
- Circleback: a minifier had rounded the pattern's image scale to scale(0),
  collapsing the embedded logo to zero size (invisible). Restored the correct
  scale (1/280 = 0.00357142857) so the C. mark renders.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(docs): sync Quiver block color card to white chip

Reflects the Quiver bgColor change (#000000 -> #FFFFFF) in the docs block info card.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* improvement(icons): enlarge AWS/Cloudflare/Dagster icons, fully white Zoom

- Enlarged (tighter viewBox, render-verified, no clipping): Cloudflare, Dagster,
  and the red AWS icons AWS IAM, Identity Center, Secrets Manager, SES, STS.
  Identity Center was anomalously small (filled ~32% of its frame); the group is
  now sized consistently (~80% fill).
- Zoom: the camera lens triangle was still #0B5CFF (blue-on-blue); switched it to
  currentColor so the whole camera renders white on the blue chip.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(wiza): consolidate individual reveal into a single operation

Merges the separate Start/Get Individual Reveal operations into one Individual
Reveal operation in the Wiza docs and integrations data (operationCount 5 -> 4).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* improvement(icons): size remaining AWS icons to match the set (~80% fill)

Bring RDS, DynamoDB, SQS, CloudFormation, Athena, CloudWatch and S3 up to the
same ~80% fill as the AWS IAM/Identity Center/Secrets Manager/SES/STS group, so
all AWS icons are visually consistent. Bedrock left as-is (already ~92% fill).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(icons): use Bright Data flame mark, enlarge ZoomInfo

- Bright Data: the full 'bright data' wordmark was illegible at chip size.
  Replaced with just the flame-'i' brand mark (blue #4280f6 on the white chip),
  centered.
- ZoomInfo: cropped the viewBox toward the white 'Zi' so it's larger; the red
  rounded-square background still fills the chip.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* improvement(icons): enlarge CrowdStrike icon

The falcon mark sat small in its chip because the icon used a wide 768x500
viewBox (letterboxed in the square chip). Switched to a square viewBox centered
on the mark so it fills ~80%, consistent with the other icons.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* fix(tables): serialize schema mutations to prevent parallel column clobber (#4812)

* Make workflow description nullable

* fix(tables): serialize schema mutations to prevent parallel column clobber

* fix(tables): load workflow outside schema lock; use DbOrTx for getTableById

* fix(tables): scale idle timeout in updateColumnType to avoid aborting large type changes

* fix(tables): skip stale remap types when workflowId changes concurrently

* fix(tables): scale idle timeout in updateColumnConstraints for large tables

* fix(wait): resume live/draft async waits and preserve cell context on chained waits (#4814)

* Make workflow description nullable

* fix(wait): resume live/draft async waits and preserve cell context on chained waits

* improvement(knowledge): polish tag filter dropdowns

* improvement(knowledge): soften filter section labels

* improvement(knowledge): soften list filter labels

* fix(security): harden SSO domain registration, webhook path isolation, and CSV export (#4813)

* fix(security): harden KB file access, SSO domain registration, webhook path isolation, env secrets, and CSV export

* fix(sso): scope domain conflict query with indexed lower(domain) filter

Address PR review: avoid a full-table scan on every SSO provider
registration by filtering candidate rows in SQL with
lower(domain) = <normalized>, keeping the in-memory ownership check.
Also tighten the normalizeSSODomain TSDoc.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore: condense env route security comments

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* icons update

* chore(security): tighten inline comments in CSV export and KB file authorization

Condense verbose comment blocks to concise TSDoc/single-line form; no behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(security): validate internal serve origin in KB file authorization

Replace the bypassable isInternalFileUrl substring check in resolveInternalKbKey
with an origin allow-list (base URL, internal API base URL, TRUSTED_ORIGINS).
A crafted external host whose path is /api/files/serve/<victim-key> no longer
resolves to the victim key. Relative same-origin URLs are unaffected.

* style(sso): use idiomatic sql lower() comparison for domain conflict query

Match the repo's prevailing `sql`lower(col) = value`` idiom for the
case-insensitive SSO domain conflict lookup.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(security): align workspace env admin gate with hasWorkspaceAdminAccess

Use the same admin check the secrets UI uses (owner, admin permission, or
org-admin) so owners and org-admins are not wrongly denied their own decrypted
workspace secrets, while read-only members remain restricted to names only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(sso): rely on lower(domain) match for conflict detection, drop dead in-memory recheck

Address PR review: the SQL `lower(domain) = <normalized>` predicate already
excludes rows that the in-memory `normalizeSSODomain(...) === domain` recheck
claimed to catch, making that recheck dead/misleading code. Match on the
canonical lower-cased domain and filter purely by ownership. Malformed legacy
values (wildcards, schemes, ports) never match an email domain at sign-in, so
excluding them is not a gap. Test DB mock now applies the lower() predicate so
the casing-variant case is genuinely exercised.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(security): scope webhook deploy path conflict to active webhooks

findConflictingWebhookPathOwner omitted the isActive filter that the
runtime dispatcher (findAllWebhooksForPath) applies, so an inactive but
non-archived webhook from another workflow (e.g. after undeploy or
failure auto-disable) would permanently block any new deployment on that
path even though it never receives deliveries. Align the guard with the
runtime isActive + archivedAt filter; the earliest-owner runtime check
remains the authoritative cross-tenant protection. Also trims verbose
TSDoc on the webhook path-isolation helpers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(security): exclude archived workflows from webhook deploy path conflict

findConflictingWebhookPathOwner now joins workflow and filters
isNull(workflow.archivedAt), matching the runtime dispatcher
(findAllWebhooksForPath). A webhook on an archived workflow can never
receive deliveries at runtime, so it must not block legitimate path reuse
with a 409.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(security): anchor KB file ownership to earliest document in any state

A KB file's owner is now the earliest document referencing its key regardless of
state (active/archived/deleted/excluded); access is granted only when that owning
document is still active. Closes the residual where an attacker could plant an
active document to claim a file whose original document was archived or deleted.

* updated greptile icon

* revert(security): drop KB file authorization changes

Reverts the knowledge-base file-access work (origin-pinning / owner-pinning /
origin allow-list in verifyKBFileAccess) and its test. The other hardening fixes
(SSO domain registration, webhook path isolation, workspace env secrets, CSV
export) are unchanged. apps/sim/app/api/files/authorization.ts is restored to its
origin/staging baseline.

* fix(sso): treat caller's own user-scoped provider as owned during conflict check

Self-hosters often register SSO user-scoped via the CLI script (no
SSO_ORGANIZATION_ID). If they later enable organizations and reconfigure the
same domain org-scoped through the UI, the conflict check previously treated
their own user-scoped row as another tenant's and returned a misleading 409.
Recognize the caller's own user-scoped provider as owned so that migration is
allowed, while still blocking another user's or another org's domain.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* revert(security): remove workspace-env admin gate

Defer to a credential-based access model (separate change). Restores
GET /api/workspaces/[id]/environment to main behavior and removes the test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(security): consolidate webhook path-collision check into one helper

Extract findConflictingWebhookPathOwner to lib/webhooks/utils.server.ts as
the single source of truth for cross-tenant path-collision detection, used by
both webhook creation paths (deploy sync and the manual /api/webhooks route).

This also repairs two latent issues in the manual route's previous inline
check, which queried with limit(1) and only webhook.archivedAt:
- limit(1) inspected one arbitrary row, so a same-workflow row could mask a
  foreign collision (false negative). The shared helper scans all matching
  rows.
- It omitted isActive/workflow.archivedAt, so inactive or archived-workflow
  webhooks (which never receive deliveries) permanently blocked path reuse.
  The helper mirrors the runtime dispatcher's filter.

Same-workflow webhook reuse for upsert is now a separate, explicit lookup.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* fix(security): block private/reserved IPs for hosted 1Password Connect SSRF (#4818)

* fix(security): block private/reserved IPs for hosted 1Password Connect SSRF

* test(security): use real isPrivateOrReservedIP and cover IPv6 edge cases

* improvement(integrations): validate and expand devin, cursor, and greptile (#4820)

* improvement(integrations): validate and expand devin, cursor, and greptile

- devin: fix missing org_id path segment on all session endpoints, add 7 session sub-resource tools (list messages/attachments, get/append/replace tags, archive, terminate), pagination, and is_archived output
- cursor: add get_api_key_info, list_models, list_repositories tools
- greptile: align block and docs
- normalize array outputs to default [] and tighten types

* refactor(cursor): simplify list_repositories v2 array normalization

Collapse the redundant `?? []` + `Array.isArray` double-guard into a
single Array.isArray check, per PR review feedback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(devin): scope session-tag mapping to tag ops and normalize array tag inputs

- Only map sessionTags into the tools tags param for append/replace operations,
  preventing stale sessionTags state from clobbering create_session tags
- Fall back to a wired tags value when sessionTags is empty for tag operations
- Normalize tag inputs (string or wired string[]) via normalizeTags so array
  values from other blocks no longer throw on .split

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cursor): restore base64 file data in legacy download_artifact metadata

The legacy CursorBlock exposes only content + metadata (no v2 file
output), so metadata.data was the only way legacy-block workflows could
access downloaded artifact bytes. Restore the base64 data field and
document it in the outputs/type instead of dropping it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(devin): coerce terminateArchive to archive flag for boolean-wired input

* docs(integrations): regenerate tool docs for new devin and cursor operations

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* fix(search-replace): don't auto-navigate when content edits invalidate the active match (#4819)

* fix(search-replace): don't auto-navigate when content edits invalidate the active match

* fix(search-replace): clear afterReplaceIndexRef on apply failure and zero matches

* fix(search-replace): remove duplicate setActiveSearchTarget(null) on close

* fix(search-replace): move afterReplaceIndexRef write inside handleApply past the guard

* fix(search-replace): auto-navigate when hydration resolves with no prior active match

* chore(search-replace): remove inline comments

* fix(search-replace): revert !activeMatchId guard that caused immediate re-navigation after deselect

* improvement(enrichments): limit company-info to fields both providers return (#4817)

Hunter's company dataset returns null industry/foundedYear for many large companies (verified against the live API for Microsoft, Amazon, Google), so under the first-non-empty-wins cascade those columns appeared inconsistently across rows. Limit company-info outputs to employee count and description — the fields Hunter and PDL both reliably return — so every row is consistent. employeeCount is a string so Hunter's range bucket and PDL's exact count share the column.

* fix(files): don't reject external URLs containing '..' in file parse validation (#4821)

* fix(files): don't reject external URLs containing '..' in file parse validation

The file block's file_fetch operation rejected any external URL whose path
contained '..' (e.g. Slack files-pri slugs with a literal '...') with
'Access denied: path traversal detected'. Traversal checks only apply to
local paths — external http(s) URLs are fetched with SSRF protection
downstream and are never resolved against the filesystem, so they now
short-circuit as valid. Internal /api/files/serve/ URLs keep full traversal
protection.

* test(files): fix external-URL assertion to handle undefined error

* test(files): assert success explicitly in external-URL traversal test

* fix(files): keep traversal protection for https URLs matching internal serve paths

* feat(google-sheets): add row filtering to read with numeric operators (#4822)

* feat(google-sheets): add row filtering to read with numeric operators

Adds client-side row filtering to the Google Sheets read (v2) operation.
Filter the returned rows by a header column using text operators
(contains, not_contains, exact, not_equals, starts_with, ends_with) and
numeric/ordering operators (gt, gte, lt, lte). Filtering lives in a pure,
unit-tested helper (filterSheetRows) and runs over the fetched read range;
an optional `filter` output reports whether the column was found and how
many rows matched.

Also hardens the surrounding tools:
- trim spreadsheetId in write/update/append URL builders (matches read)
- URL-encode the v1 read default range
- expose valueInputOption for the update operation in the block

Backwards compatible: with no filter requested, read output is byte-
identical and the `filter` field is omitted. The filterMatchType union is
widened additively (4 -> 10 values).

* fix(google-sheets): correct filter metadata for missing column and header-only sheets

- matchedRows is now 0 (not totalRows) when the filter column is not found,
  so it no longer contradicts applied=false / columnFound=false
- columnFound now reflects an actual header lookup for empty/header-only
  sheets instead of being hardcoded true
- add tests covering header-only and empty sheets with present/absent columns

* fix(selectors): fetch all pages for paginated dropdown list routes (#4823)

* fix(selectors): fetch all pages for paginated dropdown list routes

Dropdown selectors fetched only the first page of paginated provider
APIs, silently hiding results past page one. Add bounded server-side
draining to the list routes across Microsoft Graph, Google, Notion,
Atlassian, Linear, AWS CloudWatch, and offset/token REST APIs, plus a
shared client-side drain cap in the selector hook. Response shapes,
stored values, and tool execution are unchanged; CloudWatch list tools
still honor a caller-supplied limit. Also fixes the Word file picker
that was searching for .xlsx files.

* fix(selectors): harden JSM and Monday pagination draining

- JSM service-desk/request-type drains advance `start` by the actual row
  count returned (not the fixed page size) and stop on an empty page, so a
  short non-final page can't skip items.
- Monday boards drain now checks `response.ok` per page, surfacing a
  mid-drain HTTP failure instead of treating it as an empty final page and
  returning a partial 200.

* docs(selectors): clarify JSM drain advances start by actual row count

The offset-advancement fix (advance `start` by the rows returned, not the
fixed page size) landed in 7b19788a8; update the TSDoc to match so it no
longer reads as advancing by `limit`.

* fix(selectors): drain fetchPage in direct fetchList callers

Making `fetchList` optional left three direct callers (outside the
useSelectorOptions hook) calling it unguarded, which broke the build's
type check. Route them through a shared `loadAllSelectorOptions` helper
that uses `fetchList` when present and otherwise drains `fetchPage`.
This also prevents a regression: `confluence.spaces` / `knowledge.documents`
now paginate via `fetchPage` only, and these callers (search/replace,
value resolution) would otherwise have silently returned no options.

* chore(selectors): rename MAX_PAGE_PAGES to MAX_NOTION_PAGES for readability

* fix(sso): re-check domain conflict before write and reject IP-address domains (#4825)

* improvement(copilot): make copilot_messages the sole transcript store, remove JSONB dual-write (#4826)

Stop writing/reading the legacy copilot_chats.messages JSONB column now that
reads are cut over to copilot_messages. Make appendCopilotChatMessages the
primary write (throws on failure instead of swallowing), repoint peripheral
readers (workspace VFS, chat cleanup, data drains, fork, superuser import) to
copilot_messages, and persist the assistant turn inside finalizeAssistantTurn's
transaction so it commits atomically with the stream-marker clear. The column
itself is dropped in a follow-up migration after this bakes.

* feat(tables): expand filter operators (not-contains, starts/ends-with, not-in, empty) (#4827)

Add does-not-contain ($ncontains), starts-with ($startsWith), ends-with
($endsWith), not-in-array ($nin, previously executed server-side but unexposed
in the UI), and is-empty/is-not-empty ($empty) filter operators end-to-end —
SQL builder, condition types, query-builder converters/constants, the filter
UI, the Table tools/block descriptions, and docs.

Also fix correctness bugs in the filter builder surfaced by the wider operator
set:
- Same-column AND rules (e.g. age > 18 AND age < 65, or name startsWith 'A'
  AND name endsWith 'Z') silently overwrote each other because the AND group
  was keyed by column name. They now merge into one operator object, which
  also makes Filter -> rules -> Filter round-trip losslessly for multi-operator
  columns.
- $nin values were not split into an array like $in, and textual-match values
  like "123" were numeric-coerced (breaking the ILIKE path).
- A non-boolean $empty operand from the raw API silently inverted the check; it
  now coerces 'true'/'false' strings and otherwise returns a 400.

* improvement(copilot): stop persisting tool-call result outputs in transcripts (#4829)

Opening a Mothership task could take many seconds because a single persisted
assistant message in copilot_messages.content can reach hundreds of MB, almost
entirely inside contentBlocks[].toolCall.result.output (e.g. a get_workflow_logs
or run_workflow result). The DB query is ~2ms; the cost is detoasting that
payload, shipping it to the browser, and parsing it.

These outputs are dead weight on the Sim side: they are never rendered (the
thread shows only tool name/title/status) and never replayed to the model (the
upstream copilot service owns conversation memory). So drop result.output before
it is persisted, keeping result.success/error plus the tool metadata.

- add stripToolResultOutput() in persisted-message.ts
- apply it in messages-store toRow (covers every write path) and in
  loadCopilotChatMessages (existing rows render fast on read)

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* feat(providers): add Together AI, Baseten, and Ollama Cloud model providers (#4830)

* feat(providers): add Together AI, Baseten, and Ollama Cloud model providers

* fix(providers): guard Ollama streaming fast-path with hasActiveTools

Match Together/Baseten/Fireworks: when tools are supplied but all are
filtered out (usageControl 'none'), take the single streaming call instead
of an extra non-streaming round-trip.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(providers): filter non-chat model types from Together model list

* refactor(providers): dedupe Ollama Cloud upstream schema

ollamaCloudUpstreamResponseSchema was byte-for-byte identical to
ollamaUpstreamResponseSchema (both /api/tags endpoints return the same
{ models: [{ name }] } shape). Drop the duplicate and reuse the shared schema.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* fix(knowledge): calendar view sync, deduplicate popover animation classes, type-safe filter cast

* cleanup(knowledge): remove TRIGGER_BORDER_CLASS duplication, inline displayLabel, drop enabledFilterParam alias

---------

Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Waleed <walif6@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Theodore Li <theo@sim.ai>
Co-authored-by: andresdjasso <andresdjasso@users.noreply.github.com>

* feat(blocks): add BlockMeta to Quiver and Linq; fix invalid block config fields; update skills

Block fixes:
- Add QuiverBlockMeta (tags + 3 templates: icon generator, diagram creator, vectorizer)
- Fix QuiverBlock: remove invalid tags field from BlockConfig, IntegrationType.Design →
  IntegrationType.AI (Design doesn't exist in the enum)
- Fix GreptileBlock: remove invalid tags field from BlockConfig,
  IntegrationType.DeveloperTools → IntegrationType.DevOps
- Fix LinqBlock: remove invalid tags field from BlockConfig (tags belong only in BlockMeta)

Skills:
- add-block: add dedicated BlockMeta section with structure, rules, and registration
  pattern; add BlockMeta checklist items
- add-integration: add BlockMeta to block structure template, add rules clarifying
  that tags must NOT appear on BlockConfig and integrationType must be a valid enum
  value; update registry snippet to include blocksMeta; add checklist items

* fix(integrations): fix category dropdown by defining missing LANDING_INTEGRATIONS_DATA_PATH and regenerating integrations.json

The staging merge introduced landing-content.ts but forgot to define
LANDING_INTEGRATIONS_DATA_PATH in generate-docs.ts, causing the script
to crash before writing integrations.json.

The stale JSON had integrationTypes (plural array) from an older script
version, while the Integration type and workspace UI both read
integrationType (singular string) — so ALL_CATEGORY_SECTIONS bucketed
to undefined and the category filters never appeared in the dropdown.

Fixed by adding the missing path constant and re-running the generator.
integrations.json now has 192 entries with the correct integrationType field.

* fix(sidebar): restore resize handle on all pages

commit 3109104582 wrapped the resize handle in {(isCollapsed ||
isOnWorkflowPage) && ...} and added a useEffect that resets sidebar
width to SIDEBAR_WIDTH.MIN whenever the user navigates away from a
workflow page. Together these made the sidebar non-resizable on Tasks,
Tables, Knowledge Base, and every other non-workflow page.

Restore the staging behavior: always render the resize handle and
remove the effect that forced the width reset on page transitions.

* fix(sidebar): match staging onKeyDown and tabIndex on resize handle

The resize handle was still conditionalizing onKeyDown and tabIndex
on isCollapsed, blocking keyboard accessibility of the separator role
when expanded. Staging always attaches both unconditionally.

onKeyDown={isCollapsed ? handleEdgeKeyDown : undefined} → onKeyDown={handleEdgeKeyDown}
tabIndex={isCollapsed ? 0 : undefined}                 → tabIndex={0}

* feat(integrations): show connected credentials on integration detail page

When navigating to /integrations/google-docs (or any integration), a
Connected section now appears above the templates listing all workspace
credentials tied to that provider. Each row links back to the credential
detail page (/integrations/connected/${id}) for management actions.

Pairs with the earlier change that routes connected items from the
integrations list to the provider detail page instead of directly to
the credential detail page.

* fix(integrations): rename Add in chat to Add to Sim

* fix(skills): rename Add button to Add to Sim

* fix(platform): restore M1/M2/M3 regressions and LazyMotion on landing page

M1 — Invitation guard: re-introduce usePermissionConfig().isInvitationsDisabled
alongside the workspace inviteDisabledReason check. The flag now also
respects NEXT_PUBLIC_DISABLE_INVITATIONS and EE permission-group
disableInvitations, not just billing policy.

M2 — Settings redirects: /settings/integrations and /settings/skills
now server-redirect to /integrations and /skills respectively so old
bookmarks and emails don't silently land on General.

M3 — Starter block search exclusion: restore block.type !== 'starter'
guard in the search store so users cannot add a duplicate Starter block
via the command palette.

LazyMotion: restore LazyMotion + domMax/domAnimation wrappers and m.*
components in landing-preview-panel and landing-preview-home. The
removal was accidental (the full motion bundle was left after an
import cleanup), which caused the entire framer-motion feature set to
load eagerly on the landing page.

* fix(integrations): revert connected list to credential detail; remove settings redirects

* feat(sidebar): restore workspace switcher search with updated styling

Shows a search input in the workspace dropdown when the user has more
than 3 workspaces (WORKSPACE_SEARCH_THRESHOLD). Keyboard navigation:
ArrowDown/Up to move through results, Enter to switch, resets on close.

Styled to match the current branch (border-1/surface-5 tokens, sm text,
11px Search icon) rather than the old staging styles. Highlight state
is wired through chipVariants active prop so it follows the same active
appearance as clicked/hovered items.

* fix(sidebar): clean up workspace search — layout, memo, and effect guard

* fix(sidebar): align workspace rename input selection style with workflow rename

* perf(sidebar): eliminate React re-renders during sidebar drag resize

Previously, every mousemove during resize called setSidebarWidth(), which
both updated the --sidebar-width CSS variable (sync) and set Zustand state
(async). This caused:
  1. A 1-frame transition flash on mousedown — isResizing state had to
     round-trip through React before the is-resizing CSS class was applied,
     so the width transition fired for the first pixel of movement.
  2. A React re-render per pixel dragged — components reading sidebarWidth
     from the store (avatars, usage-indicator) lagged one frame behind the
     container, making the + and ... buttons appear to jump ahead.

New approach:
  - handleMouseDown adds is-resizing directly to the sidebar DOM node before
    any React involvement (synchronous, no frame lag).
  - mousemove writes only to the CSS custom property (zero React renders).
  - mouseup persists the final width to Zustand exactly once.
  - isResizing / setIsResizing state removed from the store and hook — they
    are no longer needed since the class is managed via direct DOM mutation.

* perf(sidebar): add requestAnimationFrame throttle to resize mousemove handler

* fix(sidebar): fix drag-right lag caused by WorkspaceChrome overflow-hidden transition

The sidebar-container's is-resizing class correctly suppressed its own width
transition, but the two wrapper divs in WorkspaceChrome both have
transition-[width]/transition-transform with a 175ms ease. The outer wrapper
also has overflow-hidden, so while the sidebar content was at the correct width
instantly, it was visually clipped by the outer wrapper which was still
animating — causing the + and ... buttons to appear to lag behind the resize
line on drag-right (not on drag-left, since shrinking doesn't clip content).

Fix: add sidebar-shell-outer/sidebar-shell-inner class names to both chrome
wrappers, and suppress their transitions via html.sidebar-resizing rule when
a drag is active. The html.sidebar-resizing class is toggled directly in the
resize hook alongside is-resizing, so it takes effect synchronously on mousedown.

* fix(icons): redesign Download icon to match Upload style; fix Upload/Download confusion

Download icon was missing the tray/shelf line at the bottom that Upload has,
making it look like a plain arrow rather than a matched pair. Updated Download
to use the same viewBox, stroke weight, and three-path structure as Upload
(tray + stem + arrowhead), just pointing down.

Also fix 5 places where Upload (↑) was incorrectly used for download/export
actions:
  - files.tsx: two Download action rows in toolbar and context menu
  - tables/table.tsx: Export CSV toolbar button
  - table-context-menu.tsx: Export CSV context menu item
  - logs.tsx: Export toolbar button
  - landing-preview-logs.tsx: decorative Export button

Import CSV and actual upload actions correctly keep the Upload icon.

* fix(icons): replace Upload with Download on all remaining export/download actions

- panel.tsx: Export workflow dropdown item
- context-menu.tsx: Export in sidebar workflow context menu
- chat.tsx: Export chat button
- output-panel.tsx: Export console CSV button
- terminal.tsx: Export console CSV button
- resource-content.tsx: Export table as CSV + Download file buttons

* fix(icons): fix remaining Upload→Download on download actions in files and logs

- action-bar.tsx: download button in files toolbar
- file-row-context-menu.tsx: Download item in file context menu
- file-download.tsx: both download buttons in log details file viewer

* updated block skills, settings pages, modals, buttons -> chips, blocks missing metadata

* updated skill modal

* improvement(resource-header): refine breadcrumb truncation ux

* Fixes

* improvement(resource): add floating overflow text tooltips

* wire up credits counter

* improvement(resource-header): mute path dropdown title

* refactor(resource-header): share floating-tooltip engine, prune dead overlay tooltips (#4844)

Clean up the breadcrumb truncation feature for reuse and correctness:

- Extract useFloatingTooltip / useIsOverflowing / FloatingTooltip into a shared
  floating-tooltip module. BreadcrumbSegment and FloatingOverflowText now consume
  one implementation instead of duplicating ~150 lines of positioning, velocity,
  overflow-detection, and portal logic.
- Replace the hardcoded terminal-label regex in ResourceHeader with a typed
  `terminal` flag on BreadcrumbItem (set by the document chunk/loading crumbs),
  decoupling the generic header from knowledge-base copy.
- Clear the path-popover close timeout on unmount and reuse the shared
  POPOVER_ANIMATION_CLASSES constant.
- Drop the redundant manual overflow-state writes (fixes a sticky fade mask).
- Revert FloatingOverflowText inside Combobox `overlayContent` back to plain
  truncating spans across files/logs/tables/scheduled-tasks/document: the combobox
  overlay is pointer-events-none, so the tooltip handlers never fired there.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(emcn,resource-header): address PR #4844 review feedback

- useIsOverflowing now uses a callback ref so the ResizeObserver follows the
  element across mount/unmount/reassignment instead of capturing it once at mount.
  Safe for conditionally rendered consumers of the shared hook. (greptile P2)
- Move POPOVER_ANIMATION_CLASSES out of chip-date-picker implementation internals
  into emcn/components/popover/popover-animation.ts, exported from the
  @/components/emcn barrel. Consumers now import from the module boundary.
  (greptile P2)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Prompt surface

* Regenerated contracts

* upgrade table and styling upgrade

* Literalness gaps

* Updates

* Remove prefix for running code

* Display names for reads

* Tool catalog

* fix schema to include integration

* fix(files): align delete icon with tables view (Trash → Trash2)

Co-Authored-By: waleed <waleed@simstudio.ai>

* fix(mothership): preserve blockType for integration contexts in sent messages

Integration mention chips were missing their provider icons in sent messages
because blockType was dropped when mapping ChatContext to messageContexts.
renderIntegrationTile returns null without blockType, silently hiding the icon.

* fix(mothership): allow 'integration' resource type in chat resources API

The VALID_RESOURCE_TYPES allowlist was missing 'integration', causing a
400 error when adding integrations to the Mothership resource tab — so
they never persisted and disappeared on refresh.

* fix(ui): Add "File" title next to file resource header

* fix(ui): fix resource header columns being bolded

* fix(resource): keep the floating tooltip from jumping on click

Gate the focus-driven show behind :focus-visible so a mouse click (which
focuses the trigger) no longer re-shows the tooltip anchored to the element's
bottom edge. On click the tooltip now hides cleanly instead of jumping down;
keyboard focus still shows it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* perf(sidebar): eliminate unnecessary re-renders in workspace switcher for non-search users

- onMouseEnter: only set highlightedIndex when showSearch is true, preventing
  a state update + re-render on every workspace row hover for users with ≤ 3
  workspaces where the search is never shown
- onOpenChange: only reset workspaceSearch and highlightedIndex when showSearch
  is true, since both values are always already at their defaults for non-search
  users and setting them triggers a pointless re-render during dropdown close
- data-workspace-row-idx: only set when showSearch is true since the scroll
  effect that reads this attribute is already gated on showSearch

* feat(search): context-aware cmd-k results on the integrations page

When cmd-k is opened on the integrations page, show two new result
groups: connected accounts (visible even with empty input) and catalog
integrations (appear once the user types). Selecting an OAuth integration
deep-links to its detail page with ?connect=oauth so the connect modal
auto-opens. Non-OAuth integrations navigate to the plain detail page.

Both groups are gated to the integrations page only and respect the
hideIntegrationsTab permission. The credentials fetch shares the same
React Query cache key as the integrations page itself (no double fetch).

* refactor(emcn): make the floating tooltip the one canonical Tooltip

Replace the Radix-based emcn Tooltip with the cursor-following floating tooltip so
every tooltip in the app uses one consistent style. Built on the shared
floating-tooltip engine (relocated into emcn), not a parallel implementation.

- Move the floating-tooltip engine into emcn/components/tooltip and export it from
  the barrel; re-point its consumers (FloatingOverflowText, resource-header)
- Extend the FloatingTooltip bubble to render arbitrary children (+ role/id for
  a11y) so it can back general tooltips, not just overflow text
- Rebuild emcn Tooltip (Root/Trigger/Content/Provider/Shortcut/Preview) on
  useFloatingTooltip — compound API preserved, ~350 call sites unchanged, legacy
  side/align props accepted and ignored (the tooltip follows the cursor). Removes
  @radix-ui/react-tooltip usage (package kept for a later cleanup; react-slot
  retained for asChild)

Note: general tooltips now show instantly (no hover delay) and follow the cursor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* style(emcn): put tooltip text on the design scale (text-caption)

Replace the tooltip's ad-hoc `text-xs` + `leading-[18px]` with the semantic
`text-caption` (12px) font-size token so the text styling is fully on the design
scale and self-documenting, matching how the rest of the system is set up. The
color already used the global `--text-body` token. No visual change (still 12px
with a ~18px line height).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Mship byok

* feat(sidebar): add empty task state and inline task creation

- Show "No tasks yet" in the Tasks section (expanded and collapsed) when the list is empty
- Clicking + now creates a task via the API and navigates directly to it, rather than navigating to home
- Add isCreatingTaskRef guard to prevent double-click from spawning multiple tasks
- Disable + button while creation is pending
- Fall back to home navigation on creation error

* invite, billing, home

* Sandbox warning

* improvement(seats): auto purchase seats on invitations into workspace (#4857)

* improvement(seats): auto purchase seats on invitations into workspace

* improve sampling for seat drift reconciler

* address comments

* feat(knowledge): align connector UI with integrations page styling

- ConnectorTypeCard now matches integration rows: brand-colored rounded-xl tile, ArrowRight, title/subtitle hierarchy
- ConnectorCard icon upgraded from flat surface-4 to branded tile (white icon on brand bg, graceful fallback)
- Connector header badges use chipVariants instead of custom Button classes
- Add-connector search input aligned to integrations style (h-[30px], rounded-lg, border-1)

* fix(icons): trim Folder SVG viewBox to remove right-side whitespace

The folder path only extends to x≈14.33 in a 15-unit viewBox, leaving
~0.5 units of empty space on the right. At 12px rendered size this
produces ~0.4px extra gap (visible as ~1px on retina displays) compared
to solid icons like the workflow color square. Trimming the viewBox to
14.5 units makes the folder fill its chip slot evenly.

* fix(user-input): restore draft text synchronously to preserve contexts on nav

The SSR-safe approach (empty useState + effect restore) created a timing
window where the sync effect in useContextManagement fired with message=''
before the value was set, clearing any restored contexts. Folder and workflow
contexts (not re-added by applyAutoMentions) were lost on every nav-back.

Revert to the staging approach: initialize value synchronously from the
draft store so message is already populated when effects run, matching
the behavior on staging.

* fix(queue): render context chips in queued messages

- Remove plainMentions from queued message rows so context chips render
  with icons, consistent with sent messages
- Fix computeMentionRanges to use '/' prefix for skill contexts (content
  has the slash trigger restored at submit time, not '@')

* fix(mothership): remove integrations from add-resource dropdown

* fix(mothership): comment out integrations from add-resource dropdown

* File serializer

* Skills

* chore(db): drop form, templates, template_creators, template_stars tables

These tables backed the Forms and Templates platform features which were
intentionally removed from this branch. Clean up the DB schema to match.

* chore(db): add migration metadata for 0224 drop tables

* block icons, sidebar, toolbar

* chore: remove remaining dead code for template-profile feature

- Remove 'template-profile' from SettingsSection union type
- Remove 'template-profile' entry from SECTION_TITLES
- Remove now-redundant template-profile guard in settings sidebar
- Remove commented-out template-profile nav item

* fix(multi-select): preserve anchor on range selection for tasks and folders

After a shift+click range, the anchor (lastSelectedTaskId / lastSelectedFolderId)
was being updated to the end of the range (toId). This caused subsequent
shift+clicks to extend from the wrong point instead of the original click.

Standard behavior: anchor stays at the initial click (fromId) so repeated
shift+clicks always expand/contract relative to where you started.

* Update skills

* Load skill tool

* Fix vfs dynamic context encoding

* Media subagent

* feat(emcn): add SearchInput component and unify search bars platform-wide

- Add SearchInput to emcn: 30px chip-family filled search input matching the
  integrations page pattern (border-1, surface-5, leading Search icon)
- Migrate all 22 search bars across settings, EE pages, and integrations to
  SearchInput (only layout classes allowed at callsites)
- Rename Sim Keys -> Sim API Keys in nav/title; page copy now says API key
- Remove components/ui input, label, and verified-badge; migrate consumers
  to emcn equivalents or raw inputs (table cell editor, wand prompt bar)
- Delete dead EE skeleton files (data-drains, data-retention)
- General settings: Home Page chip moves to header left as navigation

* fix(files,tables): restore new-file editor autofocus and CSV import error toasts

Both were dropped on the staging line and regressed vs production (main):

- Files: the new-file editor autofocus chain (files.tsx -> file-viewer ->
  text-editor) was stripped by the react-doctor dead-code pass in #4544,
  which misread the prop-drilled `autoFocus` (consumed by an imperative
  `editor.focus()` effect) as unused. Restored the prop through all three
  layers and the one-shot focus effect so creating a new file focuses the
  editor immediately.
- Tables: CSV import failures were silently logged with no user feedback.
  Restored the per-file and generic `toast.error` surfacing.

* Move superagnet back into superagent

* feat(home): score suggested actions by workspace signals

- Derive the suggestion pool from the curated block template catalog
  (1,343 prompts across 172 blocks) instead of 15 hardcoded entries
- Fix inverted relevance: prompts for connected providers are now boosted
  4x (instantly runnable) instead of excluded; unconnected discounted 0.4x
- Weight by featured (3x), popular category (1.5x), and resource gaps
  (no tables -> boost table starters; has KBs -> dampen KB-creation prompts)
- Weighted sampling without replacement, max one suggestion per block
- Connect rows weighted by catalog template count; 2 for fresh workspaces,
  1 once something is connected
- Key the catalog map by both versioned and base block types so gmail_v2
  templates resolve (gmail, github, notion, linear were silently dropped)
- Replace derive-in-effect state with a useMemo keyed by a shuffle nonce
- Add suggested_action_clicked / suggested_actions_shuffled /
  suggested_actions_toggled PostHog events

* billing, teammates

* improvement(credentials): credentials invites, secrets tab wiring up (#4874)

* improvement(credentials): move away from invite notion

* wire up secrets ui/ux

* address comments

* get consistent styling by removing emcninput + text area

* styling consistency

* remove fallback

* address comment:

* refactor(ui): migrate settings & workspace UI to chip design system

Migrate modals to ChipModal (showDivider, hint, resizable, size, leading,
ChipModalTabs), standardize Chip variants, add ChipCombobox wrapper, and apply
chip inputs/dropdowns across settings, knowledge, logs, tables, inbox, EE tabs.
Render ChipModalTabs as a ChipSwitch segmented control. Align /settings/secrets
detail with /integrations, refresh whitelabeling, and restore file-editor
autofocus and CSV import error toasts.

* Update media

* fix(mothership): restore integrations to useAvailableResources for @ mention

Integrations were fully removed from useAvailableResources which broke
the @ mention menu since user-input shares the same hook. Now integrations
are always included in the hook but excluded at the AddResourceDropdown
component level, keeping them out of the sidebar + menu while remaining
available for @ mention autocomplete.

* refactor(settings): chip design-system consistency pass across all tabs

Extract a shared chip-field shell (CHIP_FIELD_SHELL/CHIP_FIELD_INPUT) mirroring
Input variant='chip' and route secrets, credential detail, and integrations
credential detail through it (30px height, font-medium, focus ring). Add a
Discard action to the secrets header when dirty. Group BYOK providers into
Models/Search & web/Enrichment sections and align its tiles to the integrations
tile.

Normalize list-row typography to text-[14px]/text-[12px] and icon tiles to
rounded-xl + border across api-keys, copilot, custom-tools, mcp,
workflow-mcp-servers, credential-sets, and access-control. Tone the secrets
Details chip and per-row affordances to ghost. Fix token correctness: raw
tailwind colors to design tokens (data-drains), missing chip variant on the
Snowflake role input, ColorInput chip-field reuse (whitelabeling), error token
and icon sizes (workflow-mcp-servers), border token (mcp), no-results sizing
(secrets), row chrome (recently-deleted), hover token and Button to Chip
(access-control), and deduped textarea chrome (sso).

* feat(settings): unify filter dropdowns on ChipSelect (integrations style)

Add a ChipSelect emcn component — a filled chip trigger + chevron opening a
DropdownMenu, matching the integrations category filter — supporting single
select, multi-select (checkbox rows), grouped options, and optional in-menu
search. Migrate every settings/EE filter dropdown off ChipCombobox to it:
audit-logs (resource-type multi + time-range), data-retention, data-drains,
general, admin (grouped tool picker), inbox status filter, and the workflow
MCP-server pickers. The SSO provider-id field stays an editable combobox since
it accepts free-text slugs.

Also fix audit findings: chip the MCP client-secret input, normalize an MCP
error-text size, and drop now-dead destination-icon code in data-drains.

* fix(mentions): require explicit @ for integration mentions; decorate sent messages robustly

- Bare integration names in prose (Monday, Notion, Clay) are no longer
  auto-converted to mentions or chipped — mention treatment is strictly
  opt-in via a token-starting @ (fixes the scunthorpe problem)
- @-prefixed mentions still canonicalize casing (@slack -> @Slack) on both
  the keystroke fast-path and bulk paths (paste, template, draft, STT)
- Sent/queued messages now self-sufficiently decorate @IntegrationName
  tokens via a text scan, covering messages sent before the input pass
  ran or authored outside the chat input
- Integration contexts missing a resolvable blockType (messages persisted
  before blockType was saved) are backfilled by label lookup so their
  mention pills render the brand icon again

* refactor(settings): section the API keys page like secrets

Wrap Workspace, Personal, and the allow-personal-keys toggle in SettingsSection
(muted label + divider) instead of bare bold headers, matching the secrets and
BYOK pages.

* fix(settings): ChipSelect renders above modals + full-width form mode

Raise the ChipSelect menu to --z-popover so it layers above modal surfaces
(--z-modal) instead of opening behind them. Add a fullWidth prop that stretches
the trigger and right-aligns the chevron for form-field use, and apply it to the
workflow MCP-server pickers.

* fix(emcn): ChipSelect uses the emcn flat chevron, not lucide's square one

The lucide ChevronDown is square; rendering it at the chip's 9x7 footprint
stretched it. Switch to the custom emcn ChevronDown (built for that wide aspect),
matching the integrations filter and ChipDropdown.

* fix(emcn): ChipSelect trigger hugs its content (w-fit)

In a stacked form layout the trigger was stretched by align-items: stretch,
leaving an empty gap to the right of the value. Add w-fit so the chip sizes to
its content (a compact pill) everywhere; fullWidth form selects are unaffected.

* fix(emcn): ChipSelect uses a square lucide chevron

Revert to lucide's ChevronDown sized square (size-[14px]) so it renders crisp,
matching the standard select chevron used by Combobox.

* renamed tasks to chats

* rename and file change

* Wspace resource refs

* improvement(billing): wire up billing, org, teammates tabs + remove deprecated subscription tab (#4887)

* improvement(billing): wire up billing, org, teammates tabs + remove depr subscription tab

* pass exec timeout to tool routes

* reuse helper

* address comments

* address disable comment

* Vfs updates

* chore(db): remove migration 0224 to regenerate on top of staging

Co-authored-by: Cursor <cursoragent@cursor.com>

* VFS updates and linter

* fix type errors and regen migration?

* File block v5

* Remove docs

* Fix

* Doccer image

* Nuke the dag

* chore(db): drop branch migration 0226 ahead of staging merge; will regenerate

* chore(db): regenerate migration 0226 after staging merge

* Fix lint

* externalize before compaction in fallback'

* Run workflow improvements

* Loosen run options validation

* Lint

* fix save/discard chips to be consistent

* fix(ui): remove smodal tabs in favor of chip modal tabs

* update byok manager component

* fix(ui): skip auto-scrolling on mouse highlight of workspace

* Add creds to wspace context

* Wscontext

* fix(platform): restore settings redirects, forgot-password Enter submit, and tag tooltip visibility

- Re-add SETTINGS_REDIRECTS so /settings/integrations and /settings/skills
  deep links redirect to their top-level routes instead of rendering an
  empty settings panel (accidentally removed in 86da193cc3 one minute
  after cca5054cf6 added it)
- Add opt-in onSubmit to ChipModalField input/email variants and wire it
  in the forgot-password modal so Enter submits again (lost in the
  ChipModal conversion)
- Knowledge tag tooltip: drop the max-h/overflow-y-auto clamp that the
  pointer-events-none floating tooltip made unreachable; truncate each
  tag row instead so all tags stay visible with bounded height

* feat(usage-limits): org member specific limits (#4893)

* feat(usage-limits): org member specific limits

* revert feature flags

* fix type issues

* address comments

* address comments and address other billing gapes'

* fix

* improvements

* fix remaining credits display

* fix

* pass ws id to route validations

* chore(db): remove migration 0226_third_spot before staging merge

* chore(db): regenerate migration as 0227 after staging merge

* Block

* feat(telemetry): add posthog + audit coverage for new platform actions

Audit log (compliance/permission-relevant only):
- org_seat.provisioned — seat auto-purchased when an invite acceptance
  grows the org (actor = accepting user, includes seat delta)
- org_plan.converted — Pro→Team conversion triggered by invite acceptance
- org_seat.drift_reconciled — hourly cron healed a drifted seat count
- credential_member.added/removed/role_changed — credential sharing
  surface was previously fully unaudited
- table.created — parity with existing table.updated/deleted
- skill updates now record skill.updated instead of mislabeled
  skill.created

PostHog:
- seats_provisioned, credential_shared/unshared,
  environment_updated/deleted (key counts only, never names/values)
- table_import_started/completed — background CSV imports previously had
  zero failure observability
- table_exported, file_downloaded, skill_updated
- credential_connected now fires for OAuth completions (draft-hooks),
  credential_deleted for OAuth disconnects; previously only manual
  credentials were tracked
- table_workflow_run gains deployment_mode (live/deployed/mixed)

Also: logger.warn on all new credential-admin 403 denials (members +
environment routes); invite-created audit enriched with
enforcedFixedSeats/plan.

Deliberately excluded as noise: per-hour dead-letter audit rows (would
re-record the same stuck event every cron run) and a duplicate
system-actor org_plan.converted in the Stripe outbox handler.

* feat(home): fill textarea on suggested prompt click instead of sending

Clicking a prompt action in the Suggested Actions panel now populates
the Mothership user-input textarea (via applyAutoMentions) and focuses
it with the caret at end, rather than immediately submitting. The user
can review, edit, and send manually.

* fix(templates): name owning integration in featured block template prompts

Four featured prompts omitted their owning integration name, making them
unbranded and disconnected from their title. Each prompt now explicitly
names GitHub or Google Sheets so mentionifyIntegrations renders the chip
and the copy reads as a self-contained agent-building instruction.

* fix(templates): rewrite fragment prompts so each names its integration and reads as a complete instruction

Featured and non-featured block template prompts that either omitted
the owning integration's canonical name or were phrased as marketing
fragments rather than natural user instructions have been rewritten.
Each updated prompt now starts with an imperative verb ("Build a
workflow that…"), names the owning integration explicitly so the
@-mention chip renders correctly, and aligns with the entry's title.

Files changed: salesforce, hubspot (×2), github, slack, airtable,
firecrawl, iam.

* fix(templates): name integration in remaining block template prompts

- google_docs.ts: replace 'Google Doc' with 'Google Docs document' in 4 prompts; update title 'Meeting notes to Google Doc' to 'Meeting notes to Google Docs'
- google_sheets.ts: replace 'Google Sheet' with 'Google Sheets spreadsheet/Google Sheets' in 3 non-featured prompts
- slack.ts: replace 'Google Doc' with 'Google Docs document' in 'Daily standup summary'
- stripe.ts: replace 'Google Sheet'/'Slacks' with 'Google Sheets'/'Slack' in 'Weekly metrics report'
- reddit.ts: add 'Reddit' to 3 prompts that only referenced subreddits
- notion.ts: rewrite featured prompt to start with a verb and name Notion
- jira.ts: rewrite featured marketing-fragment prompt to start with a verb
- linear.ts: rewrite featured marketing-fragment prompt to start with a verb
- gmail.ts: rewrite featured marketing-fragment prompt to start with a verb and name Gmail

* fix(icons): convert monochrome dark brand icons to currentColor for dark mode

LinkupIcon, InfisicalIcon, IntercomIcon, LumaIcon, GranolaIcon, OnePasswordIcon,
and RailwayIcon were hardcoded to black/near-black fills/strokes, making them
invisible in bare DARK mode. Convert all to currentColor so they follow the
theme-aware text color.

Add iconColor: '#286efa' to IntercomBlock (Intercom brand blue is a confident
mid-tone, safe on both themes).

Two-tone icons (StagehandIcon, AgentPhoneIcon, QuiverIcon) are left unchanged
because their white details are structural — converting to single-tone would
destroy logo legibility.

* removed color, user input, sidebar, suggested actions, chips/emcn

* removed color migration

* Table bump

* improve(blocks): audit block catalog metadata for accuracy and fill gaps

- Fix template prompts that claimed capabilities blocks don't have
  (fabricated triggers, non-existent tools) across ~98 integrations
- Normalize integration tags to family conventions and valid union values
- Align alsoIntegrations and modules with template prompt content
- Add BlockMeta for circleback, imap, and rss trigger integrations
- Add templates to clickhouse and greptile metas
- Remove duplicate PageSpeed deploy-gate template

* chore(telemetry): drop org_seat.drift_reconciled audit

System self-heal bookkeeping doesn't belong in the user-facing audit
trail — membership and seat-purchase changes are already audited, and
the cron's logger output covers ops visibility.

* chore(db): consolidate branch migrations into single 0227

* fix(emcn): make ChipModal scroll internally when content exceeds viewport

The Modal→ChipModal migration dropped the old ModalBody scroll container:
ChipModal renders ModalContent bare (no overflow-hidden) and its wrappers
had no min-h-0 chain, so tall modals (e.g. New data drain with an S3
destination) overflowed max-h-[84vh] off-screen with no way to scroll.

Complete the flex min-h-0 chain through ChipModal's frame and give
ChipModalBody flex-1 min-h-0 overflow-y-auto — header/footer stay pinned,
body scrolls only when constrained. Short modals are unaffected (max-h
caps, it doesn't stretch), and dropdowns inside the body are Radix-portaled
so the new scroll container cannot clip them. Five modals that had locally
patched this with max-h-[Nvh] overrides keep working unchanged.

* fix(emcn): don't close modal when dismissing a dropdown via outside click

Radix dispatches pointer-down-outside to every open dismissable layer at
once, so clicking outside an open dropdown/select inside a modal closed
both the dropdown and the modal in one jarring step. ModalContent now
prevents its own dismissal while a portaled popper layer is open — the
first outside click closes just the popper, the next one closes the
modal.

* docs(skills): de-duplicate and correct agent docs; canonical styling tokens; mirror sim-sandbox rule

* docs(skills): broaden boundary-raw-fetch scope note, sync cursor rule mirrors

* fix(emcn): harden modal popper guard and exempt caret-anchored dropdowns from body scroll

Adversarial review follow-ups to the ChipModal scroll + dismissal fixes:

- Popper guard now requires data-state="open" inside the popper wrapper,
  so a dropdown that is merely animating closed no longer swallows the
  next outside click on the modal (DropdownMenu has an exit animation
  that keeps its wrapper mounted briefly)
- Port the same guard to SModalContent for consistency
- custom-tool-modal: opt its body out of the chrome scroll container
  (flex-none + overflow-visible); the caret-anchored EnvVar/Tag
  autocomplete dropdowns are absolute-positioned inside the body and
  must spill past its bounds rather than clip against a scroll boundary

* refactor(billing): drop unnecessary useCallback wrappers from event handlers

* fix(data-drains): complete chip migration of destination forms and fill missing placeholders

Finishes the in-flight FormField → ChipModalField conversion for all
destination form specs and adds the placeholders that several inputs
never had (S3 bucket/region/access keys, Azure account key, Datadog API
key, webhook signing secret/bearer token) — the cause of the New Data
Drain modal showing placeholder-less inputs inconsistently.

* fix(emcn): broaden modal popper guard to onInteractOutside

Covers the focusOutside dismissal path too: when a popper's focus scope
unwinds on close, the transient focus shift could still dismiss the
modal (and simultaneous body pointer-events lock teardown could freeze
the page). Same data-state="open" scoping as the pointer guard.

* fix

* fix(emcn): harden modal outside interactions

* Remove migration 0227 to regenerate after merge

Drop the 0227_org_member_usage_limit migration, its snapshot, and journal
entry so it can be regenerated cleanly after merging improvement/platform.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Regenerate migration as 0228 after merging improvement/platform

drizzle-kit generate re-emits the org_member_usage_limit table, document
uploaded_by, and table_run_dispatches triggered_by_user_id changes on top of
improvement/platform's 0227 snapshot. Also collapse the admin.tsx emcn import
left multi-line after merge conflict resolution.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix pre-existing type errors surfaced by post-merge type-check

These were present on dev before the merge; the post-merge type-check made
them visible.

- workspace-vfs.ts: materializeEnvironment now emits full oauth integration
  objects (id, providerId, displayName, role) so the summary matches
  WorkspaceMdData['oauthIntegrations'] (consumer reads credentialId from id)
- workflows/utils.ts: narrow resolved workflow rows to non-null workspaceId
  (the inArray filter already guarantees it) so the resolved result type holds

Co-authored-by: Cursor <cursoragent@cursor.com>

* Slack trigger

* fix tool mark complete bugs

* change thinking icon to just be blimp

* fix tests

* MCP fixes

* Fix files

* refactor use chat

* multiple fixes

* Fix deploys

* fix billing, tracing, retry lifecycle issues

* remove stale generated migration

Drop migration 0228 and its metadata so it can be regenerated after merging staging.

Co-authored-by: Cursor <cursoragent@cursor.com>

* merge staging in

* fix sockets err classification

* sockets invite flow fix

* remove stale generated migration 0229 before staging merge

Co-authored-by: Cursor <cursoragent@cursor.com>

* regen migration

* Update bun lock

* Fix schema issues

* Fix lint

* Fix build

---------

Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com>
Co-authored-by: Theodore Li <theo@sim.ai>
Co-authored-by: Waleed <walif6@gmail.com>
Co-authored-by: Emir Karabeg <emirkarabeg@berkeley.edu>
Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
Co-authored-by: andres <k62hc5kjst@privaterelay.appleid.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: andresdjasso <andresdjasso@users.noreply.github.com>
Co-authored-by: waleed <waleed@simstudio.ai>
2026-06-09 13:39:51 -07:00
WaleedandTheodore Li 540835a7a5 feat(integrations): add AWS AppConfig integration with tools, block, and docs (#4928)
* feat(integrations): add AWS AppConfig integration with tools, block, and docs

* fix(integrations): preserve latestVersionNumber 0 and tighten locationUri type in AppConfig

* fix(integrations): use valid IntegrationTag values in AppConfig BlockMeta

* feat(appconfig): add list_hosted_configuration_versions operation

* fix(appconfig): stringify configurationVersion in block params

* feat(appconfig): add full CRUD for applications, environments, and configuration profiles

Adds get/update/delete for applications, environments, and configuration
profiles, plus delete_hosted_configuration_version — 10 tools rounding out
the integration to management-grade CRUD completeness.

---------

Co-authored-by: Theodore Li <theo@sim.ai>
2026-06-09 13:17:40 -07:00
Waleed a72e35e2f4 feat(sendblue): add Sendblue iMessage/SMS integration with tools and triggers (#4917)
* feat(sendblue): add Sendblue iMessage/SMS integration with tools and triggers

* fix(sendblue): address review — status-aware webhook dedup, shared routing map, uniform output casing, api-key authType

* fix(docs-gen): skip nested array `items` descriptor in tool input tables

* chore(sendblue): use SendblueSendStyle type, trim URL identifiers

* fix(sendblue): keep is_outbound routing map local to the webhook handler

Avoids a webhook-providers -> triggers cross-subgraph import (single source
of truth in the handler, its only runtime consumer).

* fix(sendblue): remove invalid `tags` from block config (belongs on BlockMeta)
2026-06-08 19:41:09 -07:00
25771358d0 feat(enrichment): add ZeroBounce, NeverBounce, and MillionVerifier email verification (#4854)
* feat(enrichment): add ZeroBounce, NeverBounce, and MillionVerifier email verification

* docs gen

* mdx

* fix(zerobounce): handle 200-status API errors and guard JSON parsing

* fix(byok): render new email-verify providers + raise hosted rate limits

- Add zerobounce/neverbounce/millionverifier to the Enrichment
  PROVIDER_SECTIONS so they render in the workspace BYOK settings UI
  (they were in PROVIDERS + the API allowlist but no section listed them).
- Raise ZeroBounce and MillionVerifier hosted per-workspace rate limits
  from 60 to 1200 req/min, sized against documented upstream ceilings
  (ZeroBounce 80k/10s; MillionVerifier 160/sec). NeverBounce stays at 60
  pending its account-configured throttle.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Theodore Li <theo@sim.ai>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 17:45:18 -04:00
0075ab9cf6 improvement(platform): remove tour, simplify sidebar/header, drop loading skeletons (#4354)
* improvement(platform): workspace UI/UX overhaul + integrations catalog

Rework the workspace around the AI-workspace model: a Mothership home, a
top-level Skills route, connected-credential and integration-detail pages,
and a polished sidebar/settings surface. Replace the notifications store
with a unified toast system (provider-level dismiss/pause, countdown ring).

Integrations & catalog:
- Add a BlockMeta layer (tags + catalog templates) scoped to catalog-visible
  integrations; every catalog integration carries >=7 grounded templates.
- Rework the taxonomy: each block declares category tools|blocks|triggers.
  3rd-party services are 'tools'; first-party primitives (postgres, mysql,
  knowledge, file, search, stt/tts, image/video generators, thinking, etc.)
  are 'blocks'. Versioned blocks follow the upgrade paradigm (old hidden,
  latest in toolbar/docs).
- Generate integrations.json + tool docs canonically from block configs.

Architecture & cleanup:
- Consolidate block data extraction behind a single latest-version strategy
  (getCanonicalBlocksByCategory; version-consistent getBlockMeta).
- Unify version-suffix handling in @sim/utils/string (stripVersionSuffix /
  isVersionedType, with tests); registry, generate-docs, tools/utils, and
  integrations all route through it.
- Repair latent broken barrels, remove dead code, fix BlockMeta-related type
  errors and 5 broken docs links.

Behavior-preserving for block execution and the toolbar's tool/block listing.

* refactor(platform): remove forms, templates, and creators features

Remove three standalone features and their supporting code:
- Forms: form-deployment pages, API routes, execution path, and docs.
- Templates: the template gallery (landing + workspace) and template APIs.
- Creators: creator-profile routes and contracts.

Add a super-user permissions module (lib/permissions/super-user) and an
organizations API contract; update the audit/db/testing packages, billing,
and the session/theme providers accordingly.

* test(workflows): update archiveWorkflow update count after forms removal

The forms feature was removed, dropping the form-table update from
archiveWorkflow. Update the stale assertion from 8 to 7 tx.update calls.

* upgrade

* improvement(knowledge): polish tag filter dropdowns (#4816)

* improvement(logs): object storage backed tracespans (#4787)

* improvement(logs): obj storage backed tracespans

* fix storage write context

* fix tests

* address comments

* address comments

* chore(db): remove migration 0219 to regenerate after staging merge

Drops the 0219_robust_shard SQL, its snapshot, and the journal entry so the
trace-spans/cost schema migration can be regenerated on top of the latest
staging migration chain (avoids a number collision with staging's migrations).

Co-authored-by: Cursor <cursoragent@cursor.com>

* improvement(billing): accurate per-member usage via shared ledger helper

Per-member/per-user usage in the org-member routes now adds the usage_log
ledger to the currentPeriodCost baseline (which is no longer incremented),
via a shared getOrgMemberLedgerByUser helper to avoid repeating the
subscription→period→ledger lookup across the admin and member-facing routes.

Co-authored-by: Cursor <cursoragent@cursor.com>

* regen migrations

* update migration

* address comments

* more code cleanup

* incorrect type cast

---------

Co-authored-by: Cursor <cursoragent@cursor.com>

* improvement(providers): harden OpenAI-compatible providers + add tests (#4796)

* improvement(providers): harden OpenAI-compatible providers + add tests

* fix(vllm): let tool-loop errors propagate instead of returning silent partial success

* fix(litellm): force tool_choice 'none' on final structured-output call

The deferred final call used tool_choice 'auto', so the model could emit
another tool_calls round instead of the structured answer, leaving content
stale. Use 'none' (matching vLLM/Fireworks) on both the streaming and
non-streaming final calls so the model must return the structured response.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(providers/ollama): drop tools from post-tool streaming call

Ollama ignores tool_choice (not in its supported fields), so vLLM/Fireworks'
tool_choice:'none' guard is a no-op here. Omit tools from the final streaming
payload instead so the summarization turn can't emit dropped tool calls.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(litellm): spread payload into deferred final call so reasoning_effort carries over

The non-streaming deferred finalPayload hand-picked fields and dropped
reasoning_effort (and any future payload field), diverging from the streaming
path which spreads ...payload. Spread payload here too for consistency.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(providers/ollama): restore enrichment TSDoc block

Keeps parity with sibling Chat Completions providers (cerebras/mistral/xai).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(fireworks): restore TSDoc on utils helpers

Restore the TSDoc blocks on supportsNativeStructuredOutputs,
createReadableStreamFromOpenAIStream, and checkForForcedToolUsage —
TSDoc is the codebase documentation standard and should not have been
stripped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(litellm): remove inline rationale comments (codebase uses TSDoc)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(providers/ollama): drop orphaned enrichment TSDoc

The block documented a function that now lives in trace-enrichment.ts, so it
documents nothing in this file.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* chore(copilot): deprecate mcp server (#4797)

* chore(copilot): deprecate mcp

* update error codes

* deprecate copilot api v1 route

* feat(integrations): hosted API keys for Findymail, Prospeo, and Wiza (#4777)

* feat(integrations): hosted API keys for Findymail, Prospeo, and Wiza

Add hosted-key support across all credit-consuming Findymail, Prospeo, and Wiza operations so Sim provides the key when a workspace has not brought its own. Register the three BYOK providers, consolidate Wiza's two-step reveal into a single polling wiza_individual_reveal op, and hide the API key field on hosted Sim for hosted operations.

* fix(integrations): harden Wiza reveal polling, soften enrichment getCost guards

Address Greptile + Cursor Bugbot review on #4777: return explicit failures from the Wiza individual_reveal poller instead of throwing (thrown errors were swallowed into a false queued success), short-circuit when the initial reveal is already terminal, tolerate transient 5xx/429 during polling, and return 0 (not throw) from Findymail getCost when the contacts/employees array is absent.

* chore(integrations): biome formatting after wiza merge resolution

* fix(wiza): type isTerminalReveal param structurally for next build typecheck

* feat(enrichments): add Findymail, Prospeo, Wiza to work-email waterfall

* feat(enrichments): add Wiza + Prospeo phone reveal to phone-number waterfall

* feat(enrichments): opportunistic identifiers + LinkedIn URL input across work-email & phone cascades

* fix(tables): reduce column header chevron size and fix sidebar shadow bleed (#4800)

* feat(slack): add install + privacy section to integration landing page (#4799)

* feat(slack): add install + privacy section to integration landing page

Adds a hand-authored, slug-keyed landing-content module (separate from the generated integrations.json so it survives regeneration) and renders an install walkthrough + privacy-policy link on integration pages when present. Also refreshes generated docs (data-enrichment entry, icon mappings, tool mdx).

* fix(landing): render privacy section independently, align CTA analytics label

* docs(landing): clarify the Slack install button is behind sign-in

* refactor(landing): bake integration landing content into generated json via docs-gen

Moves landing content (install walkthrough + privacy) out of a render-time augment and into the generation pipeline: generate-docs reads the pure-data content map and writes landingContent into integrations.json, so the page reads a single source (integration.landingContent). Canonical types live in integrations/data/types.ts.

* improvement(enrichments): align enrichments sidebar with design system (#4801)

* improvement(enrichments): align enrichments sidebar with design system

* fix(enrichments): consistent close button pattern and fix url link hover

* fix(misc): upgrade path change for new better-auth version, billing issue for workflow block agent usage (#4803)

* fix(misc): upgrade path change for new better-auth version, double-billing for workflow block agent usage

* fail loudly if stripe sub id missing

* fix(copilot): seq migration (#4804)

* chore(db): drop redundant idx_webhook_on_workflow_id_block_id index (#4809)

Removed because (workflow_id, block_id) is a left-prefix of idx_webhook_on_workflow_id_block_id_updated_at_desc, which fully covers it. The dropped index was non-unique and enforced no constraint.

* perf(copilot): read chat transcripts from copilot_messages (R+1 cutover) (#4808)

* perf(copilot): read chat transcripts from copilot_messages, not JSONB

Flip user-facing chat reads from the legacy copilot_chats.messages JSONB
array (5.7GB, 99% TOAST) to the normalized copilot_messages table via a
new loadCopilotChatMessages helper ordered by seq NULLS LAST, created_at,
id — the verified canonical order. Both chat-detail getters
(getAccessibleCopilotChat, getAccessibleCopilotChatWithMessages) now drop
the messages column from their metadata select (no more whole-array
detoast on every load) and assemble the transcript from the table after
authorization. This cascades to the copilot + mothership GET endpoints
and to resolveOrCreateChat's conversationHistory (the LLM payload).

The normalize/effective-transcript pipeline is source-agnostic
(copilot_messages.content == a JSONB array element), so transcripts are
byte-identical. Dual-write and the JSONB column stay in place as the
internal-logic source and fallback; removing JSONB writes is a later step.

Prod integrity verified before cutover: 0 messages missing, 0 NULL-seq,
0 dup keys/seq, 0 orphans, order-parity vs JSONB = 0 mismatches.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(copilot): cover auth-deny on a found row skips the messages query

Address PR review: exercise the `if (!authorized) return null` contract —
when the chat row exists but authorization fails, the getter returns null
and never issues the copilot_messages read.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* fix(tables): right-align run/stop in embedded toolbar; workflow cells format like normal cells (#4806)

* fix(tables): right-align run/stop in the embedded table toolbar

Add a right-aligned `trailing` slot to ResourceOptionsBar and move the embedded
mothership table's run/stop control into it, so Filter + Sort stay left-aligned
and run/stop sits opposite on the right. No-op for the search-bearing consumers
(logs, resource list), which don't pass `trailing`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(tables): workflow-output cells format values like normal cells

Workflow-output columns short-circuited in resolveCellRender and rendered their
value as plain text, so a sim-resource URL / external URL / JSON / date produced
by a workflow never got the chip, favicon link, or typed formatting a normal
cell gets. Factor value formatting into a shared `resolveValueKind` helper used
by both the workflow-value branch and the plain-cell branch; the workflow branch
keeps the typewriter reveal for plain streaming text via a `typewriter` flag.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(tables): detect resource/URL links on workflow output regardless of column type

Workflow output columns default to `json` (columnTypeForLeaf), so routing their
values through the type-based formatter (a) gated chip/URL promotion behind
`column.type === 'string'` — a URL produced by a json-typed output never became
a chip — and (b) JSON.stringify'd plain string values, adding quotes and losing
the typewriter reveal. Detect links (sim-resource chip / favicon URL) on the
value string directly for workflow outputs, falling back to the plain `value`
kind; plain cells keep the type-based formatting. Addresses Greptile P2 on #4806.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(icons): repair broken integration icon rendering (#4810)

* fix(icons): repair broken integration icon rendering

Two distinct bugs left integration icons broken on the /integrations page
(visible at 32-40px, hidden at the toolbar's 16px):

1. Corrupted SVG paths (Notion, Greptile, Granola, Calendly, Grafana, Bedrock):
   over-minified data dropped elliptical-arc flag digits (e.g. `A1 1 0 5.9 7`
   instead of `A1 1 0 0 0 5.9 7`); Granola's cubic stream was truncated. Browsers
   abort path parsing at the first invalid arc flag, so each rendered as a fragment
   or blank. Replaced with correct path data from canonical sources, preserving each
   icon's existing fill/gradient and bgColor.

2. Invisible glyph (Bright Data): its icon uses fill='currentColor' but bgColor was
   '#FFFFFF', and every surface forces text-white on the glyph - white-on-white.
   Changed bgColor to Bright Data's brand blue (#3d7ffc) so the white glyph reads,
   matching the white-glyph-on-brand-chip convention.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(icons): restore Calendly dual-tone brand colors

Addresses review feedback: the previous fix replaced the broken Calendly icon
with a monochrome #006BFF path, dropping the cyan #0ae8f0 accent from the
original dual-tone mark. Restored the two-tone logo (blue + cyan) using clean,
valid path data, cropped to a tight square viewBox so it fills the chip.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* improvement(icons): enlarge icons, fix Zoom contrast and Quiver chip

- Zoom: glyph was blue-on-blue (#0B5CFF on #2D8CFF chip); switched to
  currentColor so it renders as a white glyph on the blue chip.
- Quiver: chip bgColor #000000 -> #FFFFFF to match the icon's near-white box,
  and enlarged the mark slightly (viewBox crop).
- Enlarged (tightened viewBox, verified no clipping): RevenueCat, Prospeo,
  Granola, Firecrawl, Enrich.so, and the AWS icons (RDS, DynamoDB, SQS,
  CloudFormation, Athena, CloudWatch, SES, Bedrock, S3).
- ZoomInfo left unchanged: it is a full red rounded-square logo that already
  fills its frame, so a crop would clip it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(icons): use Bright Data wordmark on white chip; repair Circleback

- Bright Data: replaced the flame glyph with the official two-tone 'bright data'
  wordmark (provided asset), centered in a symmetric viewBox. Reverted the chip
  bgColor from #3d7ffc to #FFFFFF since the blue wordmark is invisible on a blue
  chip (the wordmark is designed for a light background).
- Circleback: a minifier had rounded the pattern's image scale to scale(0),
  collapsing the embedded logo to zero size (invisible). Restored the correct
  scale (1/280 = 0.00357142857) so the C. mark renders.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(docs): sync Quiver block color card to white chip

Reflects the Quiver bgColor change (#000000 -> #FFFFFF) in the docs block info card.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* improvement(icons): enlarge AWS/Cloudflare/Dagster icons, fully white Zoom

- Enlarged (tighter viewBox, render-verified, no clipping): Cloudflare, Dagster,
  and the red AWS icons AWS IAM, Identity Center, Secrets Manager, SES, STS.
  Identity Center was anomalously small (filled ~32% of its frame); the group is
  now sized consistently (~80% fill).
- Zoom: the camera lens triangle was still #0B5CFF (blue-on-blue); switched it to
  currentColor so the whole camera renders white on the blue chip.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(wiza): consolidate individual reveal into a single operation

Merges the separate Start/Get Individual Reveal operations into one Individual
Reveal operation in the Wiza docs and integrations data (operationCount 5 -> 4).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* improvement(icons): size remaining AWS icons to match the set (~80% fill)

Bring RDS, DynamoDB, SQS, CloudFormation, Athena, CloudWatch and S3 up to the
same ~80% fill as the AWS IAM/Identity Center/Secrets Manager/SES/STS group, so
all AWS icons are visually consistent. Bedrock left as-is (already ~92% fill).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(icons): use Bright Data flame mark, enlarge ZoomInfo

- Bright Data: the full 'bright data' wordmark was illegible at chip size.
  Replaced with just the flame-'i' brand mark (blue #4280f6 on the white chip),
  centered.
- ZoomInfo: cropped the viewBox toward the white 'Zi' so it's larger; the red
  rounded-square background still fills the chip.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* improvement(icons): enlarge CrowdStrike icon

The falcon mark sat small in its chip because the icon used a wide 768x500
viewBox (letterboxed in the square chip). Switched to a square viewBox centered
on the mark so it fills ~80%, consistent with the other icons.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* fix(tables): serialize schema mutations to prevent parallel column clobber (#4812)

* Make workflow description nullable

* fix(tables): serialize schema mutations to prevent parallel column clobber

* fix(tables): load workflow outside schema lock; use DbOrTx for getTableById

* fix(tables): scale idle timeout in updateColumnType to avoid aborting large type changes

* fix(tables): skip stale remap types when workflowId changes concurrently

* fix(tables): scale idle timeout in updateColumnConstraints for large tables

* fix(wait): resume live/draft async waits and preserve cell context on chained waits (#4814)

* Make workflow description nullable

* fix(wait): resume live/draft async waits and preserve cell context on chained waits

* improvement(knowledge): polish tag filter dropdowns

* improvement(knowledge): soften filter section labels

* improvement(knowledge): soften list filter labels

* fix(security): harden SSO domain registration, webhook path isolation, and CSV export (#4813)

* fix(security): harden KB file access, SSO domain registration, webhook path isolation, env secrets, and CSV export

* fix(sso): scope domain conflict query with indexed lower(domain) filter

Address PR review: avoid a full-table scan on every SSO provider
registration by filtering candidate rows in SQL with
lower(domain) = <normalized>, keeping the in-memory ownership check.
Also tighten the normalizeSSODomain TSDoc.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore: condense env route security comments

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* icons update

* chore(security): tighten inline comments in CSV export and KB file authorization

Condense verbose comment blocks to concise TSDoc/single-line form; no behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(security): validate internal serve origin in KB file authorization

Replace the bypassable isInternalFileUrl substring check in resolveInternalKbKey
with an origin allow-list (base URL, internal API base URL, TRUSTED_ORIGINS).
A crafted external host whose path is /api/files/serve/<victim-key> no longer
resolves to the victim key. Relative same-origin URLs are unaffected.

* style(sso): use idiomatic sql lower() comparison for domain conflict query

Match the repo's prevailing `sql`lower(col) = value`` idiom for the
case-insensitive SSO domain conflict lookup.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(security): align workspace env admin gate with hasWorkspaceAdminAccess

Use the same admin check the secrets UI uses (owner, admin permission, or
org-admin) so owners and org-admins are not wrongly denied their own decrypted
workspace secrets, while read-only members remain restricted to names only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(sso): rely on lower(domain) match for conflict detection, drop dead in-memory recheck

Address PR review: the SQL `lower(domain) = <normalized>` predicate already
excludes rows that the in-memory `normalizeSSODomain(...) === domain` recheck
claimed to catch, making that recheck dead/misleading code. Match on the
canonical lower-cased domain and filter purely by ownership. Malformed legacy
values (wildcards, schemes, ports) never match an email domain at sign-in, so
excluding them is not a gap. Test DB mock now applies the lower() predicate so
the casing-variant case is genuinely exercised.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(security): scope webhook deploy path conflict to active webhooks

findConflictingWebhookPathOwner omitted the isActive filter that the
runtime dispatcher (findAllWebhooksForPath) applies, so an inactive but
non-archived webhook from another workflow (e.g. after undeploy or
failure auto-disable) would permanently block any new deployment on that
path even though it never receives deliveries. Align the guard with the
runtime isActive + archivedAt filter; the earliest-owner runtime check
remains the authoritative cross-tenant protection. Also trims verbose
TSDoc on the webhook path-isolation helpers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(security): exclude archived workflows from webhook deploy path conflict

findConflictingWebhookPathOwner now joins workflow and filters
isNull(workflow.archivedAt), matching the runtime dispatcher
(findAllWebhooksForPath). A webhook on an archived workflow can never
receive deliveries at runtime, so it must not block legitimate path reuse
with a 409.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(security): anchor KB file ownership to earliest document in any state

A KB file's owner is now the earliest document referencing its key regardless of
state (active/archived/deleted/excluded); access is granted only when that owning
document is still active. Closes the residual where an attacker could plant an
active document to claim a file whose original document was archived or deleted.

* updated greptile icon

* revert(security): drop KB file authorization changes

Reverts the knowledge-base file-access work (origin-pinning / owner-pinning /
origin allow-list in verifyKBFileAccess) and its test. The other hardening fixes
(SSO domain registration, webhook path isolation, workspace env secrets, CSV
export) are unchanged. apps/sim/app/api/files/authorization.ts is restored to its
origin/staging baseline.

* fix(sso): treat caller's own user-scoped provider as owned during conflict check

Self-hosters often register SSO user-scoped via the CLI script (no
SSO_ORGANIZATION_ID). If they later enable organizations and reconfigure the
same domain org-scoped through the UI, the conflict check previously treated
their own user-scoped row as another tenant's and returned a misleading 409.
Recognize the caller's own user-scoped provider as owned so that migration is
allowed, while still blocking another user's or another org's domain.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* revert(security): remove workspace-env admin gate

Defer to a credential-based access model (separate change). Restores
GET /api/workspaces/[id]/environment to main behavior and removes the test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(security): consolidate webhook path-collision check into one helper

Extract findConflictingWebhookPathOwner to lib/webhooks/utils.server.ts as
the single source of truth for cross-tenant path-collision detection, used by
both webhook creation paths (deploy sync and the manual /api/webhooks route).

This also repairs two latent issues in the manual route's previous inline
check, which queried with limit(1) and only webhook.archivedAt:
- limit(1) inspected one arbitrary row, so a same-workflow row could mask a
  foreign collision (false negative). The shared helper scans all matching
  rows.
- It omitted isActive/workflow.archivedAt, so inactive or archived-workflow
  webhooks (which never receive deliveries) permanently blocked path reuse.
  The helper mirrors the runtime dispatcher's filter.

Same-workflow webhook reuse for upsert is now a separate, explicit lookup.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* fix(security): block private/reserved IPs for hosted 1Password Connect SSRF (#4818)

* fix(security): block private/reserved IPs for hosted 1Password Connect SSRF

* test(security): use real isPrivateOrReservedIP and cover IPv6 edge cases

* improvement(integrations): validate and expand devin, cursor, and greptile (#4820)

* improvement(integrations): validate and expand devin, cursor, and greptile

- devin: fix missing org_id path segment on all session endpoints, add 7 session sub-resource tools (list messages/attachments, get/append/replace tags, archive, terminate), pagination, and is_archived output
- cursor: add get_api_key_info, list_models, list_repositories tools
- greptile: align block and docs
- normalize array outputs to default [] and tighten types

* refactor(cursor): simplify list_repositories v2 array normalization

Collapse the redundant `?? []` + `Array.isArray` double-guard into a
single Array.isArray check, per PR review feedback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(devin): scope session-tag mapping to tag ops and normalize array tag inputs

- Only map sessionTags into the tools tags param for append/replace operations,
  preventing stale sessionTags state from clobbering create_session tags
- Fall back to a wired tags value when sessionTags is empty for tag operations
- Normalize tag inputs (string or wired string[]) via normalizeTags so array
  values from other blocks no longer throw on .split

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cursor): restore base64 file data in legacy download_artifact metadata

The legacy CursorBlock exposes only content + metadata (no v2 file
output), so metadata.data was the only way legacy-block workflows could
access downloaded artifact bytes. Restore the base64 data field and
document it in the outputs/type instead of dropping it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(devin): coerce terminateArchive to archive flag for boolean-wired input

* docs(integrations): regenerate tool docs for new devin and cursor operations

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* fix(search-replace): don't auto-navigate when content edits invalidate the active match (#4819)

* fix(search-replace): don't auto-navigate when content edits invalidate the active match

* fix(search-replace): clear afterReplaceIndexRef on apply failure and zero matches

* fix(search-replace): remove duplicate setActiveSearchTarget(null) on close

* fix(search-replace): move afterReplaceIndexRef write inside handleApply past the guard

* fix(search-replace): auto-navigate when hydration resolves with no prior active match

* chore(search-replace): remove inline comments

* fix(search-replace): revert !activeMatchId guard that caused immediate re-navigation after deselect

* improvement(enrichments): limit company-info to fields both providers return (#4817)

Hunter's company dataset returns null industry/foundedYear for many large companies (verified against the live API for Microsoft, Amazon, Google), so under the first-non-empty-wins cascade those columns appeared inconsistently across rows. Limit company-info outputs to employee count and description — the fields Hunter and PDL both reliably return — so every row is consistent. employeeCount is a string so Hunter's range bucket and PDL's exact count share the column.

* fix(files): don't reject external URLs containing '..' in file parse validation (#4821)

* fix(files): don't reject external URLs containing '..' in file parse validation

The file block's file_fetch operation rejected any external URL whose path
contained '..' (e.g. Slack files-pri slugs with a literal '...') with
'Access denied: path traversal detected'. Traversal checks only apply to
local paths — external http(s) URLs are fetched with SSRF protection
downstream and are never resolved against the filesystem, so they now
short-circuit as valid. Internal /api/files/serve/ URLs keep full traversal
protection.

* test(files): fix external-URL assertion to handle undefined error

* test(files): assert success explicitly in external-URL traversal test

* fix(files): keep traversal protection for https URLs matching internal serve paths

* feat(google-sheets): add row filtering to read with numeric operators (#4822)

* feat(google-sheets): add row filtering to read with numeric operators

Adds client-side row filtering to the Google Sheets read (v2) operation.
Filter the returned rows by a header column using text operators
(contains, not_contains, exact, not_equals, starts_with, ends_with) and
numeric/ordering operators (gt, gte, lt, lte). Filtering lives in a pure,
unit-tested helper (filterSheetRows) and runs over the fetched read range;
an optional `filter` output reports whether the column was found and how
many rows matched.

Also hardens the surrounding tools:
- trim spreadsheetId in write/update/append URL builders (matches read)
- URL-encode the v1 read default range
- expose valueInputOption for the update operation in the block

Backwards compatible: with no filter requested, read output is byte-
identical and the `filter` field is omitted. The filterMatchType union is
widened additively (4 -> 10 values).

* fix(google-sheets): correct filter metadata for missing column and header-only sheets

- matchedRows is now 0 (not totalRows) when the filter column is not found,
  so it no longer contradicts applied=false / columnFound=false
- columnFound now reflects an actual header lookup for empty/header-only
  sheets instead of being hardcoded true
- add tests covering header-only and empty sheets with present/absent columns

* fix(selectors): fetch all pages for paginated dropdown list routes (#4823)

* fix(selectors): fetch all pages for paginated dropdown list routes

Dropdown selectors fetched only the first page of paginated provider
APIs, silently hiding results past page one. Add bounded server-side
draining to the list routes across Microsoft Graph, Google, Notion,
Atlassian, Linear, AWS CloudWatch, and offset/token REST APIs, plus a
shared client-side drain cap in the selector hook. Response shapes,
stored values, and tool execution are unchanged; CloudWatch list tools
still honor a caller-supplied limit. Also fixes the Word file picker
that was searching for .xlsx files.

* fix(selectors): harden JSM and Monday pagination draining

- JSM service-desk/request-type drains advance `start` by the actual row
  count returned (not the fixed page size) and stop on an empty page, so a
  short non-final page can't skip items.
- Monday boards drain now checks `response.ok` per page, surfacing a
  mid-drain HTTP failure instead of treating it as an empty final page and
  returning a partial 200.

* docs(selectors): clarify JSM drain advances start by actual row count

The offset-advancement fix (advance `start` by the rows returned, not the
fixed page size) landed in 7b19788a8; update the TSDoc to match so it no
longer reads as advancing by `limit`.

* fix(selectors): drain fetchPage in direct fetchList callers

Making `fetchList` optional left three direct callers (outside the
useSelectorOptions hook) calling it unguarded, which broke the build's
type check. Route them through a shared `loadAllSelectorOptions` helper
that uses `fetchList` when present and otherwise drains `fetchPage`.
This also prevents a regression: `confluence.spaces` / `knowledge.documents`
now paginate via `fetchPage` only, and these callers (search/replace,
value resolution) would otherwise have silently returned no options.

* chore(selectors): rename MAX_PAGE_PAGES to MAX_NOTION_PAGES for readability

* fix(sso): re-check domain conflict before write and reject IP-address domains (#4825)

* improvement(copilot): make copilot_messages the sole transcript store, remove JSONB dual-write (#4826)

Stop writing/reading the legacy copilot_chats.messages JSONB column now that
reads are cut over to copilot_messages. Make appendCopilotChatMessages the
primary write (throws on failure instead of swallowing), repoint peripheral
readers (workspace VFS, chat cleanup, data drains, fork, superuser import) to
copilot_messages, and persist the assistant turn inside finalizeAssistantTurn's
transaction so it commits atomically with the stream-marker clear. The column
itself is dropped in a follow-up migration after this bakes.

* feat(tables): expand filter operators (not-contains, starts/ends-with, not-in, empty) (#4827)

Add does-not-contain ($ncontains), starts-with ($startsWith), ends-with
($endsWith), not-in-array ($nin, previously executed server-side but unexposed
in the UI), and is-empty/is-not-empty ($empty) filter operators end-to-end —
SQL builder, condition types, query-builder converters/constants, the filter
UI, the Table tools/block descriptions, and docs.

Also fix correctness bugs in the filter builder surfaced by the wider operator
set:
- Same-column AND rules (e.g. age > 18 AND age < 65, or name startsWith 'A'
  AND name endsWith 'Z') silently overwrote each other because the AND group
  was keyed by column name. They now merge into one operator object, which
  also makes Filter -> rules -> Filter round-trip losslessly for multi-operator
  columns.
- $nin values were not split into an array like $in, and textual-match values
  like "123" were numeric-coerced (breaking the ILIKE path).
- A non-boolean $empty operand from the raw API silently inverted the check; it
  now coerces 'true'/'false' strings and otherwise returns a 400.

* improvement(copilot): stop persisting tool-call result outputs in transcripts (#4829)

Opening a Mothership task could take many seconds because a single persisted
assistant message in copilot_messages.content can reach hundreds of MB, almost
entirely inside contentBlocks[].toolCall.result.output (e.g. a get_workflow_logs
or run_workflow result). The DB query is ~2ms; the cost is detoasting that
payload, shipping it to the browser, and parsing it.

These outputs are dead weight on the Sim side: they are never rendered (the
thread shows only tool name/title/status) and never replayed to the model (the
upstream copilot service owns conversation memory). So drop result.output before
it is persisted, keeping result.success/error plus the tool metadata.

- add stripToolResultOutput() in persisted-message.ts
- apply it in messages-store toRow (covers every write path) and in
  loadCopilotChatMessages (existing rows render fast on read)

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* feat(providers): add Together AI, Baseten, and Ollama Cloud model providers (#4830)

* feat(providers): add Together AI, Baseten, and Ollama Cloud model providers

* fix(providers): guard Ollama streaming fast-path with hasActiveTools

Match Together/Baseten/Fireworks: when tools are supplied but all are
filtered out (usageControl 'none'), take the single streaming call instead
of an extra non-streaming round-trip.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(providers): filter non-chat model types from Together model list

* refactor(providers): dedupe Ollama Cloud upstream schema

ollamaCloudUpstreamResponseSchema was byte-for-byte identical to
ollamaUpstreamResponseSchema (both /api/tags endpoints return the same
{ models: [{ name }] } shape). Drop the duplicate and reuse the shared schema.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* fix(knowledge): calendar view sync, deduplicate popover animation classes, type-safe filter cast

* cleanup(knowledge): remove TRIGGER_BORDER_CLASS duplication, inline displayLabel, drop enabledFilterParam alias

---------

Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Waleed <walif6@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Theodore Li <theo@sim.ai>
Co-authored-by: andresdjasso <andresdjasso@users.noreply.github.com>

* feat(blocks): add BlockMeta to Quiver and Linq; fix invalid block config fields; update skills

Block fixes:
- Add QuiverBlockMeta (tags + 3 templates: icon generator, diagram creator, vectorizer)
- Fix QuiverBlock: remove invalid tags field from BlockConfig, IntegrationType.Design →
  IntegrationType.AI (Design doesn't exist in the enum)
- Fix GreptileBlock: remove invalid tags field from BlockConfig,
  IntegrationType.DeveloperTools → IntegrationType.DevOps
- Fix LinqBlock: remove invalid tags field from BlockConfig (tags belong only in BlockMeta)

Skills:
- add-block: add dedicated BlockMeta section with structure, rules, and registration
  pattern; add BlockMeta checklist items
- add-integration: add BlockMeta to block structure template, add rules clarifying
  that tags must NOT appear on BlockConfig and integrationType must be a valid enum
  value; update registry snippet to include blocksMeta; add checklist items

* fix(integrations): fix category dropdown by defining missing LANDING_INTEGRATIONS_DATA_PATH and regenerating integrations.json

The staging merge introduced landing-content.ts but forgot to define
LANDING_INTEGRATIONS_DATA_PATH in generate-docs.ts, causing the script
to crash before writing integrations.json.

The stale JSON had integrationTypes (plural array) from an older script
version, while the Integration type and workspace UI both read
integrationType (singular string) — so ALL_CATEGORY_SECTIONS bucketed
to undefined and the category filters never appeared in the dropdown.

Fixed by adding the missing path constant and re-running the generator.
integrations.json now has 192 entries with the correct integrationType field.

* fix(sidebar): restore resize handle on all pages

commit 3109104582 wrapped the resize handle in {(isCollapsed ||
isOnWorkflowPage) && ...} and added a useEffect that resets sidebar
width to SIDEBAR_WIDTH.MIN whenever the user navigates away from a
workflow page. Together these made the sidebar non-resizable on Tasks,
Tables, Knowledge Base, and every other non-workflow page.

Restore the staging behavior: always render the resize handle and
remove the effect that forced the width reset on page transitions.

* fix(sidebar): match staging onKeyDown and tabIndex on resize handle

The resize handle was still conditionalizing onKeyDown and tabIndex
on isCollapsed, blocking keyboard accessibility of the separator role
when expanded. Staging always attaches both unconditionally.

onKeyDown={isCollapsed ? handleEdgeKeyDown : undefined} → onKeyDown={handleEdgeKeyDown}
tabIndex={isCollapsed ? 0 : undefined}                 → tabIndex={0}

* feat(integrations): show connected credentials on integration detail page

When navigating to /integrations/google-docs (or any integration), a
Connected section now appears above the templates listing all workspace
credentials tied to that provider. Each row links back to the credential
detail page (/integrations/connected/${id}) for management actions.

Pairs with the earlier change that routes connected items from the
integrations list to the provider detail page instead of directly to
the credential detail page.

* fix(integrations): rename Add in chat to Add to Sim

* fix(skills): rename Add button to Add to Sim

* fix(platform): restore M1/M2/M3 regressions and LazyMotion on landing page

M1 — Invitation guard: re-introduce usePermissionConfig().isInvitationsDisabled
alongside the workspace inviteDisabledReason check. The flag now also
respects NEXT_PUBLIC_DISABLE_INVITATIONS and EE permission-group
disableInvitations, not just billing policy.

M2 — Settings redirects: /settings/integrations and /settings/skills
now server-redirect to /integrations and /skills respectively so old
bookmarks and emails don't silently land on General.

M3 — Starter block search exclusion: restore block.type !== 'starter'
guard in the search store so users cannot add a duplicate Starter block
via the command palette.

LazyMotion: restore LazyMotion + domMax/domAnimation wrappers and m.*
components in landing-preview-panel and landing-preview-home. The
removal was accidental (the full motion bundle was left after an
import cleanup), which caused the entire framer-motion feature set to
load eagerly on the landing page.

* fix(integrations): revert connected list to credential detail; remove settings redirects

* feat(sidebar): restore workspace switcher search with updated styling

Shows a search input in the workspace dropdown when the user has more
than 3 workspaces (WORKSPACE_SEARCH_THRESHOLD). Keyboard navigation:
ArrowDown/Up to move through results, Enter to switch, resets on close.

Styled to match the current branch (border-1/surface-5 tokens, sm text,
11px Search icon) rather than the old staging styles. Highlight state
is wired through chipVariants active prop so it follows the same active
appearance as clicked/hovered items.

* fix(sidebar): clean up workspace search — layout, memo, and effect guard

* fix(sidebar): align workspace rename input selection style with workflow rename

* perf(sidebar): eliminate React re-renders during sidebar drag resize

Previously, every mousemove during resize called setSidebarWidth(), which
both updated the --sidebar-width CSS variable (sync) and set Zustand state
(async). This caused:
  1. A 1-frame transition flash on mousedown — isResizing state had to
     round-trip through React before the is-resizing CSS class was applied,
     so the width transition fired for the first pixel of movement.
  2. A React re-render per pixel dragged — components reading sidebarWidth
     from the store (avatars, usage-indicator) lagged one frame behind the
     container, making the + and ... buttons appear to jump ahead.

New approach:
  - handleMouseDown adds is-resizing directly to the sidebar DOM node before
    any React involvement (synchronous, no frame lag).
  - mousemove writes only to the CSS custom property (zero React renders).
  - mouseup persists the final width to Zustand exactly once.
  - isResizing / setIsResizing state removed from the store and hook — they
    are no longer needed since the class is managed via direct DOM mutation.

* perf(sidebar): add requestAnimationFrame throttle to resize mousemove handler

* fix(sidebar): fix drag-right lag caused by WorkspaceChrome overflow-hidden transition

The sidebar-container's is-resizing class correctly suppressed its own width
transition, but the two wrapper divs in WorkspaceChrome both have
transition-[width]/transition-transform with a 175ms ease. The outer wrapper
also has overflow-hidden, so while the sidebar content was at the correct width
instantly, it was visually clipped by the outer wrapper which was still
animating — causing the + and ... buttons to appear to lag behind the resize
line on drag-right (not on drag-left, since shrinking doesn't clip content).

Fix: add sidebar-shell-outer/sidebar-shell-inner class names to both chrome
wrappers, and suppress their transitions via html.sidebar-resizing rule when
a drag is active. The html.sidebar-resizing class is toggled directly in the
resize hook alongside is-resizing, so it takes effect synchronously on mousedown.

* fix(icons): redesign Download icon to match Upload style; fix Upload/Download confusion

Download icon was missing the tray/shelf line at the bottom that Upload has,
making it look like a plain arrow rather than a matched pair. Updated Download
to use the same viewBox, stroke weight, and three-path structure as Upload
(tray + stem + arrowhead), just pointing down.

Also fix 5 places where Upload (↑) was incorrectly used for download/export
actions:
  - files.tsx: two Download action rows in toolbar and context menu
  - tables/table.tsx: Export CSV toolbar button
  - table-context-menu.tsx: Export CSV context menu item
  - logs.tsx: Export toolbar button
  - landing-preview-logs.tsx: decorative Export button

Import CSV and actual upload actions correctly keep the Upload icon.

* fix(icons): replace Upload with Download on all remaining export/download actions

- panel.tsx: Export workflow dropdown item
- context-menu.tsx: Export in sidebar workflow context menu
- chat.tsx: Export chat button
- output-panel.tsx: Export console CSV button
- terminal.tsx: Export console CSV button
- resource-content.tsx: Export table as CSV + Download file buttons

* fix(icons): fix remaining Upload→Download on download actions in files and logs

- action-bar.tsx: download button in files toolbar
- file-row-context-menu.tsx: Download item in file context menu
- file-download.tsx: both download buttons in log details file viewer

* updated block skills, settings pages, modals, buttons -> chips, blocks missing metadata

* updated skill modal

* improvement(resource-header): refine breadcrumb truncation ux

* improvement(resource): add floating overflow text tooltips

* wire up credits counter

* improvement(resource-header): mute path dropdown title

* refactor(resource-header): share floating-tooltip engine, prune dead overlay tooltips (#4844)

Clean up the breadcrumb truncation feature for reuse and correctness:

- Extract useFloatingTooltip / useIsOverflowing / FloatingTooltip into a shared
  floating-tooltip module. BreadcrumbSegment and FloatingOverflowText now consume
  one implementation instead of duplicating ~150 lines of positioning, velocity,
  overflow-detection, and portal logic.
- Replace the hardcoded terminal-label regex in ResourceHeader with a typed
  `terminal` flag on BreadcrumbItem (set by the document chunk/loading crumbs),
  decoupling the generic header from knowledge-base copy.
- Clear the path-popover close timeout on unmount and reuse the shared
  POPOVER_ANIMATION_CLASSES constant.
- Drop the redundant manual overflow-state writes (fixes a sticky fade mask).
- Revert FloatingOverflowText inside Combobox `overlayContent` back to plain
  truncating spans across files/logs/tables/scheduled-tasks/document: the combobox
  overlay is pointer-events-none, so the tooltip handlers never fired there.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(emcn,resource-header): address PR #4844 review feedback

- useIsOverflowing now uses a callback ref so the ResizeObserver follows the
  element across mount/unmount/reassignment instead of capturing it once at mount.
  Safe for conditionally rendered consumers of the shared hook. (greptile P2)
- Move POPOVER_ANIMATION_CLASSES out of chip-date-picker implementation internals
  into emcn/components/popover/popover-animation.ts, exported from the
  @/components/emcn barrel. Consumers now import from the module boundary.
  (greptile P2)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* upgrade table and styling upgrade

* fix schema to include integration

* fix(files): align delete icon with tables view (Trash → Trash2)

Co-Authored-By: waleed <waleed@simstudio.ai>

* fix(mothership): preserve blockType for integration contexts in sent messages

Integration mention chips were missing their provider icons in sent messages
because blockType was dropped when mapping ChatContext to messageContexts.
renderIntegrationTile returns null without blockType, silently hiding the icon.

* fix(mothership): allow 'integration' resource type in chat resources API

The VALID_RESOURCE_TYPES allowlist was missing 'integration', causing a
400 error when adding integrations to the Mothership resource tab — so
they never persisted and disappeared on refresh.

* fix(ui): Add "File" title next to file resource header

* fix(ui): fix resource header columns being bolded

* fix(resource): keep the floating tooltip from jumping on click

Gate the focus-driven show behind :focus-visible so a mouse click (which
focuses the trigger) no longer re-shows the tooltip anchored to the element's
bottom edge. On click the tooltip now hides cleanly instead of jumping down;
keyboard focus still shows it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* perf(sidebar): eliminate unnecessary re-renders in workspace switcher for non-search users

- onMouseEnter: only set highlightedIndex when showSearch is true, preventing
  a state update + re-render on every workspace row hover for users with ≤ 3
  workspaces where the search is never shown
- onOpenChange: only reset workspaceSearch and highlightedIndex when showSearch
  is true, since both values are always already at their defaults for non-search
  users and setting them triggers a pointless re-render during dropdown close
- data-workspace-row-idx: only set when showSearch is true since the scroll
  effect that reads this attribute is already gated on showSearch

* feat(search): context-aware cmd-k results on the integrations page

When cmd-k is opened on the integrations page, show two new result
groups: connected accounts (visible even with empty input) and catalog
integrations (appear once the user types). Selecting an OAuth integration
deep-links to its detail page with ?connect=oauth so the connect modal
auto-opens. Non-OAuth integrations navigate to the plain detail page.

Both groups are gated to the integrations page only and respect the
hideIntegrationsTab permission. The credentials fetch shares the same
React Query cache key as the integrations page itself (no double fetch).

* refactor(emcn): make the floating tooltip the one canonical Tooltip

Replace the Radix-based emcn Tooltip with the cursor-following floating tooltip so
every tooltip in the app uses one consistent style. Built on the shared
floating-tooltip engine (relocated into emcn), not a parallel implementation.

- Move the floating-tooltip engine into emcn/components/tooltip and export it from
  the barrel; re-point its consumers (FloatingOverflowText, resource-header)
- Extend the FloatingTooltip bubble to render arbitrary children (+ role/id for
  a11y) so it can back general tooltips, not just overflow text
- Rebuild emcn Tooltip (Root/Trigger/Content/Provider/Shortcut/Preview) on
  useFloatingTooltip — compound API preserved, ~350 call sites unchanged, legacy
  side/align props accepted and ignored (the tooltip follows the cursor). Removes
  @radix-ui/react-tooltip usage (package kept for a later cleanup; react-slot
  retained for asChild)

Note: general tooltips now show instantly (no hover delay) and follow the cursor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* style(emcn): put tooltip text on the design scale (text-caption)

Replace the tooltip's ad-hoc `text-xs` + `leading-[18px]` with the semantic
`text-caption` (12px) font-size token so the text styling is fully on the design
scale and self-documenting, matching how the rest of the system is set up. The
color already used the global `--text-body` token. No visual change (still 12px
with a ~18px line height).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(sidebar): add empty task state and inline task creation

- Show "No tasks yet" in the Tasks section (expanded and collapsed) when the list is empty
- Clicking + now creates a task via the API and navigates directly to it, rather than navigating to home
- Add isCreatingTaskRef guard to prevent double-click from spawning multiple tasks
- Disable + button while creation is pending
- Fall back to home navigation on creation error

* invite, billing, home

* improvement(seats): auto purchase seats on invitations into workspace (#4857)

* improvement(seats): auto purchase seats on invitations into workspace

* improve sampling for seat drift reconciler

* address comments

* feat(knowledge): align connector UI with integrations page styling

- ConnectorTypeCard now matches integration rows: brand-colored rounded-xl tile, ArrowRight, title/subtitle hierarchy
- ConnectorCard icon upgraded from flat surface-4 to branded tile (white icon on brand bg, graceful fallback)
- Connector header badges use chipVariants instead of custom Button classes
- Add-connector search input aligned to integrations style (h-[30px], rounded-lg, border-1)

* fix(icons): trim Folder SVG viewBox to remove right-side whitespace

The folder path only extends to x≈14.33 in a 15-unit viewBox, leaving
~0.5 units of empty space on the right. At 12px rendered size this
produces ~0.4px extra gap (visible as ~1px on retina displays) compared
to solid icons like the workflow color square. Trimming the viewBox to
14.5 units makes the folder fill its chip slot evenly.

* fix(user-input): restore draft text synchronously to preserve contexts on nav

The SSR-safe approach (empty useState + effect restore) created a timing
window where the sync effect in useContextManagement fired with message=''
before the value was set, clearing any restored contexts. Folder and workflow
contexts (not re-added by applyAutoMentions) were lost on every nav-back.

Revert to the staging approach: initialize value synchronously from the
draft store so message is already populated when effects run, matching
the behavior on staging.

* fix(queue): render context chips in queued messages

- Remove plainMentions from queued message rows so context chips render
  with icons, consistent with sent messages
- Fix computeMentionRanges to use '/' prefix for skill contexts (content
  has the slash trigger restored at submit time, not '@')

* fix(mothership): remove integrations from add-resource dropdown

* fix(mothership): comment out integrations from add-resource dropdown

* chore(db): drop form, templates, template_creators, template_stars tables

These tables backed the Forms and Templates platform features which were
intentionally removed from this branch. Clean up the DB schema to match.

* chore(db): add migration metadata for 0224 drop tables

* block icons, sidebar, toolbar

* chore: remove remaining dead code for template-profile feature

- Remove 'template-profile' from SettingsSection union type
- Remove 'template-profile' entry from SECTION_TITLES
- Remove now-redundant template-profile guard in settings sidebar
- Remove commented-out template-profile nav item

* fix(multi-select): preserve anchor on range selection for tasks and folders

After a shift+click range, the anchor (lastSelectedTaskId / lastSelectedFolderId)
was being updated to the end of the range (toId). This caused subsequent
shift+clicks to extend from the wrong point instead of the original click.

Standard behavior: anchor stays at the initial click (fromId) so repeated
shift+clicks always expand/contract relative to where you started.

* feat(emcn): add SearchInput component and unify search bars platform-wide

- Add SearchInput to emcn: 30px chip-family filled search input matching the
  integrations page pattern (border-1, surface-5, leading Search icon)
- Migrate all 22 search bars across settings, EE pages, and integrations to
  SearchInput (only layout classes allowed at callsites)
- Rename Sim Keys -> Sim API Keys in nav/title; page copy now says API key
- Remove components/ui input, label, and verified-badge; migrate consumers
  to emcn equivalents or raw inputs (table cell editor, wand prompt bar)
- Delete dead EE skeleton files (data-drains, data-retention)
- General settings: Home Page chip moves to header left as navigation

* fix(files,tables): restore new-file editor autofocus and CSV import error toasts

Both were dropped on the staging line and regressed vs production (main):

- Files: the new-file editor autofocus chain (files.tsx -> file-viewer ->
  text-editor) was stripped by the react-doctor dead-code pass in #4544,
  which misread the prop-drilled `autoFocus` (consumed by an imperative
  `editor.focus()` effect) as unused. Restored the prop through all three
  layers and the one-shot focus effect so creating a new file focuses the
  editor immediately.
- Tables: CSV import failures were silently logged with no user feedback.
  Restored the per-file and generic `toast.error` surfacing.

* feat(home): score suggested actions by workspace signals

- Derive the suggestion pool from the curated block template catalog
  (1,343 prompts across 172 blocks) instead of 15 hardcoded entries
- Fix inverted relevance: prompts for connected providers are now boosted
  4x (instantly runnable) instead of excluded; unconnected discounted 0.4x
- Weight by featured (3x), popular category (1.5x), and resource gaps
  (no tables -> boost table starters; has KBs -> dampen KB-creation prompts)
- Weighted sampling without replacement, max one suggestion per block
- Connect rows weighted by catalog template count; 2 for fresh workspaces,
  1 once something is connected
- Key the catalog map by both versioned and base block types so gmail_v2
  templates resolve (gmail, github, notion, linear were silently dropped)
- Replace derive-in-effect state with a useMemo keyed by a shuffle nonce
- Add suggested_action_clicked / suggested_actions_shuffled /
  suggested_actions_toggled PostHog events

* billing, teammates

* improvement(credentials): credentials invites, secrets tab wiring up (#4874)

* improvement(credentials): move away from invite notion

* wire up secrets ui/ux

* address comments

* get consistent styling by removing emcninput + text area

* styling consistency

* remove fallback

* address comment:

* refactor(ui): migrate settings & workspace UI to chip design system

Migrate modals to ChipModal (showDivider, hint, resizable, size, leading,
ChipModalTabs), standardize Chip variants, add ChipCombobox wrapper, and apply
chip inputs/dropdowns across settings, knowledge, logs, tables, inbox, EE tabs.
Render ChipModalTabs as a ChipSwitch segmented control. Align /settings/secrets
detail with /integrations, refresh whitelabeling, and restore file-editor
autofocus and CSV import error toasts.

* fix(mothership): restore integrations to useAvailableResources for @ mention

Integrations were fully removed from useAvailableResources which broke
the @ mention menu since user-input shares the same hook. Now integrations
are always included in the hook but excluded at the AddResourceDropdown
component level, keeping them out of the sidebar + menu while remaining
available for @ mention autocomplete.

* refactor(settings): chip design-system consistency pass across all tabs

Extract a shared chip-field shell (CHIP_FIELD_SHELL/CHIP_FIELD_INPUT) mirroring
Input variant='chip' and route secrets, credential detail, and integrations
credential detail through it (30px height, font-medium, focus ring). Add a
Discard action to the secrets header when dirty. Group BYOK providers into
Models/Search & web/Enrichment sections and align its tiles to the integrations
tile.

Normalize list-row typography to text-[14px]/text-[12px] and icon tiles to
rounded-xl + border across api-keys, copilot, custom-tools, mcp,
workflow-mcp-servers, credential-sets, and access-control. Tone the secrets
Details chip and per-row affordances to ghost. Fix token correctness: raw
tailwind colors to design tokens (data-drains), missing chip variant on the
Snowflake role input, ColorInput chip-field reuse (whitelabeling), error token
and icon sizes (workflow-mcp-servers), border token (mcp), no-results sizing
(secrets), row chrome (recently-deleted), hover token and Button to Chip
(access-control), and deduped textarea chrome (sso).

* feat(settings): unify filter dropdowns on ChipSelect (integrations style)

Add a ChipSelect emcn component — a filled chip trigger + chevron opening a
DropdownMenu, matching the integrations category filter — supporting single
select, multi-select (checkbox rows), grouped options, and optional in-menu
search. Migrate every settings/EE filter dropdown off ChipCombobox to it:
audit-logs (resource-type multi + time-range), data-retention, data-drains,
general, admin (grouped tool picker), inbox status filter, and the workflow
MCP-server pickers. The SSO provider-id field stays an editable combobox since
it accepts free-text slugs.

Also fix audit findings: chip the MCP client-secret input, normalize an MCP
error-text size, and drop now-dead destination-icon code in data-drains.

* fix(mentions): require explicit @ for integration mentions; decorate sent messages robustly

- Bare integration names in prose (Monday, Notion, Clay) are no longer
  auto-converted to mentions or chipped — mention treatment is strictly
  opt-in via a token-starting @ (fixes the scunthorpe problem)
- @-prefixed mentions still canonicalize casing (@slack -> @Slack) on both
  the keystroke fast-path and bulk paths (paste, template, draft, STT)
- Sent/queued messages now self-sufficiently decorate @IntegrationName
  tokens via a text scan, covering messages sent before the input pass
  ran or authored outside the chat input
- Integration contexts missing a resolvable blockType (messages persisted
  before blockType was saved) are backfilled by label lookup so their
  mention pills render the brand icon again

* refactor(settings): section the API keys page like secrets

Wrap Workspace, Personal, and the allow-personal-keys toggle in SettingsSection
(muted label + divider) instead of bare bold headers, matching the secrets and
BYOK pages.

* fix(settings): ChipSelect renders above modals + full-width form mode

Raise the ChipSelect menu to --z-popover so it layers above modal surfaces
(--z-modal) instead of opening behind them. Add a fullWidth prop that stretches
the trigger and right-aligns the chevron for form-field use, and apply it to the
workflow MCP-server pickers.

* fix(emcn): ChipSelect uses the emcn flat chevron, not lucide's square one

The lucide ChevronDown is square; rendering it at the chip's 9x7 footprint
stretched it. Switch to the custom emcn ChevronDown (built for that wide aspect),
matching the integrations filter and ChipDropdown.

* fix(emcn): ChipSelect trigger hugs its content (w-fit)

In a stacked form layout the trigger was stretched by align-items: stretch,
leaving an empty gap to the right of the value. Add w-fit so the chip sizes to
its content (a compact pill) everywhere; fullWidth form selects are unaffected.

* fix(emcn): ChipSelect uses a square lucide chevron

Revert to lucide's ChevronDown sized square (size-[14px]) so it renders crisp,
matching the standard select chevron used by Combobox.

* renamed tasks to chats

* rename and file change

* improvement(billing): wire up billing, org, teammates tabs + remove deprecated subscription tab (#4887)

* improvement(billing): wire up billing, org, teammates tabs + remove depr subscription tab

* pass exec timeout to tool routes

* reuse helper

* address comments

* address disable comment

* chore(db): remove migration 0224 to regenerate on top of staging

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix type errors and regen migration?

* chore(db): drop branch migration 0226 ahead of staging merge; will regenerate

* chore(db): regenerate migration 0226 after staging merge

* externalize before compaction in fallback'

* fix save/discard chips to be consistent

* fix(ui): remove smodal tabs in favor of chip modal tabs

* fix(ui): skip auto-scrolling on mouse highlight of workspace

* fix(platform): restore settings redirects, forgot-password Enter submit, and tag tooltip visibility

- Re-add SETTINGS_REDIRECTS so /settings/integrations and /settings/skills
  deep links redirect to their top-level routes instead of rendering an
  empty settings panel (accidentally removed in 86da193cc3 one minute
  after cca5054cf6 added it)
- Add opt-in onSubmit to ChipModalField input/email variants and wire it
  in the forgot-password modal so Enter submits again (lost in the
  ChipModal conversion)
- Knowledge tag tooltip: drop the max-h/overflow-y-auto clamp that the
  pointer-events-none floating tooltip made unreachable; truncate each
  tag row instead so all tags stay visible with bounded height

* chore(db): remove migration 0226_third_spot before staging merge

* chore(db): regenerate migration as 0227 after staging merge

* feat(telemetry): add posthog + audit coverage for new platform actions

Audit log (compliance/permission-relevant only):
- org_seat.provisioned — seat auto-purchased when an invite acceptance
  grows the org (actor = accepting user, includes seat delta)
- org_plan.converted — Pro→Team conversion triggered by invite acceptance
- org_seat.drift_reconciled — hourly cron healed a drifted seat count
- credential_member.added/removed/role_changed — credential sharing
  surface was previously fully unaudited
- table.created — parity with existing table.updated/deleted
- skill updates now record skill.updated instead of mislabeled
  skill.created

PostHog:
- seats_provisioned, credential_shared/unshared,
  environment_updated/deleted (key counts only, never names/values)
- table_import_started/completed — background CSV imports previously had
  zero failure observability
- table_exported, file_downloaded, skill_updated
- credential_connected now fires for OAuth completions (draft-hooks),
  credential_deleted for OAuth disconnects; previously only manual
  credentials were tracked
- table_workflow_run gains deployment_mode (live/deployed/mixed)

Also: logger.warn on all new credential-admin 403 denials (members +
environment routes); invite-created audit enriched with
enforcedFixedSeats/plan.

Deliberately excluded as noise: per-hour dead-letter audit rows (would
re-record the same stuck event every cron run) and a duplicate
system-actor org_plan.converted in the Stripe outbox handler.

* feat(home): fill textarea on suggested prompt click instead of sending

Clicking a prompt action in the Suggested Actions panel now populates
the Mothership user-input textarea (via applyAutoMentions) and focuses
it with the caret at end, rather than immediately submitting. The user
can review, edit, and send manually.

* fix(templates): name owning integration in featured block template prompts

Four featured prompts omitted their owning integration name, making them
unbranded and disconnected from their title. Each prompt now explicitly
names GitHub or Google Sheets so mentionifyIntegrations renders the chip
and the copy reads as a self-contained agent-building instruction.

* fix(templates): rewrite fragment prompts so each names its integration and reads as a complete instruction

Featured and non-featured block template prompts that either omitted
the owning integration's canonical name or were phrased as marketing
fragments rather than natural user instructions have been rewritten.
Each updated prompt now starts with an imperative verb ("Build a
workflow that…"), names the owning integration explicitly so the
@-mention chip renders correctly, and aligns with the entry's title.

Files changed: salesforce, hubspot (×2), github, slack, airtable,
firecrawl, iam.

* fix(templates): name integration in remaining block template prompts

- google_docs.ts: replace 'Google Doc' with 'Google Docs document' in 4 prompts; update title 'Meeting notes to Google Doc' to 'Meeting notes to Google Docs'
- google_sheets.ts: replace 'Google Sheet' with 'Google Sheets spreadsheet/Google Sheets' in 3 non-featured prompts
- slack.ts: replace 'Google Doc' with 'Google Docs document' in 'Daily standup summary'
- stripe.ts: replace 'Google Sheet'/'Slacks' with 'Google Sheets'/'Slack' in 'Weekly metrics report'
- reddit.ts: add 'Reddit' to 3 prompts that only referenced subreddits
- notion.ts: rewrite featured prompt to start with a verb and name Notion
- jira.ts: rewrite featured marketing-fragment prompt to start with a verb
- linear.ts: rewrite featured marketing-fragment prompt to start with a verb
- gmail.ts: rewrite featured marketing-fragment prompt to start with a verb and name Gmail

* fix(icons): convert monochrome dark brand icons to currentColor for dark mode

LinkupIcon, InfisicalIcon, IntercomIcon, LumaIcon, GranolaIcon, OnePasswordIcon,
and RailwayIcon were hardcoded to black/near-black fills/strokes, making them
invisible in bare DARK mode. Convert all to currentColor so they follow the
theme-aware text color.

Add iconColor: '#286efa' to IntercomBlock (Intercom brand blue is a confident
mid-tone, safe on both themes).

Two-tone icons (StagehandIcon, AgentPhoneIcon, QuiverIcon) are left unchanged
because their white details are structural — converting to single-tone would
destroy logo legibility.

* removed color, user input, sidebar, suggested actions, chips/emcn

* removed color migration

* improve(blocks): audit block catalog metadata for accuracy and fill gaps

- Fix template prompts that claimed capabilities blocks don't have
  (fabricated triggers, non-existent tools) across ~98 integrations
- Normalize integration tags to family conventions and valid union values
- Align alsoIntegrations and modules with template prompt content
- Add BlockMeta for circleback, imap, and rss trigger integrations
- Add templates to clickhouse and greptile metas
- Remove duplicate PageSpeed deploy-gate template

* chore(telemetry): drop org_seat.drift_reconciled audit

System self-heal bookkeeping doesn't belong in the user-facing audit
trail — membership and seat-purchase changes are already audited, and
the cron's logger output covers ops visibility.

* chore(db): consolidate branch migrations into single 0227

* fix(emcn): make ChipModal scroll internally when content exceeds viewport

The Modal→ChipModal migration dropped the old ModalBody scroll container:
ChipModal renders ModalContent bare (no overflow-hidden) and its wrappers
had no min-h-0 chain, so tall modals (e.g. New data drain with an S3
destination) overflowed max-h-[84vh] off-screen with no way to scroll.

Complete the flex min-h-0 chain through ChipModal's frame and give
ChipModalBody flex-1 min-h-0 overflow-y-auto — header/footer stay pinned,
body scrolls only when constrained. Short modals are unaffected (max-h
caps, it doesn't stretch), and dropdowns inside the body are Radix-portaled
so the new scroll container cannot clip them. Five modals that had locally
patched this with max-h-[Nvh] overrides keep working unchanged.

* fix(emcn): don't close modal when dismissing a dropdown via outside click

Radix dispatches pointer-down-outside to every open dismissable layer at
once, so clicking outside an open dropdown/select inside a modal closed
both the dropdown and the modal in one jarring step. ModalContent now
prevents its own dismissal while a portaled popper layer is open — the
first outside click closes just the popper, the next one closes the
modal.

* docs(skills): de-duplicate and correct agent docs; canonical styling tokens; mirror sim-sandbox rule

* docs(skills): broaden boundary-raw-fetch scope note, sync cursor rule mirrors

* fix(emcn): harden modal popper guard and exempt caret-anchored dropdowns from body scroll

Adversarial review follow-ups to the ChipModal scroll + dismissal fixes:

- Popper guard now requires data-state="open" inside the popper wrapper,
  so a dropdown that is merely animating closed no longer swallows the
  next outside click on the modal (DropdownMenu has an exit animation
  that keeps its wrapper mounted briefly)
- Port the same guard to SModalContent for consistency
- custom-tool-modal: opt its body out of the chrome scroll container
  (flex-none + overflow-visible); the caret-anchored EnvVar/Tag
  autocomplete dropdowns are absolute-positioned inside the body and
  must spill past its bounds rather than clip against a scroll boundary

* refactor(billing): drop unnecessary useCallback wrappers from event handlers

* fix(data-drains): complete chip migration of destination forms and fill missing placeholders

Finishes the in-flight FormField → ChipModalField conversion for all
destination form specs and adds the placeholders that several inputs
never had (S3 bucket/region/access keys, Azure account key, Datadog API
key, webhook signing secret/bearer token) — the cause of the New Data
Drain modal showing placeholder-less inputs inconsistently.

* fix(emcn): broaden modal popper guard to onInteractOutside

Covers the focusOutside dismissal path too: when a popper's focus scope
unwinds on close, the transient focus shift could still dismiss the
modal (and simultaneous body pointer-events lock teardown could freeze
the page). Same data-state="open" scoping as the pointer guard.

* fix(emcn): harden modal outside interactions

* update audit mock

---------

Co-authored-by: andres <k62hc5kjst@privaterelay.appleid.com>
Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Waleed <walif6@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Theodore Li <theo@sim.ai>
Co-authored-by: andresdjasso <andresdjasso@users.noreply.github.com>
Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
Co-authored-by: waleed <waleed@simstudio.ai>
2026-06-06 11:39:48 -07:00
WaleedandClaude Opus 4.8 16954e46fe feat(integrations): add ClickHouse block and expand Dagster + Tinybird tools (#4883)
* feat(integrations): add ClickHouse block and expand Dagster + Tinybird tools

* fix(tinybird): fail loudly on invalid query_pipe parameters JSON

parsePipeParameters previously returned {} on any JSON parse error, so a
mistyped 'parameters' input produced a successful pipe call with the dynamic
filters silently dropped. Throw a clear error for non-empty, non-object input
instead; an omitted/empty value still means 'no parameters'.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(dagster): guard NaN numeric coercions and bound list_assets pagination

Address PR review:
- Route all block numeric coercions (list_runs limit/createdAfter/createdBefore,
  get_run_logs logsLimit, list_assets assetsLimit) through a toFiniteNumber()
  guard so invalid/wand-generated text becomes undefined instead of NaN.
- list_assets now applies a default page size (100) when no limit is given, so
  paging stays bounded and hasMore is meaningful even when limit is omitted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(dagster): make list_assets hasMore exact via fetch N+1

Address PR review (hasMore true on exact page): request one extra row
(pageSize + 1), use its presence as the authoritative hasMore, slice it off,
and derive the returned cursor from the last RETURNED asset's key path
(JSON-serialized; Dagster normalizes JS/Python whitespace on the way in).
This removes the false-positive hasMore when the final page is exactly full.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(clickhouse): enforce read-only query operation and harden WHERE-clause guard

* fix(dagster): make list_runs hasMore exact via fetch N+1

Address PR review (list runs false hasMore): request one extra row
(pageSize + 1), use its presence as the authoritative hasMore, and slice it
off before mapping. Removes the false-positive hasMore (and misleading cursor)
when the final page is exactly `limit` runs long. Mirrors the list_assets fix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(clickhouse): restrict DROP PARTITION to literal values to prevent SQL injection

* fix(clickhouse): reject chained statements in read-only query operation

* fix(clickhouse): force JSON output on query path and ignore comments when detecting chained statements

* fix(tinybird): encode datasource/pipe names in URL paths to prevent traversal

A user-or-llm datasource/pipe name interpolated raw into the URL path (e.g.
'real_ds/../../other') is normalized by the WHATWG URL parser and can target a
different endpoint. Wrap the path segment with encodeURIComponent in the
truncate, delete, and query_pipe URLs. Events/append pass the name via
URLSearchParams, which already encodes, so they were unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(clickhouse): block WITH-led writes/DDL in read-only query operation

* fix(clickhouse): validate column types structurally and normalize FORMAT around SETTINGS

* fix(clickhouse): balance-check ORDER BY/PARTITION BY and skip leading comments in read-only guard

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 14:49:51 -07:00
Waleed e5a46d7959 feat(linq): add Linq iMessage/SMS/RCS integration (34 tools, block, attachment upload) (#4831) 2026-06-01 13:44:33 -07:00