Commit Graph
5112 Commits
Author SHA1 Message Date
Waleed 48c1b453df feat(integrations): extend ElevenLabs, Google Drive, Firecrawl, Pinecone, Resend, and S3 tool depth (#5270)
* feat(firecrawl): add crawl status/cancel, batch scrape + status, extract status, credit usage tools

* feat(resend): add audiences, broadcasts, and cancel-email tools

* feat(pinecone): add delete/update vectors, index, and stats tools

* feat(google-drive): add revisions, comments, and export tools

* feat(elevenlabs): add voices, settings, models, user, sound-effects, speech-to-speech, audio-isolation tools

* feat(s3): add bucket CRUD, head-object, presigned-url, and batch-delete tools

* chore(api-validation): bump route baseline to 873 for wave-3 internal tool routes (s3, elevenlabs, google_drive export)

* docs(integrations): regenerate docs + catalog for wave-3 tools

* fix(integrations): audit fixes for wave-3

- pinecone: read camelCase vectorType/deletionProtection (with snake_case fallback) so list_indexes/describe_index populate them; make describe_index_stats casing defensive
- google-drive: URL-encode fileId in the export route
- remove extraneous inline/section-divider comments across new blocks/tools; convert type docs to TSDoc

* fix(integrations): address review — elevenlabs settings bleed, batch-scrape job-id guard, s3 head-object existence

- elevenlabs: select stability/similarityBoost by operation so a stale edit-settings value can't bleed into a TTS call
- firecrawl: fail fast with a clear error when batch scrape returns no job id (avoids a misleading polling timeout)
- s3: head_object on a missing key now returns exists:false instead of a generic failure

* fix(s3): allow exists:false in head-object response contract (schema must match the missing-object output)

* fix(pinecone): guard JSON.parse of ids/filter/values/sparseValues/setMetadata

Malformed JSON-string input now throws a clear '<field> must be valid JSON' error via a shared parseJsonParam helper instead of crashing the request body builder.

* fix(pinecone): enforce mutual exclusivity of ids/deleteAll/filter in delete_vectors

Pinecone treats these delete selectors as mutually exclusive; the tool now requires exactly one and throws a clear error otherwise, instead of sending a conflicting body.

* fix(integrations): I/O fidelity vs API docs (wave-3 audit)

- pinecone: normalize describe_index_stats per-namespace vector_count -> vectorCount
- firecrawl: remove phantom 'sources' from extract_status, add real creditsUsed/tokensUsed; expose batch_scrape maxConcurrency/ignoreInvalidURLs
- google-drive: drop undocumented supportsAllDrives from the files.export URL
- elevenlabs: add next_page_token input to list_voices (fixes pagination dead-end)
- resend: surface segment_id on get_broadcast; declare replyTo + segment_id in block outputs
2026-06-29 15:59:14 -07:00
Waleed c7bb37d488 feat(workflow-renderer): extract pure WorkflowBlockView + SubBlockRowView (#5267)
* feat(workflow-renderer): extract pure SubBlockRowView from the block's summary row

Splits the canvas block's collapsed subblock summary row into a pure SubBlockRowView (title + resolved displayValue + monospace flag) in @sim/workflow-renderer. The ~9 selector-name hydration hooks stay in the SubBlockRow container behind its memo comparator; the view receives only resolved strings. Byte-identical row JSX. First step toward the full WorkflowBlockView.

* feat(workflow-renderer): extract pure WorkflowBlockView shell

Moves the canvas block's render (header, badges, dynamic handles, ring) into a pure WorkflowBlockView in @sim/workflow-renderer. WorkflowBlock becomes a thin container that resolves all stores/hooks/permissions, builds the subblock rows + actionBar slots, and binds wouldCreateConnectionCycle (reads the edge store fresh per call to preserve cycle prevention). getHandleClasses/getHandleStyle move into the view; config.icon/bgColor, the webhook provider name, and every visual flag cross as props. Byte-identical JSX — every handle id/class/style/offset and badge guard preserved (verified by an independent adversarial audit). Container drops from 1137 to 829 lines.
2026-06-29 13:06:08 -07:00
Waleed 69e3550a72 feat(integrations): extend Telegram, Outlook, and Notion tool depth (#5265)
* feat(telegram): add edit, forward, copy, location, contact, poll, pin, reaction, chat-action, and chat-info tools

* feat(outlook): add reply, reply-all, folders, attachments, search, and message-update tools

* feat(notion): add block children CRUD, comments, and users tools (v1 + v2)

* docs(integrations): regenerate docs + catalog for telegram, outlook, notion tools

* fix(notion): guard pageSize coercion with Number.isFinite so non-numeric input isn't forwarded as NaN

* fix(telegram): normalize send_poll options (array, JSON string, or newlines) so json-typed input can't crash on .map

* fix(integrations): harden outlook search quoting, notion archive default, and telegram poll options

- outlook: strip embedded double quotes from the $search term so they can't break the quoted KQL query
- notion: default the update_block Archive dropdown to 'Leave unchanged' so updates don't send an unintended archived:false restore flag
- telegram: pass raw poll options through to the tool's normalizePollOptions (handles array/JSON-string/newlines) so the block layer no longer mishandles JSON-string input

* fix(integrations): normalize outlook categories input and clamp notion pageSize

- outlook: normalize update_message categories (array, JSON string, or comma/newline) so a JSON-string value isn't silently dropped
- notion: clamp pageSize to Notion's 1-100 range (truncated) so out-of-range values don't hit an API error

* fix(outlook): pass raw categories to the tool's normalizeCategories so the block handles JSON-string input too

* fix(outlook): allow clearing message categories by passing an empty array

An explicit empty array now sends categories:[] to clear all labels, a non-empty value replaces, and an absent/empty value leaves them untouched — matching the documented replace semantics.

* fix(outlook): only clear categories on an explicit empty array, not a delimiter-only string

A string that normalizes to no categories (e.g. just commas) is now a no-op rather than clearing all labels; clearing requires an explicit empty array.

* fix(notion): clamp page_size to 1-100 at the tool layer for list comments/users and block children

Adds a shared clampNotionPageSize helper so the agent-direct path is bounded to Notion's range, not just the block path.

* fix(outlook): use consistent set/replace semantics for message categories

Categories are replaced with the provided non-empty list and left unchanged when empty, consistent across the block and agent paths. Drops the ambiguous empty-array clear (clearing all categories isn't expressible unambiguously from the comma-separated field) and updates the description to match.
2026-06-29 13:02:29 -07:00
Waleed 950c2602c2 fix(uploads): attach compiled binary for AI-generated docs, not source (#5266)
* fix(uploads): attach compiled binary for AI-generated docs, not source

AI-generated documents (pdf/docx/pptx/xlsx) created in Chat are stored as
their generation source, with the rendered binary in a separate
content-addressed artifact store. Read/preview paths swap in the binary, but
attachment/upload/provider paths downloaded the raw source — so a generated
PDF emailed via Gmail (and 30+ other tools) arrived as the generator script
renamed .pdf.

- Add shared resolveServableDocBytes resolver + downloadServableFileFromStorage
  wrapper; the file-serve route now delegates to the same resolver so the two
  paths resolve identically.
- Migrate ~34 attachment/upload/parse tool routes + the LLM provider attachment
  path to the servable download; media-only tools and source-editing paths keep
  the raw download intentionally.
- Surface a retryable 409 (shared docNotReadyResponse) when a doc artifact is
  still compiling instead of shipping source.

* fix(uploads): return retryable 409 for not-ready docs in slack/teams sends

The slack send-message and teams write_channel/write_chat routes call
download helpers that can throw DocCompileUserError while a generated doc is
still compiling. Map it to the shared docNotReadyResponse 409 (matching the
other migrated tool routes) instead of a generic 500. The provider attachment
path is internal LLM execution (no HTTP response), so it intentionally
propagates the typed error.

* fix(uploads): not-ready 409 for uptimerobot, real MIME for non-doc, xlsx tests

Address review findings:
- uptimerobot create-psp/update-psp now map DocCompileUserError to the shared
  409 (Greptile + Cursor flagged the gap alongside slack/teams).
- downloadServableFileFromStorage returns the extension-derived MIME
  (getMimeTypeFromExtension) for non-doc files instead of an empty string when
  userFile.type is unset.
- Add resolveServableDocBytes tests for the three xlsx branches (binary ZIP
  passthrough, not-ready throw under E2B+beta, no-workspaceId raw passthrough).

* fix(uploads): enforce attachment size limits on resolved bytes

Size limits were checked against userFile.size (source metadata) before
resolution, but a generated doc resolves to a larger compiled binary — so a
small-source doc could pass the pre-check yet exceed the service limit. Add a
post-resolution check on the actual resolved bytes (mirroring docusign/vanta)
across gmail send/draft/edit-draft, smtp, outlook send/draft, telegram, sftp,
and teams; the cheap source pre-check stays as an early reject.

* chore(uploads): drop extraneous inline comments from servable-file changes

* fix(sftp): enforce 100MB cap on cumulative resolved bytes, not per-file

The SFTP batch upload checked each resolved file against the 100MB cap
individually, so multiple resolved attachments could each pass while their
combined size exceeded the limit. Accumulate resolved bytes across the loop
and reject once the running total exceeds the cap.

* fix(sendgrid): reject attachments exceeding SendGrid's 30MB limit on resolved bytes

SendGrid had no attachment-size guard, so a generated doc resolving to a large
compiled binary could be sent and fail opaquely at the API. Add a post-resolution
total-size check (30MB, SendGrid's documented message limit) matching the
gmail/smtp/outlook routes.
2026-06-29 13:01:56 -07:00
Theodore LiandClaude Opus 4.8 a6196e0c86 perf(dev): curate SIM_DEV_MINIMAL_REGISTRY to core toolbar blocks (#5251)
Rebuild the dev-only minimal block registry from the canonical, toolbar-visible
core (category blocks/triggers, not hideFromToolbar, latest version). Adds the
visible workflow_input and table blocks (previously only the hidden `workflow`
was present, so the Workflow block never rendered in dev:minimal), drops the
superseded/hidden entries (api_trigger, chat_trigger, input_trigger,
manual_trigger, router, starter, workflow), and hand-prunes the heaviest /
rarely-core-dev blocks (mothership, pi, tts, stt_v2, image_generator_v2,
video_generator_v3, circleback).

Dev-only, gated on SIM_DEV_MINIMAL_REGISTRY=1; never aliased in production.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 15:17:42 -04:00
Waleed 27b2a4f585 feat(workflow-renderer): extract edge, subflow, and note Views into @sim/workflow-renderer (#5263)
Adds a @sim/workflow-renderer package with pure, props-driven WorkflowEdgeView, SubflowNodeView, and NoteBlockView shared by the editor and (future) docs preview. Moves block-dimensions constants into the package. Each editor node becomes a thin Container that wires stores/permissions and injects the editor-only ActionBar via a slot. No optimizePackageImports for the workspace component packages (avoids the toast-style module duplication); Tailwind scans the package source.
2026-06-29 11:45:05 -07:00
Theodore Li 23ec96b02a feat(pii): add redaction timing metrics across sidecar and persist path (#5264)
- Log per-request duration in the Presidio sidecar (/analyze, /anonymize)
- Add durationMs to the mask-batch endpoint log line
- Emit per-execution PII redaction timing (stringCount, totalBytes, durationMs, scrubbed)
2026-06-29 14:04:49 -04:00
Waleed 56f9393d7e fix(prism): load prismjs core before language components (#5262)
prismjs language components (prismjs/components/prism-*) register on the global Prism that core installs; importing a component before core throws 'Prism is not defined' (in SSR and the client). The @sim/emcn extraction changed bundling so core no longer loaded eagerly first, exposing the latent bad order on the home (chat-content) and workflow-editor pages.

- chat-content.tsx: import prismjs core first (it needs ts/bash/css/markup, which emcn does not register).
- code-editor.tsx, input-format.tsx, code.tsx: drop the redundant prism-json/prism-python imports — these highlight via emcn's languages, which already registers js/json/python in the correct order.
2026-06-29 10:34:00 -07:00
Waleed f5116f45c3 fix(emcn): resolve Calendar icon/component barrel collision and preserve prism side effects (#5261)
Two post-extraction regressions:

1. The barrel resolves the Calendar name-collision to the date-picker COMPONENT (symmetric with Table). Scheduled-tasks imported Calendar from the barrel but used it as an ICON, so its header rendered a date picker. Route the icon consumers (scheduled-tasks + loading) to @sim/emcn/icons, matching the 'icons come from the /icons subpath' convention.

2. The package's new sideEffects: ['**/*.css'] marked code/prism.ts as side-effect-free, but it registers prismjs languages on the global Prism (a real JS side effect). The bundler could drop/reorder its core init, causing 'Prism is not defined'. Add code/prism.ts to sideEffects so the bundler preserves it.
2026-06-29 09:29:12 -07:00
Waleed 6dea1dc5ed fix(knowledge): send tag filters as a JSON string so the document filter works (#5259)
* fix(knowledge): send tag filters as a JSON string so the document filter works

The document-list tag filter never reached the database. The `tagFilters`
query field was a Zod `.transform()` that decoded the JSON string into an
array of objects; the client's `requestJson` parses the query before
serializing, so `appendQuery` received the array and emitted
`tagFilters=[object Object]` into the URL. The route then failed to
`JSON.parse` it and returned 400, so the list came back empty (or stale via
keepPreviousData) regardless of operator or value.

- Model `tagFilters` as the wire string it actually is; decode it server-side
  via a new `parseDocumentTagFiltersParam` helper (route maps a bad value to 400).
- Harden `appendQuery`: throw on an array-of-objects query param instead of
  silently serializing `[object Object]`, so this whole class fails loudly.
- Default the text tag-filter operator to `contains` so a partial value matches.
- Tests: requestJson serializes the JSON param verbatim + the guard throws; the
  query schema keeps tagFilters a string; the decode helper round-trips.

A full sweep of every GET/DELETE contract query field confirmed this was the
only field of this class — logs filters and table filter/sort are unaffected.

* fix(knowledge): reject tag-filter operators invalid for the field type

Greptile P2: documentTagFilterSchema accepted any non-empty operator string, so
an unsupported operator was silently dropped by the query builder instead of
returning 400. Validate the operator against the field type's allowed set
(single source of truth in filters/types) via superRefine.

* fix(knowledge): validate tag-filter type against the slot, not the client claim

Greptile P1: operator validation trusted the client-supplied fieldType, so a
numeric slot could be sent with fieldType 'text' + 'contains' and slip through
to build a text LIKE on a numeric column. Validate against the slot's inherent
type via getFieldTypeForSlot (the source of truth): reject unknown slots and
fieldType/slot mismatches at the boundary before checking the operator.

* fix(knowledge): validate tag-filter values against the field type

Greptile P1: value/valueTo were z.unknown(), so a number filter accepted 'abc',
a date filter 'not-a-date', etc. — unusable values the query builder then
silently dropped. Add a shared isValidFilterValue (single source of truth in
filters/types) and reject unusable value/valueTo at the boundary, including the
between upper bound.

* fix(knowledge): only send a between tag filter once both bounds are set

Cursor Bugbot: the strict valueTo validation made a partially-entered between
filter (lower bound only) 400 and break the whole document list mid-entry.
activeTagFilters now withholds a between row until both bounds are filled —
consistent with already requiring the lower bound before sending any filter — so
the list keeps loading while the range is being entered.

* fix(knowledge): reject impossible calendar dates in tag filters

Greptile P1: the date value check was format-only, so 2026-02-30 / 2026-99-99
passed the boundary and then made the document query's ::date cast throw a 500.
Validate real calendar dates by round-tripping the parsed parts.
2026-06-29 09:07:11 -07:00
Waleed f5f87de96d fix(emcn): repair app-wide crash and unstyled UI after package extraction (#5258)
Two regressions from moving emcn into @sim/emcn:

1. optimizePackageImports['@sim/emcn'] rewrote barrel imports to direct subpaths, duplicating the toast module so ToastProvider (layout) and useToast (workspace permissions provider) resolved different ToastContext objects — useToast threw 'must be used within <ToastProvider>' on every workspace route. Removed @sim/emcn from optimizePackageImports.

2. emcn's source left apps/sim's Tailwind content globs (and docs' v4 auto-content scope, which excludes node_modules), so utility classes used only inside emcn components stopped generating and components rendered unstyled. Added the package to apps/sim's Tailwind content and a @source to docs.
2026-06-28 23:52:47 -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 d878f15815 feat(integrations): extend Airtable, Google Docs, WhatsApp, and Excel tool depth (#5256)
* feat(airtable): add delete and upsert record tools

* feat(google-docs): add batchUpdate text, table, image, and style tools

* feat(whatsapp): add template, media, interactive, reaction, and mark-read tools

* feat(microsoft-excel): add clear, format, create-table, sort, and delete-worksheet tools

* fix(integrations): address review feedback

- google-docs: use camelCase fontSize field mask; normalize string booleans for bold/italic/underline and matchCase
- microsoft-excel: escape OData single quotes in worksheet/table keys; validate range for clear/format
- airtable: enforce batch limits (delete <=10 ids, upsert <=10 records and 1-3 merge fields) with clear errors

* fix(integrations): address round-2 review

- airtable: coerce upsert typecast as string-aware boolean (string "false" no longer truthy)
- microsoft-excel: format_range surfaces precise partial-state error when fill PATCH fails after font (no atomic font+fill endpoint in Graph)

* fix(whatsapp): treat 2xx mark-as-read as success unless body says success:false

* fix(integrations): build fix, doc accuracy, and comment cleanup

- google-docs: align manualDocumentId condition with the document selector so the documentId canonical group has matching conditions (fixes canonical-pair block test / build)
- microsoft-excel: describe fill/font color as hex code only (Graph does not reliably accept named colors)
- remove verbose explanatory inline comments from new tools (keep idiomatic section dividers)
- regenerate integration docs + integrations.json catalog from the block registry

* fix(integrations): harden delete-record id coercion and excel sort-column validation

- airtable: coerce recordIds entries via String() so numeric JSON values don't crash on .trim()
- microsoft-excel: drop the silent sortColumn default to 0 so invalid input surfaces the tool's clear validation error (both v1 and v2 blocks)

* fix(airtable): coerce upsert fieldsToMergeOn entries via String() to handle non-string values
2026-06-28 22:42:32 -07:00
Vikhyath Mondreti c59631698f chore(deploy): remove deploy as a2a (#5255)
* chore(deploy): remove a2a

* add block
2026-06-28 20:01:24 -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
Vikhyath Mondreti 5a8134119a feat(workspaces): fork + push/pull (#5210)
* feat(workspaces): fork + push/pull

* type fix

* fix tests

* progress on ux

* remove modal section

* improve UI of modal

* update more ui

* make rollback part of the footer

* track skipped count correctly

* address comments

* make it workspace admin level

* update skipped count

* address more comments

* deal with unbounded memory possibility

* fix deleted kb article bug

* no deployed workflow case

* UI/UX cleanup

* fix oauth dropdown case

* fix oauth selector issue

* infra work + activity log

* consolidate migration

* update modal state

* more UI simplification

* grammar

* update audit report ui

* perf improvements

* fix tool input scenarios and add dependsOn UI handling

* minor comments

* fix webhook stability issues + drift detection removal

* make dependsOn subblock mapping cleanly stored

* fix: harden fork dependent-value mapping (clear stale rows, identity-guard first-sync fallback, perf + cleanup)

* address comments

* update comment

* enforce admin perms for activity api

* fix required + dependsOn combo
2026-06-28 13:13:57 -07:00
Waleed 3766582e7d fix(webhooks): run inactive deployment-version cleanup inline on deploy (#5250)
When a deploy activates a new version, superseded versions' webhooks are
removed by a separate, best-effort CLEANUP_INACTIVE outbox event. When that
event is lost/dead-letters, old-version webhooks linger as is_active orphans
that fetchActiveWebhooks skips (version mismatch), so they silently stop
polling (~515 webhooks across ~130 workflows in prod).

Run the existing cleanupInactiveDeploymentVersions synchronously in the
SYNC_ACTIVE handler, right after the active version's webhooks/schedules are
registered, falling back to the deferred outbox event only if the inline pass
throws. This reuses the existing guarded cleanup, which re-checks each version
is still inactive before tearing anything down (so it never touches the active
version) and runs strictly after registration (so a teardown failure can't
block it).
2026-06-27 18:11:27 -07:00
Waleed 3e03f8c285 fix(webhooks): cast json provider_config for atomic jsonb merge (#5249)
updateWebhookProviderConfig built a DB-side merge with jsonb operators
(COALESCE(provider_config, '{}'::jsonb) || $1::jsonb), but the
provider_config column is json, not jsonb. Postgres cannot apply jsonb
merge operators to a json column, so every polling state write failed
with "could not convert type jsonb to json" — silently breaking
historyId/lastCheckedTimestamp/pageToken/lastSeenGuids persistence for
all polling webhooks (Gmail, RSS, Google Sheets/Drive, Outlook, IMAP)
since the atomic-merge change landed.

Cast the column to jsonb for the || / - merge and cast the result back
to json for storage, matching the existing pattern in subscription.ts.
2026-06-27 17:26:56 -07:00
Waleed 7a2103e2d4 improvement(logs): move per-block progress markers to Redis to cut write amplification (#5248)
* improvement(logs): move per-block progress markers to Redis to cut write amplification

Per-block lastStartedBlock/lastCompletedBlock markers were persisted via a
jsonb_set UPDATE on workflow_execution_logs on every block start and complete
(~2N UPDATEs per run) — the heaviest write query in the DB. These are live
progress breadcrumbs with no DB-polling consumer (live progress comes from the
executor over WebSocket); their only durable value is a breadcrumb folded into
the final record.

Behind the redis-progress-markers flag, markers now live in Redis during the run
and are folded into the single terminal UPDATE at completion, dropping per-run
row UPDATEs from ~2N+1 to 1.

- New progress-markers module: HASH execution:progress:{id}, atomic Lua
  monotonic-guard writes preserving the existing <= ordering, reservation-aligned
  TTL backstop, graceful no-op when Redis is unavailable
- Deterministic GC: cleared at every terminal/pause boundary; TTL covers crashes
- Flag resolved once per logging session so a run never mixes write paths
- Fold markers into the completion record (Redis wins, falls back to row markers)
- Merge live markers for in-flight detail reads
- Extract shared getExecutionReservationTtlMs so marker and admission-slot TTLs
  share one source of truth

* fix(logs): SQL fallback when Redis marker write fails, fold markers on force-fail, validate marker shape

Addresses review feedback on the redis-progress-markers PR:
- persistLast* now falls back to the jsonb_set UPDATE when Redis is unavailable or the write fails (setLast* returns whether it persisted), so a marker is never dropped when the flag is on without a healthy Redis.
- markExecutionAsFailed folds live Redis markers into execution_data before clearing, so the last-started/last-completed breadcrumb survives the force-fail path.
- getProgressMarkers validates marker shape (rebuilds from typed fields), so a stale or wrong-shaped Redis value can never reach API consumers.

* chore(logs): convert inline marker comments to TSDoc

* fix(logs): preserve markers when the completion read fails

getProgressMarkers now returns null on a Redis read error (vs {} for genuinely empty). completeWorkflowExecution and markExecutionAsFailed skip clearProgressMarkers when the read returns null, so a transient read error at completion no longer wipes markers that are still durably in Redis — the TTL reclaims them instead.

* fix(logs): resolve marker store split-brain by latest-timestamp-wins + drain on force-fail

- When a Redis marker write falls back to SQL, Redis and the row can each hold a marker for a different block; reads/folds previously preferred Redis unconditionally and could pick a stale value. Now the completion fold, the in-flight detail read, and the force-fail fold all pick the marker with the later timestamp (pickLatestStartedMarker/pickLatestCompletedMarker; markExecutionAsFailed uses a monotonic SQL guard).
- markAsFailed now drains pending per-block marker writes (not just the completion promise) before folding, so a force-fail racing onBlockStart/onBlockComplete still captures the latest breadcrumb.

* fix(logs): harden Lua marker guard against non-table decoded values

Guard the monotonic-check index with type(decoded) == 'table' so a corrupted Redis field that decodes to a non-table (e.g. a number) can't error the eval; our write path only ever stores JSON objects, so this is defense-in-depth.

* perf(logs): skip completion Redis read/clear when markers went to SQL

completeWorkflowExecution now takes readProgressMarkers (the session's resolved marker mode); when the flag is off it skips the per-completion HGETALL+DEL entirely instead of probing a key that was never written. Sticky to the session so it stays flip-safe (an execution that wrote to Redis always folds+clears Redis). Non-session callers default to true (safe read-and-fold). Also hardened the Lua guard with type(decoded)=='table'.
2026-06-27 16:56:30 -07:00
Waleed b8d0b4f318 fix(sso): keep an exit affordance in edit mode when clean (#5247)
After clicking Edit on an existing SSO provider, the only header action was
SaveDiscardActions, which renders nothing when the form is clean — so there was
no way to leave edit mode back to the read-only summary without first changing a
field or navigating away. Render a Cancel chip when isEditing && !hasChanges
(the dirty-gated Discard already exits when there are changes).
2026-06-27 15:30:27 -07:00
Waleed d49326316a improvement(clickhouse): expand block templates and skills, normalize tool versions (#5246)
* improvement(clickhouse): expand block templates and skills, normalize tool versions

- Expand ClickHouseBlockMeta templates from 3 to 9 (schema docs, table maintenance, partition retention, long-running-query alerts, table provisioning, storage growth report)
- Add document-schema and maintain-tables skills (now 5, all grounded in tools.access)
- Normalize tool version '1.0' to '1.0.0' across all 26 tools for repo consistency

* improvement(clickhouse): enforce explicit safeguards in destructive-op guidance

Address Greptile P1 review: tighten the partition-retention and kill-query
templates/skill so agent guidance requires an explicit retention cutoff and
elapsed-time threshold, lists/verifies targets first, defaults to alert-only,
and never drops a partition or kills a query without confirmation.

* improvement(clickhouse): make long-running-query template alert-only

Address Greptile P1: the kill-query path is a low-level primitive (like
drop_table/delete) and shouldn't carry tool-specific kill policy. Remove the
autonomous-kill suggestion from the scheduled template so no shipped template
steers an agent toward killing a query unattended; alert a human instead. The
kill tool stays available for explicit manual use.
2026-06-27 13:56:30 -07:00
Theodore LiandClaude Opus 4.8 2686f043bc perf(dev): SIM_DEV_MINIMAL_REGISTRY mode to slash local dev-server RAM (#5223)
* perf(dev): SIM_DEV_MINIMAL_REGISTRY mode to slash local dev-server RAM

Adds a dev-only escape hatch (`bun run dev:minimal`, or `dev:full:minimal` with
the realtime server): when SIM_DEV_MINIMAL_REGISTRY=1, a Turbopack/webpack
resolve-alias swaps the two heavy registries for tiny curated variants —
`@/tools/registry` → 2 tools, `@/blocks/registry-maps` → ~20 core blocks. The
shared workspace layout drags the full ~247-tool registry (~2,074 modules) into
every route via providers/utils → tools/params, and the editor/executor pull all
~268 block configs; aliasing both stops Turbopack from compiling those graphs at
all.

To make the blocks alias clean, the heavy block import maps move out of
registry.ts into registry-maps.ts (registry.ts keeps only its accessors,
importing the maps); its public API is unchanged and full builds/tests use the
full maps. The alias is gated on isDev + the flag and is never applied in
production.

Measured (Turbopack dev, authenticated, /logs): peak next-server RSS ~16 GB →
~4.7 GB, compile 4.9 min → ~18 s; the workflow editor route similarly drops to
~5 GB / ~17 s. Only http_request + function_execute and the curated core blocks
work in minimal mode; unset the flag for the full set.

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

* perf(dev): add table block to minimal registry; doc block registration in registry-maps

Adds the Table block to the SIM_DEV_MINIMAL_REGISTRY curated set so the
tables surface works under dev:minimal. Updates the integration skills/rules
and CLAUDE.md to point block registration at blocks/registry-maps.ts (the
BLOCK_REGISTRY / BLOCK_META_REGISTRY maps), reflecting that registry.ts now
holds only the accessor functions.

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

* chore(dev): rename dev:full:minimal → dev:full:minimal-registry

Matches the dev:full:* formatting and makes the suffix self-explanatory —
it is the registry that's minimal, not the dev stack.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 16:41:32 -04:00
Waleed 2554725de2 improvement(execution): stop rewriting execution snapshots on reuse + skip redundant actor lookup (#5242)
* improvement(execution): stop rewriting execution snapshots on reuse + skip redundant actor lookup

- SnapshotService.createSnapshotWithDeduplication: switch the per-execution dedup
  write from onConflictDoUpdate(set state_data) to onConflictDoNothing + a
  conditional select. A (workflowId, stateHash) row is byte-identical by hash, so
  rewriting the full state jsonb every run only churned a dead tuple + TOAST/WAL
  under Postgres MVCC. The reuse path (the common case) now performs no write.
- preprocessExecution: add an optional resolvedActorUserId so a caller that already
  resolved the billing actor upstream can skip the redundant workspace billed-account
  lookup. The ban/usage/rate/archived gates still run against the actor — only the
  resolution is reused, never a gate. The webhook background job passes the
  route-resolved payload.userId.

* fix(webhooks): scope actor reuse to inline execution only

Addresses review: a queued/Trigger.dev webhook can outlive a workspace
billed-account change, so reusing the route-resolved actor there could gate
against a stale account. Set resolvedActorUserId only on the in-process inline
payload (sub-second after resolution); queued and persisted payloads omit it,
so the background pass re-resolves the current billed account. Gates unchanged.

* docs(webhooks): convert inline comments on actor-reuse to TSDoc

* fix(logs): keep snapshot dedup a single atomic upsert (no select race)

Addresses review: the DO NOTHING + follow-up select could fail if cleanup
deletes the conflicting (orphaned, aged) snapshot between the no-op insert and
the select. Revert to one atomic upsert but SET only state_hash, so RETURNING
always yields the row (no race) while the unchanged TOASTed state_data jsonb is
still not rewritten under MVCC — keeping the per-execution write tiny.
2026-06-27 13:10:24 -07:00
Waleed ac95a27dab fix(security): gate credential-set invitation listing to admins and drop token (#5243)
GET /api/credential-sets/[id]/invite listed every invitation row — including
the bearer token — to any org member, matching neither its sibling methods
(POST/DELETE enforce admin/owner) nor the self-scoped /invitations endpoint.
A non-privileged member could harvest a null-email invite token and self-join
the credential set via POST /api/credential-sets/invite/[token].

- Add the admin/owner role gate to GET, matching POST/DELETE on the same route
- Project explicit columns (drop token) so the secret is never returned to the
  management list; the creating admin still receives it via the create response
2026-06-27 12:59:05 -07:00
Waleed 9b66b40aab fix(mcp): pin public IP-literal server URLs to block SSRF redirect bypass (#5244) 2026-06-27 12:57:21 -07:00
Waleed b2d43c1085 fix(copilot): gate post-tool output writes behind write permission (#5241)
* fix(copilot): gate post-tool output writes behind write permission

The Copilot/Mothership executor runs three post-tool output-redirection
sinks (maybeWriteOutputToFile, maybeWriteOutputToTable,
maybeWriteReadCsvToTable) that persist a tool's result into the
workspace. They were gated only on identity (workspaceId + userId), not
on permission. Because function_execute/user_table/read are read-allowed
for execution (absent from WRITE_ACTIONS in tools/server/router.ts), a
read-only collaborator could drive the agent to durably create/overwrite
workspace files and insert/overwrite table rows via output declarations —
a function-level authorization bypass (CWE-862) that the dedicated write
tools correctly reject.

Add a shared denyOutputWriteWithoutWritePermission guard built on the
canonical permissionSatisfies predicate and apply it to all three sinks,
once a write is actually intended, so read-only principals get the same
Permission denied outcome as the dedicated mutation tools.

* fix(copilot): move file output write-permission gate after no-op skip branches

Address Cursor review: in maybeWriteOutputToFile the gate ran before the
sandbox-export skip branch (which returns the result unchanged without
writing), so a read-only caller with a sandbox files payload was denied
even though no workspace write would occur. Move the check to immediately
before writeWorkspaceFileByPath so it only fires when a write is actually
performed.
2026-06-27 12:51:12 -07:00
Waleed 0420feed1d chore(data-drains): remove settings callout and unused InfoNote component (#5235)
* chore(data-drains): remove settings callout and unused InfoNote component

* improvement(data-retention): convert policy editor from modal to full-surface page

Mirror the access-control group-detail pattern: clicking a retention policy
now drills into a full-surface PolicyDetail page (back chip + dirty-gated
header Save/Discard + Remove) instead of an xl modal. Form fields use
SettingsSection (Workspaces / Retention / PII redaction) and the page chrome
matches group-detail exactly. The unsaved-changes confirm stays a small modal.

* improvement(settings): shared save/discard + unsaved-changes guard

Consolidate every editable settings surface onto one stack instead of
per-page custom logic:

- SaveDiscardActions: the canonical dirty-gated Discard+Save chip pair
- useSettingsUnsavedGuard: syncs local dirty into useSettingsDirtyStore (so the
  sidebar section-switch confirm applies) + provides guardBack/UnsavedChangesModal
  for detail sub-views' back chip
- useSettingsBeforeUnload: a single beforeunload in the settings shell

Migrate whitelabeling, sso, access-control group-detail, data-retention, and
secrets-manager (drops its duplicate beforeunload). Deletes the hand-rolled
'Unsaved changes' modals; the leave-confirm standardizes on Keep editing /
Discard. Documents the pattern in the settings rule + add-settings-page skill.

* fix(sso): reset originalFormData on discard/save so dirty state clears

handleDiscard and the post-save cleanup reset formData to DEFAULT but left
originalFormData on the edit snapshot, so hasChanges stayed true after leaving
the form — leaking a stuck-dirty state into the shared settings guard. Reset
originalFormData alongside formData in both leave paths (handleEdit re-seeds
both on re-entry).

* fix(settings): auto-dismiss unsaved-changes modal when page goes clean

useSettingsUnsavedGuard stashed a deferred leave + opened UnsavedChangesModal
when back was pressed while dirty, but never cleared them if isDirty later
became false. Confirming Discard could then run a stale leave with no unsaved
edits. Clear the pending leave and close the modal in the dirty-sync effect
whenever isDirty is false.
2026-06-27 12:34:54 -07:00
Waleed 37962a05b2 fix(file-parsers): guard OOXML parsers against decompression-bomb memory exhaustion (#5239)
* fix(file-parsers): guard OOXML parsers against decompression-bomb memory exhaustion

Pre-inspect the ZIP central directory of xlsx/docx/pptx buffers and reject
archives whose declared expanded size (>1 GiB) or compression ratio (>150x)
exceeds safe bounds, before SheetJS/mammoth/officeparser inflate them. The
existing pipeline only capped the compressed input (100 MB), which does not
bound decompressed size, so a crafted zip bomb could expand to many GB and OOM
the worker.

* fix(file-parsers): fail closed on unverifiable ZIP-shaped OOXML archives

Address review: the guard previously no-opped (fell through to the
decompression library) whenever the central directory could not be parsed,
and findEocdOffset accepted the first backward EOCD signature without checking
it sat at the buffer tail. A crafted archive with a decoy EOCD or an
unsupported directory layout could bypass the size limits.

- findEocdOffset now requires the EOCD comment length to place the record
  exactly at the buffer tail, defeating decoy signatures planted in the trailing
  region.
- assertOoxmlArchiveWithinLimits now fails closed: a ZIP-shaped buffer (local
  file header / EOCD magic) whose central directory cannot be parsed is rejected
  rather than passed through. Genuine non-ZIP inputs (legacy OLE .xls/.doc,
  plaintext) still no-op and defer to the downstream parser.
2026-06-27 12:34:37 -07:00
Waleed b8a197b99c fix(security): cap KB document download size to prevent memory-exhaustion DoS (#5240)
Knowledge-base ingestion downloaded an attacker-controlled external fileUrl
with no byte cap: downloadFileFromUrl defaults maxBytes to MAX_SAFE_INTEGER,
so the streaming reader buffered the entire response into memory uncapped.
An authenticated user could OOM the processing worker by pointing fileUrl at
a server that streams an unbounded body.

Wire the documented 100MB file-size limit (MAX_FILE_SIZE) into the ingestion
download helper. The existing stream limiter aborts the read once the cap is
exceeded and rejects up front on an oversized Content-Length, so the body is
never fully buffered.
2026-06-27 12:22:52 -07:00
Theodore Li 845a6276d9 perf(db): per-role Postgres connection-pool profiles (#5232)
* perf(db): drive Postgres pool size + application_name from per-role profiles

Replace ad-hoc DB_APP_NAME sizing with a per-role profile map keyed by
SIM_DB_ROLE (web/trigger/realtime), defaulting to web. Trigger machines
open a small pool instead of 15 to avoid PgBouncer connection exhaustion.
Also size realtime's separate socketDb pool down to 10.

* fix(db): throw on invalid SIM_DB_ROLE instead of silently using web pools

* fix(db): use Object.hasOwn for SIM_DB_ROLE validation to avoid prototype keys
2026-06-27 15:15:05 -04:00
Waleed 6264cba52f fix(connectors): harden Zendesk connector against SSRF (#5237)
* fix(connectors): harden Zendesk connector against SSRF

Route the Zendesk connector through the SSRF-safe secureFetchWithRetry (DNS-resolve + IP-pin + per-redirect revalidation) instead of the plain fetchWithRetry, and validate the user-supplied subdomain against a strict DNS-label pattern before building the base URL. Matches the GitLab/Sentry/Obsidian/S3 precedent.

* fix(connectors): retry transient DNS failures in secureFetchWithRetry

secureFetchWithValidation throws a validation error before the request when a hostname temporarily fails to resolve. Classify that transient DNS failure as retryable so secureFetchWithRetry mirrors the old fetchWithRetry network-retry behavior, while keeping the deterministic blocked-IP SSRF rejection non-retryable.
2026-06-27 12:10:52 -07:00
Waleed 3c6c6b1515 improvement(webhooks): add trigger-age instrumentation + guard env decryption (#5236)
- Add dispatch-latency / trigger-age instrumentation: capture webhook receipt
  time + Slack x-slack-request-timestamp at the route and log structured
  dispatchLatencyMs + triggerAgeMs before execution, surfacing the pre-execution
  latency that per-block timings cannot see (Slack trigger_id expires at 3s).
- Guard the effective-env fetch in verifyProviderAuth: only fetch+decrypt when
  the handler verifies auth AND the providerConfig references env vars ({{VAR}}),
  avoiding a needless DB read/decrypt on the synchronous pre-ack path. The guard
  scope exactly matches resolveProviderConfigEnvVars, so resolution is identical.
2026-06-27 11:28:49 -07:00
Theodore Li e08fb022e6 perf(trigger): cap concurrency on background DB tasks (#5231)
* perf(trigger): cap concurrency on background DB tasks

* test(trigger): update schedule concurrency assertion to 30
2026-06-27 14:08:17 -04:00
Waleed d9da544a13 feat(triggers): add Twilio SMS, Clerk, incident.io, Rootly, RevenueCat, Loops, and Sentry webhook triggers (#5230)
* feat(triggers): add Twilio SMS, Clerk, incident.io, Rootly, RevenueCat, Loops, Sentry webhook triggers

Adds inbound webhook triggers for seven existing integrations, each verified
against the provider's official webhook docs (event types, payload fields,
signature scheme) and aligned with Sim's trigger conventions.

- Twilio SMS: inbound message + status callback (X-Twilio-Signature, HMAC-SHA1)
- Clerk: user/session/organization lifecycle (Svix)
- incident.io: incident created/updated/status + alert created (Svix)
- Rootly: incident created/updated/resolved + alert created (HMAC-SHA256);
  auto-registers and tears down the webhook via the Rootly API
- RevenueCat: purchase/renewal/cancellation/expiration/product-change
  (Authorization header); auto-registers via the RevenueCat v2 API
- Loops: email lifecycle + campaign/loop/transactional sent (Svix-compatible)
- Sentry: issue/error/issue-alert/metric-alert (Sentry-Hook-Signature, fail-closed)

Clerk, incident.io, Loops, Twilio, and Sentry use manual signing-secret setup
where the provider exposes no clean webhook-management API; Rootly and RevenueCat
auto-provision so the user only supplies an API key.

* fix(triggers): address review — fail-closed auth, Twilio event filtering, user-only secrets

- Twilio: add matchEvent so inbound-SMS and status-callback triggers don't
  cross-fire on a shared webhook URL (inbound = SmsStatus 'received')
- Rootly + RevenueCat: verifyAuth now fails closed (401) when the signing
  secret is absent from provider config (both auto-register, so it is always
  present after deploy) instead of skipping verification
- Mark every user-provided credential field paramVisibility 'user-only'
  (Clerk, incident.io, RevenueCat, Sentry, Twilio) so secrets are never
  exposed as LLM-visible trigger params
- Sentry: correct metric_alert web_url description per docs

* fix(triggers): key Twilio status callbacks by SID + status for idempotency

Twilio sends multiple delivery callbacks per message (sent -> delivered -> ...)
sharing one MessageSid; keying idempotency on the SID alone dropped every status
after the first. Status callbacks now key on SID + delivery status so each state
is distinct (while still deduping Twilio's retries of the same status); inbound
messages still key by SID since they fire once.

* fix(triggers): require a positive signal in Twilio matchEvent routing

Previously an empty MessageStatus/SmsStatus was treated as a status callback,
so an ambiguous or partial payload could match twilio_sms_status (or skip
twilio_sms_received). Both triggers now require a positive signal — inbound
needs status 'received', status callbacks need a non-'received' status — so a
payload missing both fields matches neither rather than misrouting.

* fix(triggers): attach Twilio auth fields to both triggers so status callbacks are verified

The Account SID / Auth Token fields were only on the primary trigger, so
deploying twilio_sms_status alone never captured authToken into providerConfig
and signature verification was silently skipped. Following the incident.io /
Rootly / RevenueCat pattern, the auth fields are now added to each trigger
conditioned on its own selectedTriggerId; the shared inner subBlock IDs keep the
values persisted across trigger types.

* fix(triggers): mark shared block-level API keys user-only to protect trigger secrets

The trigger credential fields (RevenueCat/Rootly apiKey, Twilio authToken) are
user-only, but the same-id tool-level block fields were not, so the stored
secret stayed reachable as an LLM-visible block parameter. Mark those block
credential fields paramVisibility 'user-only' too (matching instantly.ts) so the
secret is user-only on every path. accountSid is an identifier, not a secret, so
it is left as-is.
2026-06-26 18:50:32 -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 c7eda5b217 feat(rich-editor): rich markdown field + @ mentions for skill & deploy modals (#5215)
* feat(rich-editor): rich markdown field + @ mentions for skill & deploy modals

- Add controlled, file-less RichMarkdownField (sibling of the file editor) used for
  skill Content and deploy version descriptions; placeholder/typography match chip fields
- Add @-mention menu (TipTap suggestion) inserting portable [label](sim:kind/id) links;
  wired into the field and the file viewer via a shared useEditorMentions hook
- Extract a shared suggestion-popup renderer + menu chrome (slash + mention)
- Fix false dirty-on-open: normalize the editor's dirty baseline to canonical markdown
- Always show the deployment version number (v3 · name) so named versions keep a short ref
- Skill import: drop the paste box (Create-tab editor auto-destructures a pasted SKILL.md),
  reorder GitHub → Upload

* fix(rich-editor): address review feedback on modal field

- RichMarkdownField reports the original value when the doc matches its canonical
  form, so a non-canonical input never reads as a false unsaved change (skill +
  version description modals)
- Add sim: mention link navigation (Cmd/Ctrl-click) to the modal field
- versions: keep the v{n} fallback as the rename guard/seed so re-submitting the
  displayed token is a no-op (no redundant "v3 · v3"); document the clear-name no-op
- Clarify the lazy query-gating comment in useMarkdownMentions

* fix(skills): re-seed Content editor when initialValues changes

Bump the field's remount key in the reset guard so the seed-once rich editor
re-seeds when content is reset from a changed initialValues (same skill id keeps
the React key otherwise stable), keeping the editor and saved value in sync.

* feat(rich-editor): render mentions as icon chips + menu/limit polish

- Render @ mentions as an inline chip node (entity icon + label) instead of a
  blue link; still serializes to the portable [label](sim:kind/id) markdown so
  it round-trips and stays agent-readable (shared mentionIcon resolver)
- Cap the mention/slash menu height + width and scroll it, matching the chat menu
- Give the version description editor more height; lift the 2000-char limit to a
  high anti-abuse cap (client + contract) and drop the visible counter

* fix(rich-editor): make suggestion menus scrollable inside modals

- Mount the slash/@ menu popup inside the host dialog (when present) instead of
  document.body: Radix's scroll-lock blocks wheel events outside the dialog
  subtree, so a body-level popup couldn't scroll in a modal. position:fixed keeps
  it viewport-positioned (the modal centers via flex, no transform) so it isn't clipped
- Fix the invalid max-w arbitrary value (calc needs spaces) that left the menu uncapped
- Match the version-description editor's dynamic-import loading height to the field
  so the modal doesn't grow when the chunk loads

* fix(rich-editor): escape bracketed mention labels + disable images in field editors

- Escape/unescape `[`/`]` in mention labels so an entity named e.g. `data[1].csv`
  round-trips into a chip instead of degrading to a plain link
- Hide the `/Image` command where image upload isn't wired (the skill + version
  description field editors), so images can't be inserted there; the file viewer
  keeps image support

* fix(rich-editor): keep suggestion keyboard nav working after async items load

The suggestion plugin captures the list's onKeyDown handle via ReactRenderer.ref
once at mount. The mention list's items arrive asynchronously from the workspace
store, so the captured handle closed over an empty `flat` and returned false for
arrow/enter — letting the editor move the caret instead of navigating the menu.
Read live values through a ref so the mount-time handle always sees current
items/activeIndex. Hardened the slash list the same way.

* test(rich-editor): cover suggestion keyboard nav through ReactRenderer; drop inline comments

Adds a test that drives the real ReactRenderer path the suggestion plugin uses:
the captured onKeyDown handle returns false while the store is empty and true
once async workspace items land, and arrow+enter select the right item. Removes
the explanatory inline comments from the two imperative handles.

* fix(rich-editor): suggestion menus keep arrow keys when a divider is adjacent

The leaf-selection keymap (ArrowUp/Down selects an adjacent divider/image) runs at
priority 1000, above the suggestion plugins, so it stole ArrowDown to select the
next horizontal rule instead of moving the open @/ menu selection. It now yields
while a mention or slash menu is active, detected via the plugins' exported keys.

* feat(rich-editor): Tab accepts a suggestion; unify list keyboard nav; match chip styling

- Extract useSuggestionKeyboard: one hook owns the @/ menus' active-row state,
  scroll-into-view, and arrow/enter/tab handling (removes the duplication between
  the two list components)
- Tab now accepts the active item like Enter, matching the chat composer
- Render the mention chip like the chat input's mention token: borderless inline
  icon + label (no pill), 12px icon with brand color via getBareIconStyle, so the
  styling is consistent across surfaces

* fix(rich-editor): harden editor edge cases found in full audit

- Skill paste: only auto-destructure on a real YAML name key, so a stray `---`
  break or heading snippet no longer overwrites all three fields (parseSkillMarkdown
  reports nameFromFrontmatter)
- Skill modal: reset by skill id, not object identity, so a background refetch of
  the open skill can't clobber in-progress edits
- Field editor: claim Mod+K (inline link editor wins over global search) and
  swallow file drops so the browser doesn't navigate away from the modal
- File editor: swallow non-image file drops (same navigation guard)
- Frontmatter: a leading `---` thematic break (e.g. a changelog) is no longer
  mistaken for frontmatter and hidden from the editor
- Mention chip: renderText emits the portable link so copying a chip into a
  plain-text target (e.g. chat) pastes back as a mention
- Suggestion nav: clamp a one-frame stale active index on Enter/Tab

* style(rich-editor): selected link reads as normal text, not standout blue

Follows the standard MD-editor convention (Linear, Slack): a highlighted link
takes the primary text color so the selection stays legible, instead of keeping
its blue on the selection highlight. Scoped to selected links only — no effect on
unselected links, regular text, the selection background, or any other surface.

* fix(rich-editor): guard async suggestion + generate lifecycles against teardown

- Suggestion onStart can fire after the editor is destroyed (the update awaits
  items()), throwing on its now-gone view/storage — e.g. a modal closing while the
  menu opens. Bail when the editor is destroyed; optional-chain mention storage.
  This also removes the unhandled rejections the headless keymap test surfaced.
- Generate version description: thread an AbortSignal so closing the modal
  mid-stream aborts the diff fetches + SSE read instead of streaming into a gone
  component.

* refactor(rich-editor): fold inline comments into TSDoc; display-only chip; polish

- Convert the editor's inline `//` comments to TSDoc on the nearest declaration and
  drop the self-explanatory ones (no logic change)
- Mention chip is now display-only (icon + label), matching the chat input exactly:
  removes select-none (so a range selection highlights the label), the cursor-pointer
  over-promise, and the cmd-click nav that could route away from a modal mid-edit
- Don't log a deliberate generate-abort as an error
- Selected strike-through text reads in the primary color so the selection is uniform

* fix(rich-editor): clean selection for the mention chip

The chip is an inline atom, so a range selection now highlights it as a whole unit
(the prior select-none left it an un-highlighted gap). A direct click selects it
with a subtle fill instead of the block-leaf outline ring meant for dividers/images.

* feat(rich-editor): Cmd/Ctrl-click a mention to its resource in the file viewer

Threads a `navigable` flag through the mention storage: the file viewer opts in so
a chip routes to its file/table/workflow/etc., while modal fields stay inert so a
click can't navigate away from an unsaved edit. Styling is identical either way.

* fix(rich-editor): icon fallback for removed integrations; smoother divider nav

- A mention to a since-removed integration falls back to a generic icon so the chip
  is never icon-less (indistinguishable from prose)
- Arrowing from a selected divider/image to an adjacent one selects it directly
  instead of stopping on the gap cursor between them, so stepping through a run of
  dividers is one press each

* fix(rich-editor): integration mentions are display-only; robust chip selection

- An integration mention's id is a block type (gmail_v2), not a routable resource —
  /integrations/[block] expects a slug and a type maps to zero-or-many credentials —
  so it no longer links to a 404. The chip only shows a pointer/navigates on kinds
  that resolve to a real page.
- Scope the block-leaf selection ring off the mention chip robustly (covers a
  node-view wrapper via :has), so a selected chip shows a subtle fill, not the outline.

* feat(icypeas): update brand icon and bgColor; regenerate integration docs

* fix(rich-editor): remove the misfiring chip selection fill (full-width gray band)

The :has fill could paint a full-width band; drop it. A selected chip just skips the
block-leaf outline ring and uses the same native text-selection highlight as the prose.

* feat(rich-editor): show an "Uploading…" toast while an image uploads

A persistent progress toast appears per image during upload and is dismissed once
it settles, when the upload hook's "Uploaded"/"Failed" toast takes over — previously
nothing showed until the upload finished.

* refactor(rich-editor): drop inline comments (TSDoc on the declarations instead)

Fold the image-upload toast note into the insert function's TSDoc and remove the
remaining inline // comments.

* refactor(rich-editor): cleanup + simplify pass over the markdown editor

- Delete dead parseSimHref (+ barrel/test); mentions parse via the node tokenizer
- Extract serializeMarkdownDocument — one canonical serialize pipeline shared by the
  dirty-check baseline and the round-trip-safety probe (was inlined in both)
- Extract selectLeafAcross — shared tail of the two arrow leaf-selection handlers
- Reset the suggestion active-row during render (prevX idiom) instead of an effect
- Inline the skill modal's trivial hasChanges (drop the useMemo)
- image.tsx: cn() over a template-literal className

* refactor(rich-editor): share the suggestion-list shell and link-URL editor

- Extract SuggestionList: the grouped-list surface, empty state, listbox/option a11y
  structure, and active-row/hover/select wiring shared by the @ and / menus. Each menu
  keeps only its own grouping + itemKey/renderItem.
- Extract link-editing (LinkUrlInput + applyLink): the inline link field and the
  normalize→extendMarkRange→set/unset commit logic, shared by the bubble menu and the
  link hover card.

* improvement(settings): align access-control detail UI + nav-driven docs link

- Move permission-group Save/Discard into the detail header (matching
  secrets/whitelabeling) and delete the one-off sticky 'Unsaved changes' bar
- Convert the Platform and Blocks config tabs to SettingsSection (drop the
  custom multi-column masonry + hand-rolled section labels); add an optional
  far-right action slot to SettingsSection for the per-section Select All
- Replace the file-share auth-mode checkboxes with a multi-select ChipDropdown
- Normalize per-tab spacing to gap-7, align the expand-chevron token to
  --text-icon, and match the list-row arrow size to the integrations precedent
- Add a nav docsLink surfaced as a header 'Docs' ChipLink by SettingsPanel,
  wired for the six enterprise settings pages

* feat(rich-editor): copy-link button shows a checkmark on copy

Use the shared useCopyToClipboard hook so the link hover card's Copy button swaps to a
Check for ~2s after copying, matching the rest of the platform.

* fix(rich-editor): RichMarkdownField falls back to raw text for lossy markdown

Mirror the file editor's safety gate: decide once from the initial value via
isRoundTripSafe — round-trip-safe content opens in the WYSIWYG editor, while lossy
markdown (raw HTML, footnotes, comments) edits as raw text, so an edit can't silently
drop those constructs.

* feat(rich-editor): divider/leaf editing — backspace, select-all, gap cursor

- Backspace at the start of an empty block whose previous sibling is a divider/image removes the
  blank line (instead of deleting the leaf) and selects the divider above; a non-empty block selects
  the leaf so a second Backspace deletes it (highlight-before-delete).
- Select-all (and any range selection) now visibly highlights dividers/images, which the native text
  highlight skips because leaves carry no text — via a decoration that paints a selection band.
- The gap cursor between two adjacent leaves no longer draws its stray caret (matching Linear); the
  position stays functional. Leading/trailing gap cursors keep their caret.
- Unit tests for the backspace + select-all behavior.

* refactor(rich-editor): decouple headless bundle, fix linked-image round-trip, a11y

- Split mention-node into the schema-only `MarkdownMention` (mention-node.ts, no React/registry)
  and the live `MentionChip` node view (mention-chip.tsx); move the live factory to
  editor-extensions.ts and inject node views via DI. The headless round-trip path
  (markdown-parse/normalize-content/round-trip-safety) no longer pulls the 269-block registry —
  it now bundles for the browser with zero node-builtin deps.
- A sized + linked image serializes as `[![alt](src)](href)` (dropping the unrepresentable size)
  instead of `[<img>](href)`, which the tokenizer can't reparse — the link is preserved, no silent
  data loss. Also escape the href title symmetrically.
- Wire the suggestion menus as an ARIA combobox: while open, the editor gets
  aria-haspopup/expanded/controls and an aria-activedescendant tracking the active option, so screen
  readers announce it; cleared on close. Empty state is a role=status live region.

* fix(rich-editor): exempt an active @-mention query from the per-group cap

The per-group MAX_PER_GROUP limit is meant to keep the unfiltered menu from flooding; applying it
while a query is active hid matches past the eighth in a category, so search couldn't reach them.
Cap only when there's no query. Adds a regression test (12 matches shown when searching).

* fix(rich-editor): mention icon fallback + typed sim-link input rule

- mentionIcon never returns undefined: an empty/unrecognized kind (schema default '', or a future
  kind on a sim: link) falls back to a generic icon instead of crashing the chip's render. Adds tests.
- Add a mention input rule so typing `[label](sim:kind/id)` becomes a chip on the closing paren —
  matching the paste/load path (the tokenizer), which previously left typed syntax as literal text.
  A plain InputRule (full-range replace) is used; nodeInputRule would keep the surrounding brackets.

* fix(rich-editor): raw-fallback paste hook + bound the filtered mention list

- RawMarkdownField now honors onPasteText (e.g. skill SKILL.md destructuring), so a full-document
  paste is intercepted in the raw fallback too, not only the WYSIWYG path.
- Bound the @-mention list while filtering (MAX_WHEN_FILTERED) so lifting the per-group cap for search
  can't render thousands of rows in the non-virtualized menu on a broad query; search still reaches
  deep matches well before the bound. Adds a test.
- Tighten an extensions.ts doc comment (the headless path omits the registry + node-view construction,
  not React itself).
2026-06-26 17:44:24 -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
Theodore Li a33c173146 fix(copilot): strip hosted apiKey on type-less edit ops + guard hosting.enabled (#5220)
* fix(copilot): strip hosted apiKey on type-less edit ops + guard hosting.enabled

The hosted-apiKey strip in preValidateCredentialInputs was gated on op.params.type, but edit ops omit type (they carry only changed inputs). An apiKey-only edit on a hosted-tool block therefore skipped both the model and tool strip paths, so a copilot-authored key persisted and disabled hosted-key injection.

Resolve the block type from workflowState when the op omits it, so type-less edits run through the strip. Also guard tool.hosting.enabled() in try/catch like the tool selector. Add a regression test mirroring the observed failure (edit op with only apiKey, type+provider resolved from workflow state).

* improvement(copilot): consolidate hosted-apiKey workflowState reads, gate off-hosted

Quality cleanup (no behavior change): the main loop read workflowState.blocks[id] three times (block type, model fallback, toolParams merge). Collapse to one existingBlock lookup + one buildSubBlockValues; derive modelValue from the merged toolParams (drops the model-fallback ladder); gate the reconstruction on isHosted so buildSubBlockValues + spreads no longer run off-hosted or for blocks the collectors skip. Add an asRecord helper to cut repeated casts.

* fix(copilot): resolve hosted-key strip against same-batch state; strip on enabled throw

Addresses review on #5220:
- Batch-aware block state: a later type-less edit now sees type/provider changed by an earlier op in the same edit_workflow request (was reading the stale initial snapshot), so a key can't survive on a block an earlier op just made hosted.
- hosting.enabled() throwing now fails toward treating the key as managed (strip) instead of preserving it, since the hosting state is unknown on throw.

* fix(copilot): unify hosted-key strip resolution across top-level and nested blocks

Greptile flagged that the batch/snapshot state reconstruction was applied only to top-level blocks; nested loop/parallel children still used raw childInputs, so a same-batch provider/model change on a nested child followed by a type-less apiKey edit could leave the key. Route both paths through one collectForBlock helper keyed by the block's own id (incl. nested children), so they share batch accumulation + snapshot enrichment and can't drift. Test added for the nested same-batch case.

* fix(copilot): recurse nestedNodes so grandchild hosted keys are stripped

The collection loop and the strip/credential-removal loops only handled the first nestedNodes level, but the apply path processes nestedNodes recursively (loop/parallel children can themselves contain nestedNodes). A hosted key on a grandchild (e.g. loop-in-loop) survived. Collection now walks the nestedNodes tree recursively; the strip/removal loops locate a descendant's inputs at any depth via findNestedInputs. Test added for a two-level-deep grandchild.

* fix(copilot): decide hosted-key strip against final batch state (two-pass)

Forward-only accumulation missed reverse order (apiKey set in an early op, block made hosted by a later op) and let an earlier bogus/empty type poison snapshot fallback. Replace it with a two-pass approach: pass 1 folds every op (and nested descendant) into each block's FINAL effective type+values for the batch; pass 2 strips the managed fields each op sets, judged against that final state. Order-independent, any nesting depth, and empty/invalid types no longer block fallback (validType guard). Tests added for reverse order and bogus-type cases.

* fix(copilot): tool selector throw falls back to access tools (fail toward strip)

Symmetric with the enabled-throw fix: when tools.config.tool throws on partial params, scan all access tools instead of returning, so a hosted key can't slip through. Test added.

* fix(copilot): only fold registry-known types into final batch state

Pass 1 recorded any non-empty type into finalType, but apply skips type changes to unknown types (keeps the existing block). An unknown type on an earlier op could poison a later type-less apiKey edit. Only advance finalType to a getBlock-resolvable type so the fallback matches what apply persists. Test covers empty and unknown types.
2026-06-26 12:56:17 -07:00
Waleed 2fa3dd65bc fix(db): retry the migration connection on transient slot exhaustion (#5226)
* ci(migrations): skip db:migrate on merges that change no migration files

Every push to main/staging ran db:migrate against the production/staging
database even when the merge changed no schema, so a no-op migration would dial
the DB and fail whenever it was at its connection limit (53300, slots reserved
for SUPERUSER) — red-X'ing UI-only merges.

Add a detect-migrations job (dorny/paths-filter on packages/db/migrations/**)
and pass the result into the reusable migrations workflow, which now skips the
apply step when no migration files changed. The migrate job still runs so
downstream build/deploy jobs that need it are never skipped, and the flag
defaults to 'true' so manual dispatch and any unknown value always apply
migrations — the gate only ever skips a provably-empty change.

* fix(db): retry the migration connection on transient slot exhaustion

The migration opens its session on the first query (the advisory-lock
acquire). When the deploy database briefly exhausts every non-superuser
connection slot at peak, that connect fails with 53300 ("remaining connection
slots are reserved for roles with the SUPERUSER attribute") and the whole
deploy's migrate step errors out — even when the spike clears within seconds.

Add a bounded connectWithRetry() before acquiring the lock that retries 53300,
the 08xxx connection_exception class, and the driver's transport errors with
backoff (10 attempts, ~90s ceiling). Non-transient errors (auth, bad config)
still fail fast. The migration is a single short-lived session, so waiting out
a transient spike is far safer than failing the deploy.

* ci: drop the migration paths-filter gate (out of scope)

Revert the detect-migrations gate carried over from the closed CI PR; we are
fixing the connection failure at its source (migrate.ts connection retry)
rather than gating db:migrate, which the reviewers correctly noted could leave
a previously-merged migration unapplied after a failed deploy.
2026-06-26 12:21:11 -07:00
Waleed 02d254e267 refactor(emcn): consolidate date pickers onto the chip Calendar (range support + retire legacy DatePicker) (#5222)
* improvement(knowledge): use canonical ChipDatePicker in document tags modal

The document tags modal used the legacy DatePicker primitive for date-typed
tag values while the rest of the knowledge base date inputs use the canonical
ChipDatePicker. Swap both usages (edit + create) to ChipDatePicker (same
YYYY-MM-DD value contract, full-width to match sibling fields) so the chrome
matches the chip design system, and align a tag-row value label to the caption
text size used by its sibling.

* feat(emcn): add range mode to ChipDatePicker via the canonical Calendar

ChipDatePicker was single-date only, so date-range surfaces had to fall back
to the legacy DatePicker primitive. Add range support as a discriminated
mode on the existing canonical Calendar (not a parallel component): a
start/end selection staged behind Clear/Cancel/Apply with optional
time-of-day inputs, built from the chip family (CalendarDayCell, chipVariants,
ChipTimePicker) and fully tokenized. ChipDatePicker gains a matching
mode='range' that renders it behind the same chip trigger.

Extract the range-bounds serialization into a pure, unit-tested helper, and
remove the dead logs-toolbar directory (LogsToolbar/AutocompleteSearch had no
consumers; the logs page renders its toolbar inline).

* refactor(emcn): retire legacy DatePicker; migrate all consumers to the chip calendar

Removes the parallel DatePicker component (its own dual-month CalendarMonth,
hardcoded colors, non-chip chrome) so the chip Calendar is the single date
surface. Migrates every consumer:

- tables row modal -> ChipDatePicker (single)
- tables inline grid editor, logs filter, ee audit-logs filter -> canonical
  Calendar inside an emcn Popover (the same headless/anchored pattern
  DatePicker rendered internally; single for the grid cell, range+time for the
  log filters)
- playground -> ChipDatePicker showcase (single, range, range+time)

The range bounds keep the exact YYYY-MM-DD / YYYY-MM-DDTHH:mm wire format, so
the log filters' parsing is unchanged. Calendar is imported via its component
path (the emcn barrel already exports a Calendar icon).

* fix(emcn): resync range calendar draft state when bound props change

Cursor Bugbot: RangeCalendarView seeded its staged selection from props only
once. Closing the popover unmounts it (so a fresh open re-seeds), but if the
bound startDate/endDate change while it stays mounted the grid could linger on
a stale draft. Add a render-phase reset keyed on the bound props.

* refactor(emcn): reset range calendar via key instead of render-phase resync

Replaces the imperative render-phase state reset with React's idiomatic
key-based reset: the range view is keyed on its committed bounds in the
Calendar dispatcher, so a newly applied range remounts it with fresh draft
state. Cleaner and fully encapsulated — consumers need no special handling.
Move the rationale into TSDoc (no inline comment).

* fix(logs): keep the date-range popover anchor from blocking the time-range select

The custom-range Calendar is anchored with an always-rendered PopoverAnchor
overlaying the time-range combobox (absolute inset-0). Without
pointer-events-none it would intercept clicks meant for the select, leaving it
unclickable. The anchor is only a positioning reference, so disable its pointer
events on both the logs and audit-logs filters.
2026-06-26 11:16:39 -07:00
Waleed 365d8be02b fix(knowledge): document tag filter matches case-insensitively and by calendar day (#5221)
The Knowledge Base document list applied tag filters with case-sensitive text
equality and compared date tags against a midnight-UTC timestamp, so text
filters missed on any casing difference and date eq never matched a stored
timestamp — both silently returned empty results.

Align the document-list filter with the knowledge search filter semantics:
text eq/neq are now case-insensitive (LOWER) and date comparisons run on the
calendar day (::date). Extract the predicate builder into its own
single-responsibility module with unit coverage, and tidy the filter popover's
secondary labels to the caption text size for chip-design consistency.
2026-06-26 10:18:23 -07:00
Waleed a1d5870681 feat(settings): unify all settings pages under a shared SettingsPanel layout (#5219)
* feat(settings): add SettingsPanel layout with nav-driven page titles

Introduce a SettingsPanel scaffold that owns the standard settings chrome
(fixed header bar with right-aligned actions, scroll region, centered content
column) and renders a consistent page title + description pulled from the
active section's navigation metadata. Adds a description to every nav item and
a SettingsSectionProvider in the shell so the title is required-by-default
without per-page wiring.

Migrate the account/subscription pages (general, secrets, teammates,
team-management, billing) to SettingsPanel, removing their hand-rolled shells
and title blocks.

* feat(settings): migrate all settings pages to SettingsPanel + bake in search

Extend SettingsPanel with a `search` prop (canonical search field with
optional anti-autofill hardening) so the repeated per-page search input is
owned by the layout. Migrate every settings page — account, subscription,
tools, system, enterprise, and superuser — onto SettingsPanel so each renders
a consistent nav-driven title + description, header actions, and search with
zero hand-rolled shell. Drill-down detail sub-views (MCP server, workflow MCP
server, credential set, permission group) keep their own back-button chrome.

Normalize all nav descriptions to a consistent voice and length.

* refactor(settings): drop unnecessary search anti-autofill; document SettingsPanel

Remove the preventAutofill prop from SettingsPanel — the shared search field
no longer needs the read-only-until-focus hack, so secrets uses the plain
search like every other page. Pre-existing honeypot inputs are untouched.

Add .claude/rules/sim-settings-pages.md (auto-scoped to settings pages) and a
settings-page skill documenting the SettingsPanel convention + add/audit
procedure.

* refactor(settings): extract SettingsEmptyState + RowActionsMenu, dedupe usages

Encapsulate two more repeated settings patterns into shared components:
- SettingsEmptyState — the muted empty/no-results/gate message (fill | inline),
  replacing ~42 hand-rolled status divs and normalizing stragglers that used
  text-small / text-tertiary back to the canonical text-muted + text-sm.
- RowActionsMenu — the trailing '...' row-actions dropdown, replacing ~11
  per-page DropdownMenu+MoreHorizontal blocks with a props-driven actions list.

Pure presentational refactor: every message, action, handler, disabled, and
destructive flag preserved (verified by diff review). Document both in
.claude/rules/sim-settings-pages.md.

* fix(settings): set autoComplete=off on the shared settings search field

Keeps browsers from offering saved-credential autofill in a filter box — the
lightweight standard guard, distinct from the removed read-only/preventAutofill
machinery. Matters most on the secrets page.

* chore(settings): strip non-TSDoc inline comments from touched files

Remove JSX label comments, section/explanatory // comments, separator block
comments, and copilot's commented-out MCP scaffold across the migrated files,
per the repo's no-non-TSDoc-comments standard. TSDoc and functional directives
kept. Comment-only deletions — no code or behavior changed.

* chore(skills): rename settings-page skill to add-settings-page

Aligns with the verb-prefixed naming of the other skills (add-integration, etc.).

* feat(icons): Thrive icon-only black mark on white

Drop the wordmark from ThriveIcon (tighten viewBox to the logo bounds), set the
mark to pure black; the block bgColor is already white. Synced the docs icon copy.
2026-06-25 18:30:41 -07:00
Waleed ca4e07baf3 chore(deps): bump undici to 7.28.0 and nodemailer to 9.0.1 (#5218)
* chore(deps): bump undici to 7.28.0 and nodemailer to 9.0.1

* chore(deps): dedupe cheerio and e2b transitive undici to 7.28.0
2026-06-25 17:33:06 -07:00
Waleed 7ba0e238cb feat(access-control): page-based permission groups, tool-level deny-list, settings row-action consistency (#5216)
* feat(access-control): page-based permission groups, tool-level deny-list, settings row-action consistency

- Replace the cramped configure modal with a full-surface tabbed Access Control page (General/Model Providers/Blocks/Platform) with a sticky save bar
- Add deniedTools denylist to permission groups: deny individual tools within an allowed integration; enforced at the universal executeTool chokepoint via ToolNotAllowedError, and hidden from the operation dropdown for governed users
- Add per-section Select/Deselect All on the Blocks tab and expandable per-tool deny rows (mirrors Providers->Models)
- Standardize every settings list row on the canonical "..." DropdownMenu (custom-tools, mcp, workflow-mcp-servers, api-keys, secrets, credential-sets) and align badges (ChipTag), avatars (MemberAvatar), inputs (ChipInput), and the mothership env picker (ChipSelect)

* fix(access-control): don't leave the detail view when an unsaved-changes save fails

The unsaved-changes dialog's Save action navigated back unconditionally after handleSaveConfig, but that helper swallows mutation errors — so a failed save still exited the view and silently dropped the edits. handleSaveConfig now returns success, and the dialog only closes + navigates back when the save actually succeeded.

* fix(access-control): prune deniedTools for blocks that get disabled

deniedTools only matters while a block is allowed, but toggleIntegration/setBlocksAllowed left a disabled block's denied tools in the config. Disabling then re-enabling an integration would silently re-apply the old per-tool denials. Both handlers now prune deniedTools to the set of allowed blocks, keeping the invariant that deniedTools only holds tools of currently-allowed integrations.

* fix(access-control): attribute denied tools to all exposing blocks when pruning

A tool id can appear in more than one block's tools.access. The single tool->block map meant pruneDeniedTools (and the per-block denied count) attributed a shared tool to only one block, so disabling that block could drop a denial while the tool was still exposed by another allowed block. Tools now map to all exposing block types; a denial is pruned only when no allowed block exposes the tool, and the per-block count is derived from each block's own tool list.

* fix(access-control): scope Platform Select/Deselect All to the search filter

The Platform tab's bulk Select/Deselect All toggled every feature regardless of the active search, unlike the Blocks tab which scopes its per-section toggle to the filtered view. Both the all-visible check and the bulk update now operate on filteredPlatformFeatures for consistent behavior while searching.

* fix(access-control): scope Model Providers Select/Deselect All to the search filter

Like the Platform fix, the Providers tab's bulk action toggled every provider via allProviderIds regardless of the active search. Added setProvidersAllowed (mirroring setBlocksAllowed) so the bulk toggle and its label operate on filteredProviders, keeping all three tabs (Blocks/Platform/Providers) consistent while searching.

* fix(access-control): don't seed a denied operation as a block's default

The operation dropdown hides denied tools from the picker, but defaultOptionValue returned the block's defaultValue without checking deniedOperationIds, so a new block could start on an operation the user isn't allowed to run. It now falls back to the first allowed option when the configured default is denied. Existing stored operation values are intentionally left untouched (auto-rewriting a user's saved block would be destructive; the server remains the authoritative gate).

* chore(access-control): prefer TSDoc over inline comments

Convert declaration-level rationale comments to TSDoc (/** */) and trim redundant/verbose inline comments added during review, per the project's TSDoc convention.
2026-06-25 16:32:51 -07:00
Will ChenandClaude Opus 4.8 6355c8e699 improvement(docs): Ask AI chat grounded in the docs vector store (#5172)
* docs: Ask AI chat grounded in the docs vector store

Adds an Ask AI chat to the docs site. A floating launcher opens a chat panel
backed by the Vercel AI SDK (OpenAI provider, OPENAI_API_KEY from the
environment). A searchDocs tool runs locale-scoped vector/keyword search over
the existing docs embeddings so answers cite real pages.

The public endpoint is hardened: per-request size/token/step caps, message
sanitization (no client-injected tool results or system prompts), origin
checks, and a per-IP rate limit. Non-English retrieval uses keyword search;
English vector search applies a similarity threshold.

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

* docs: harden Ask AI retrieval + fix stale loading state

- searchDocs: wrap the keyword query in try/catch too, so each retrieval path
  (keyword, vector) is independent best-effort
- ask-ai: gate the loading ellipsis to the in-progress (last) message so older
  empty bubbles don't re-show it while a later request streams

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 16:32:29 -07:00
Theodore Li 5db5963fc1 fix(copilot): strip platform-managed apiKey on hosted-tool blocks in edit_workflow (#5217)
preValidateCredentialInputs only stripped apiKey for hosted LLM models (getHostedModels). Agent-authored apiKey on hosted-tool blocks (Fal video/image, etc.) slipped through, disabling hosted-key injection and misleading the agent into telling users to bring a key.

Generalize the strip to key off tool.hosting — the same canonical signal injectHostedKeyIfNeeded uses at execution. Resolve the block's active tool via its tools.config.tool selector, honor the per-provider enabled gate, and strip the exact hosting.apiKeyParam field (no hardcoded 'apiKey' assumption). Surfaces the existing non-fatal note to the agent. Self-hosted and non-hosted providers untouched.
2026-06-25 19:03:30 -04: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 Ganesan 7640af0f71 improvement(mothership): add workflow lint for custom tool/skills/mcp tool additions to agent block (#5199)
* improvement(workflow-linter): added custom tool validation to workflow linter

* fix(comments): address pr comments

* improvement(validation): ensure type is known
2026-06-25 14:47:58 -07:00