Commit Graph
5383 Commits
Author SHA1 Message Date
Waleed b486aba07e fix(pricing): route Talk to sales CTA to demo request form instead of signup (#5602)
- pricing page's enterprise card was labeled "Talk to sales" but linked to
  /signup for every card, sending visitors to self-serve signup instead of
  the demo-request flow every other "Contact sales" CTA on the site uses
- resolveCta now keys off the CTA's sales intent to pick the right href
- extracted the /signup and /demo route literals (previously hardcoded
  independently in 6 files) into a single shared apps/sim/app/(landing)/constants.ts
  so no CTA can drift to the wrong destination again
- hoisted the static per-column comparison sections out of render and
  deduped the annual-discount price math in pricing-plans.tsx
2026-07-11 12:40:07 -07:00
Waleed 7c2de1d426 feat(providers): add xAI to hosted key rotation pool (#5574)
* feat(providers): add xAI to hosted key rotation pool

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

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

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

* fix(byok): add xai settings UI row

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* fix(canvas): drop unused ResizeObserver in OverflowSpan

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* Add slack trigger

* fix channel picker in slack trigger

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* chore(slack): reconcile staging merge

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

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

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

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

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

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

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

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

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

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 19:43:52 -04:00
f60d41af40 feat(landing): enterprise page redesign with platform-loop hero and feature graphics (#5535)
* feat(landing): add enterprise link to navbar and footer

* feat(landing): redesign enterprise page with platform-loop hero and feature graphics

- New enterprise hero with animated platform loop (sidebar + home stage) and hero background
- Nine redesigned feature tiles under enterprise/components/feature-graphics with a shared monochrome design vocabulary, per-tile tones, and CSS-module animations
- Shared hero-header component and landing-layout constants; hero/platform/solutions pages aligned to the same layout system

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

* chore(dev): allow 127.0.0.1 dev origins and skip root redirect in dev

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

* chore(skills): install make-interfaces-feel-better skill

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

* cleanup(landing): enterprise redesign polish + asset compression

- Fix ChipLink chrome overrides in navbar/mobile-nav to use variant='border'
- Dedup elapsed-time reveal effects in enterprise-home-stage into one hook
- Remove unused FeatureGraphicNode component and barrel export
- Convert enterprise-hero-background.png (8.9MB lossless) to WebP q90 (1.07MB, near-lossless)
- Fix team-avatar-1/2/3.png mislabeling (actual JPEG bytes) to correct .jpg extension

* fix(landing): keep feature-tile graphics uncropped at small breakpoints

Feature tiles shrank their min-height on small screens while the tallest
vignettes (audit ledger, staging panel) still needed ~300px of visual slot,
cropping their tops against the slot's overflow-hidden. Tiles now hold one
440px min-height everywhere. The 3-up grid also collapses to two columns
below lg instead of md, and the fixed-width canvases (access graph,
standards seal, ops router) scale down on the narrow two-column band and
the 3-up row just past lg so outer labels and chips are never cut off.

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

* fix(landing): regroup enterprise feature cards into 4/4/2/2 at two-column band

Merge the four enterprise feature sections into one EnterpriseFeatureGrid so
the 640-1023px two-column layout fills 4/4/2/2 with no orphan empty cell;
lg+ and <sm layouts are unchanged via sm:max-lg order utilities.

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

---------

Co-authored-by: andresdjasso <andresdjasso@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-10 16:32:46 -07:00
Waleed 65b1ca125d feat(context-dev): add Context.dev to hosted key rotation pool (#5576)
Wires all 22 context_dev tools into the same hosted-key mechanism as
Exa: CONTEXT_DEV_API_KEY_COUNT/1..N rotation, BYOK provider
registration, and hideWhenHosted on the block's API key field. Cost
is read directly from each response's reported credits_consumed
rather than estimated per endpoint.
2026-07-10 15:58:35 -07:00
Waleed 5de66db678 fix(rich-markdown-editor): stop image dupe-uploads on drag/paste; fix link color under bold/italic/strikethrough/code (#5573)
* fix(rich-markdown-editor): stop drag/paste of an existing image from re-uploading a duplicate

Dragging an image block to reorder it, or copy-pasting an already-hosted image within the
editor, both re-uploaded the image as a brand-new file instead of reusing/moving the existing
one:

- Dragging an <img> to reorder it is a native HTML5 drag; browsers synthesize an image File
  into event.dataTransfer for it (the same mechanism that lets you drag a web image to your
  desktop), indistinguishable from a real external drop by dataTransfer contents alone. Our
  handleDrop treated that File as a genuinely new image, uploaded it, and inserted a duplicate
  node — while ProseMirror's own default move logic never got to run, so the original was left
  behind too. Fixed by checking `view.dragging` (ProseMirror's own signal that a drop follows a
  dragstart within this same view) and bailing out to let its default move logic run.

- The same browser behavior applies to copy-paste: selecting a rendered <img> already on the
  page and pressing Cmd+C puts BOTH `text/html` (the real node, with its real hosted src) AND a
  synthesized image File onto the clipboard. Our handlePaste preferred the File, re-uploading and
  inserting a new node rather than cloning the original (silently dropping width/href/title in
  the process). Fixed by preferring the HTML sibling — via the existing extractEmbeddedFileRef
  helper — whenever it already names one of our own hosted files.

* fix(rich-markdown-editor): fix link color lost under bold/italic/strikethrough/code

strong/em/del/s/code each set their own explicit `color` for the plain (no-link) case. Nested
inside a link, that explicit rule on the mark itself always wins over the color inherited from
the ancestor <a> — an inherited value never beats an element's own explicit rule, regardless of
how specific the ancestor's selector is. So an italic (or bold/struck-through/inline-code) link
rendered in the mark's plain-text color instead of the link's blue.

Adds an explicit `.rich-markdown-prose a <mark>` / `.rich-markdown-prose <mark> a` override,
covering both DOM nesting orders since ProseMirror's mark order (and so which nests outside the
other) depends on which was toggled first, not a fixed schema order. Only `color` is touched —
each mark's own font-weight/font-style/text-decoration/background composes normally underneath.

24 new tests load the real, shipped CSS into jsdom and assert against getComputedStyle for every
mark x both nesting directions x multi-mark stacks, plus regression guards that a mark with no
link keeps its own color and a link elsewhere in the doc doesn't bleed color into unrelated text.
Verified all 11 color-assertion tests fail against the pre-fix CSS.

* fix(rich-markdown-editor): fix real-world gaps in the image dupe-upload fix

Found while re-verifying the drag/paste dupe-upload fix against the actual rendered DOM before
trusting it:

- hasHostedImageHtml's predicate only recognized the *persisted* src shape
  (extractEmbeddedFileRef, e.g. /api/files/view/...). The DOM (and so a same-page copy's
  clipboard html) always contains resolveImageSrc's REWRITTEN inline-route URL instead
  (/api/workspaces/{id}/files/inline?key=.../?fileId=..., or the public-share equivalent) — a shape
  the fix never recognized, so it silently never engaged for a real browser copy. Added
  isInlineRouteSrc to also recognize it, verified end-to-end against the real resolveImageSrc
  output (not a hand-typed guess).
- The img-src regex only matched quoted attribute values; an unquoted src (valid HTML) fell
  through to the old re-upload path instead of being recognized as hosted.
- The paste bypass fired on ANY hosted image found in the html, even when the clipboard also
  offered additional image files — a genuinely mixed paste (the hosted image plus a separate new
  one) would have the new file silently dropped instead of uploaded. Narrowed to only bypass when
  exactly one image file is offered.
- The drop bypass checked view.dragging unconditionally, including for the plain-file swallow
  branch below it — a stale view.dragging (ProseMirror clears it up to ~50ms late via dragend when
  a prior internal drag was dropped outside the view) could suppress swallowing an unrelated
  non-image file drop (e.g. a PDF) in that window, letting it fall through to the browser default.
  Gated on images.length > 0 so staleness can only ever affect the image-specific path it exists
  for.

Extracted the paste/drop bypass decisions into shouldSkipPasteUpload/shouldSkipDropUpload so
they're unit-testable without mounting the full editor component.

* fix(rich-markdown-editor): fix the entire class of mark-color-vs-ambient-color bugs, not just links

Auditing every explicit `color` in this file for the same failure mode (an element's own explicit
color always wins over an inherited one, regardless of ancestor specificity) surfaced a second,
previously-unfixed instance: bold/italic text inside an h6 heading showed the brighter
--text-primary instead of h6's own intentionally dimmer --text-secondary, since strong/em hardcoded
--text-primary as their default.

strong/em's color was always redundant with the prose root's own default anyway — removing it
entirely (matching the highlight/`mark` rule's existing `color: inherit` convention in this same
file) lets normal CSS inheritance carry the correct color through from ANY ambient context, not
just links: a link's blue, h6's dimmer tone, or any future colored container this file doesn't
know about yet. `code` has the same redundant color, also removed.

del/s genuinely need their own dimmer default (distinct from the prose default), so they keep an
explicit color plus the link-color override — now the only mark that needs one, since strong/em/
code no longer set a competing color to override in the first place.

10 new tests cover every heading level x strong/em/code/del/s x link, including 2 that fail against
the pre-fix CSS (bold/italic and inline-code inside h6 both incorrectly showed --text-primary).

* fix(rich-markdown-editor): stop paste-clone from persisting the display-layer image URL

Cursor caught a real correctness bug in the paste/drop dupe-upload fix: bypassing to the editor's
DEFAULT html-based paste for an already-hosted image made it re-parse the clipboard html's <img
src>, which is resolveImageSrc's REWRITTEN *display* URL, not the real persisted one — baking that
display-only URL into the document. Public share, export, and referenced-by-doc tracking only
recognize the persisted shape, so the pasted image would silently vanish from all three.

Fixed by no longer letting default paste construct the node at all: findHostedImageAttrs walks the
CURRENT doc for an existing image node whose *resolved* src matches the clipboard html's, and
returns that node's real, persisted attrs (src, width, href, title — everything) to clone
ourselves. Falls through to a normal upload (always correct, just occasionally redundant) if no
match is found, rather than ever trusting the html's src directly.

Also merged the paste- and drop-specific skip checks into one shouldSkipFileUpload, and switched
the drop side off `view.dragging` entirely (Greptile: it can go briefly stale, up to ~50ms, when a
prior internal drag was dropped outside the view, which could suppress upload of an unrelated new
file dropped in that window) — now purely a function of what the current event's html/images
actually contain, which the drop's own default move logic (relocating the real node, never
re-parsing html) was never at risk from in the first place.
2026-07-10 15:27:51 -07:00
Waleed fac8ec0c51 fix(workflow-edges): enforce edge/block validation server-side, not just client-side (#5571)
* fix(workflow-edges): enforce edge/block validation server-side, not just client-side

Dragging a connection that creates a cycle correctly refused to render
client-side, but the cyclic edge was still queued for realtime persistence
and written to the DB, so it reappeared after refresh. Root cause: cycle
detection (and several other edge/block rules) only lived in the client
Zustand store and was never enforced by the realtime persistence layer,
which is the actual source of truth on reload.

- Move wouldCreateCycle, edge scope-boundary, annotation-only-block,
  duplicate-edge, and block-name-conflict checks into @sim/workflow-types
  so the client store, the collaborative queueing layer, and
  apps/realtime's DB write path all share one implementation
- Wire these into apps/realtime/database/operations.ts's edge-add and
  block-rename handlers as the authoritative gate
- Client-side behavior is unchanged (same call sites, same error messages,
  same rule ordering) — verified via existing + new test coverage

* fix(workflow-edges): address review findings on realtime edge validation

- Select triggerMode when fetching blocks for edge-add validation —
  isKnownWorkflowTriggerBlock checked block.triggerMode but the column
  was never fetched from the DB, so trigger-mode blocks could still
  receive an incoming edge (Cursor Bugbot)
- Make filterUniqueWorkflowEdges incremental, so two duplicate edges
  within the same BATCH_ADD_EDGES payload are also deduped instead of
  both surviving (Greptile)

* fix(workflow-edges): normalize empty-string handles in duplicate-edge check

filterUniqueWorkflowEdges compared handles with ??, so a `sourceHandle: ''`
edge wasn't recognized as a duplicate of an existing null-handle edge —
even though both get persisted as the same null value at insert time
(edge.sourceHandle || null). Falsy-coalesce in the comparison so '' and
null/undefined are treated as the same "no handle" state everywhere.
(Greptile)

* improvement(workflow-edges): dedup realtime edge-add validation, reuse block-name-conflict helper

/simplify pass on the already-merged-quality PR before sign-off:

- Extract filterEdgesForPersist in apps/realtime/src/database/operations.ts:
  the single-edge ADD and batch BATCH_ADD_EDGES handlers hand-inlined the
  same six-step validation pipeline (missing block, protected target,
  annotation-only, trigger-target, scope boundary, duplicate, cycle) and
  each independently re-fetched blocksById/existingEdgesForCycleCheck. Two
  copies of one rule in the same file was exactly the drift risk this PR
  otherwise closes across client/server. One shared helper now backs both.
  Net -63 lines despite the new shared function.
- Fix a real bug this surfaced: droppedCounts was keyed by the free-text,
  block-id-bearing scope-boundary message, so it could never aggregate
  across edges/runs. Now keyed by a stable 'scope boundary' reason.
- use-collaborative-workflow.ts's collaborativeUpdateBlockName still
  hand-rolled the empty/reserved/duplicate block-name-conflict checks this
  PR centralized as getWorkflowBlockNameConflict (already adopted by
  store.ts). Switched it to the shared helper, which also fixes a latent
  check-order mismatch between the two (this pre-check ran reserved before
  duplicate; the store's real gate — after this PR's own store.ts change —
  runs duplicate before reserved, so they could disagree on which toast a
  name that was both reserved and duplicate would surface).

* docs(workflow-edges): document the per-workflow write-serialization invariant

No behavior change. Address Greptile P1 on filterEdgesForPersist ('concurrent
duplicate writes can persist without a per-workflow write guard') by
documenting, at the actual mechanism, why the concern doesn't apply here:
persistWorkflowOperation's leading 'UPDATE workflow SET updatedAt ... WHERE
id = workflowId' already takes a row lock that serializes every operation
(including edge adds) for a given workflowId — a second concurrent call
blocks on that UPDATE until the first transaction commits or rolls back, so
the validate-then-insert sequence in filterEdgesForPersist can never
interleave across two writers on the same workflow.

Verified empirically, not just by reading: ran two concurrent transactions
against a throwaway local Postgres against the exact statement shape used
here (UPDATE the parent row, sleep to simulate the read/validate window,
insert, commit). The second transaction's UPDATE blocked for the full
duration of the first's transaction and only proceeded once the first
committed — confirming the row lock, not any application-level guard,
already provides the serialization Greptile flagged as missing.

Added a comment at the lock site (not a second, redundant advisory lock)
so a future change can't silently break this invariant by making the
UPDATE conditional/skippable as a perceived no-op optimization.

* fix(workflow-edges): validate edges in BATCH_ADD_BLOCKS before persisting

Real gap Cursor's PR summary flagged ('BATCH_ADD_BLOCKS edge inserts... not
fully covered by the new server pipeline'), confirmed by reading the code:
this handler bulk-inserted the edges from a block-paste/duplicate/import
payload directly into workflowEdges with zero validation — no missing-block,
protected-target, annotation-only, trigger-target, scope-boundary,
duplicate, or cycle check. A client sending edges through this operation
instead of BATCH_ADD_EDGES could bypass every rule this PR otherwise
enforces server-side, exactly the class of gap the PR exists to close.

Routes it through the same filterEdgesForPersist used by the other two
edge-add handlers. Runs after the block insert in this same handler, so the
shared helper's blocksById lookup also sees the blocks this same batch just
inserted (a transaction observes its own prior writes).
2026-07-10 15:25:25 -07:00
Theodore Li 43e61a1eaf fix(enrichments): remove Icypeas providers and show Running on in-flight cells (#5572)
* fix(enrichments): remove Icypeas providers and show Running on in-flight cells

* fix(enrichments): keep previous value visible while an enrichment cell reruns
2026-07-10 18:23:19 -04:00
5d3809a0e3 feat(tiktok): add tiktok trigger, block (#5504)
* feat(tiktok): add TikTok integration

Adds TikTok as a full OAuth-based integration: provider registration
(with TikTok's comma-separated scope and client_key requirements),
9 tools covering profile info, video listing/querying, creator info,
direct video/photo posting (URL or file upload), inbox drafts, and
post status polling, plus the TikTok block, icon, and generated docs.

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

* fix(tiktok): add avatarFile output to Get User Info

Adds a file-typed avatarFile output (sourced from the largest available
avatar URL) alongside the existing string avatar fields, so the profile
picture can be materialized as a UserFile and chained into file-consuming
blocks (e.g. attached to an email), per PR review feedback.

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

* fix(tiktok): lower upload memory cap, drop redundant avatar string outputs

Cap the file-upload video buffer at 250MB instead of TikTok's 4GB ceiling —
relaying that much through this server's memory per request isn't safe
under concurrent load, and larger files can still go through the
PULL_FROM_URL path, which never buffers on our server. Also drop the
now-redundant avatarUrl/avatarUrl100/avatarLargeUrl string outputs from
Get User Info in favor of the file-typed avatarFile output alone, since
the feature is unreleased and the raw URL is still reachable via
avatarFile.url. Cover image URLs on List/Query Videos are confirmed to be
signed, expiring TikTok CDN links; left as strings (no file-output
conversion path exists for fields nested inside array items) but
documented the expiry behavior more clearly.

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

* chore(ci): bump API validation route-count baseline for TikTok publish-video route

The TikTok integration adds one new Zod-backed internal API route
(app/api/tools/tiktok/publish-video), which trips the route-count
ratchet in check-api-validation-contracts.ts. Bumping totalRoutes and
zodRoutes from 917 to 918 (nonZodRoutes stays 0) to acknowledge the
new route is properly validated.

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

* chore(tiktok): drop unused avatar_url_100 from default user fields

After removing the avatar string outputs, avatar_url_100 was still
requested from TikTok's user info endpoint but never surfaced anywhere.
Removed it from the default field list and the field descriptions, and
noted that avatar_url/avatar_large_url feed the avatarFile output.

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

* fix(tiktok): stop returning raw 'credential' subBlock id from tools.config.params

The block's params function built a local `credential` variable from
params.oauthCredential and returned it under the key `credential` in
every switch case. That literal token is the raw subBlock id, which is
deleted after canonical transformation into `oauthCredential` — the
blocks.test.ts canonical-param-validation suite flags any params
function that still references it.

It was also redundant: oauthCredential is already part of the base
resolved inputs, which the executor merges into the tool call before
config.params overrides are applied, so the OAuth token resolution
(which reads contextParams.oauthCredential) worked regardless. Removed
the explicit credential plumbing, matching the convention already used
by other OAuth blocks like dropbox.ts.

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

* fix(tiktok): send empty JSON body on Query Creator Info POST

query_creator_info had no request.body function, and
formatRequestParams() only attaches a body when tool.request.body is
defined at all — so despite sending Content-Type: application/json,
the request went out with no body whatsoever. Added body: () => ({}),
matching the convention already used by other parameterless-POST tools
in this codebase (Google Vault, Supabase, Square, Gmail, etc.).

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

* fix(tiktok): stop dropping valid zero values in optional numeric fields

cursor, photoCoverIndex, and videoCoverTimestampMs all used a truthy
check (params.x && {...}) to decide whether to include an optional
numeric override, which drops a legitimate 0 (first page has no
cursor issue aside, photoCoverIndex 0 is TikTok's own default cover
photo, and timestamp 0 is a valid first-frame cover). Switched to
explicit undefined/empty-string checks, matching the !== undefined
convention the underlying tools already use.

In today's resolution pipeline these fields always arrive as strings
(even chained block references get stringified by the template
resolver), and a non-empty string like "0" is truthy, so this wasn't
actively broken end-to-end - but it was relying on that subtlety
rather than being correct by construction, and was inconsistent with
the tools' own undefined checks.

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

* fix(tiktok): accept newline-separated video IDs in Query Videos

videoIds is a long-input (multiline textarea), the same widget used
for the newline-separated photoImages field on this block, but its
parser only split on commas. Entering one ID per line - the natural
pattern for a multiline field, and the one already used elsewhere on
this block - produced a single concatenated garbage string instead of
an array, so TikTok's query would fail or return nothing. Now splits
on commas or newlines, and updated the placeholder/description to
reflect both formats.

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

* feat(tiktok): add app-level webhook ingress and triggers

* fix(tiktok): only count actually queued webhook executions

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

* chore(tiktok): bump API validation baseline for staging merge

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

* cleanup code

* fix type issues

* misc code cleanup

* remove photos and add upload for videos

* move shared video output properties to types.ts so docs generation resolves them

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

* hide TikTok from toolbar and docs until the integration is ready to ship

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

* fix(ci): ratchet API validation baseline to 924 after staging merge

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

---------

Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
2026-07-10 15:08:24 -07:00
Waleed bbff34a43e fix(mcp): route object params to JSON editor, keep invalid drafts out of tool args (#5570)
* fix(mcp): route object-typed params to JSON editor, keep invalid drafts out of tool args

Two follow-up gaps from the earlier MCP schema fix:

- getInputType only routed array-typed and non-primitive-enum params to
  the long-input JSON editor; a plain object-typed param (no enum) fell
  through to the default short-input, which stores raw text via
  toString() and never round-trips a real object.

- The long-input onChange fell back to storing the raw typed text
  whenever JSON.parse failed (needed to keep the controlled textarea
  responsive mid-edit), but that meant an incomplete/invalid edit
  (e.g. `{"a":1` before the closing brace) could persist into the
  actual tool arguments. If executed in that state, the MCP execute
  route's array-coercion step would silently wrap the malformed string
  into a corrupted array instead of failing validation.

Invalid-edit text now lives in local `invalidJsonDrafts` state, keyed
by param name, instead of the real argument store — the textarea still
reflects every keystroke, but the persisted tool argument is always
either the last successfully parsed value or untouched. Drafts reset
when the selected tool changes.

* fix(mcp): reset invalid JSON drafts on schema change, not just tool change

The draft reset only fired when the selected tool id changed, so a
same-tool schema refresh (e.g. re-discovering tools from the live MCP
server) could leave a stale invalid draft displayed under a param name
whose shape had since changed. Key the reset off both the tool id and
a signature of the effective schema's properties, so any change to
what's actually being edited clears stale drafts.

* fix(mcp): key draft reset off both schema sources, not the resolved toolSchema

toolSchema resolves to cachedSchema || selectedToolConfig?.inputSchema,
so a live-only schema refresh (the discovered tool's inputSchema
changes but the cached _toolSchema snapshot doesn't) left the reset
key unchanged and could keep a stale invalid draft on screen. Track a
signature of each schema source independently so a change in either
one clears drafts, regardless of which source toolSchema resolves to.

* fix(mcp): sign whole schema for draft reset; invalidate drafts on external value changes

Two more follow-up gaps:

- schemaSignature only serialized schema.properties, so a same-tool
  refresh that changed only top-level fields like `required` (properties
  byte-identical) left the reset key unchanged. Sign the entire schema
  instead of cherry-picking fields, so nothing schema-level can be missed.

- A draft only reset on tool/schema change, so if the persisted argument
  changed for any other reason (undo/redo, a diff baseline switch, a
  collaborator's concurrent edit), the draft could keep shadowing the
  now-current value in the editor while execution used the real one.
  Drafts now carry a baseline signature of the value they were typed
  against; a draft only displays while that baseline still matches the
  live persisted value, so any external change makes it fall back to
  showing the real value instead of stale text.

* fix(mcp): restore comma-separated array input; stop spurious draft reset on tool load

Two more follow-up gaps:

- Holding every JSON.parse failure in a local draft blocked the
  documented comma-separated array shorthand (see the placeholder text)
  from ever reaching toolArgs, since plain comma-separated text is
  never valid JSON. Only an in-progress JSON array/object literal
  (starting with `[` or `{`) needs to stay in the draft until valid;
  plain array-typed text that isn't attempting JSON persists
  immediately as before, letting the execute route's existing
  comma-split/wrap coercion handle it as designed.

- draftResetKey always included the live selectedToolConfig schema
  signature, even when cachedSchema wins the `toolSchema` resolution.
  That segment flips from empty to populated the moment mcpTools
  finishes an unrelated async load, wiping in-progress drafts though
  neither the rendered schema nor the stored args changed. The live
  signature now only factors into the key when there's no cached
  snapshot for toolSchema to prefer.

* fix(mcp): reset drafts on genuine live schema refresh, not just its first load

Excluding the live schema signature whenever a cached snapshot exists
(the prior fix for a Cursor finding about mcpTools' initial load
spuriously wiping drafts) went too far the other way: a genuine
same-tool live schema refresh while a cached snapshot is still present
would no longer reset drafts either.

Track the live schema signature unconditionally, but only treat a
change as a real reset trigger when it goes from one non-empty
signature to a *different* non-empty one. The bare empty → non-empty
transition (mcpTools completing its initial fetch) is excluded, since
that's not a schema change; a populated → differently-populated
transition (an actual re-discovery) still resets drafts regardless of
whether a cached snapshot is present.

* fix(mcp): compare live schema against last-known-non-empty, not prior render

Comparing the live schema signature only against the immediately
preceding render's value meant a schema that dropped to empty and then
reappeared with different content was invisible to the reset check —
both the drop (X → '') and the reappearance ('' → Y) look like a bare
empty/non-empty transition, which was deliberately excluded to avoid
resetting on mcpTools' initial load. Track the last non-empty value
actually observed instead, so a transient empty gap no longer erases
the baseline: the schema reset now fires correctly when the tool
reappears with a genuinely different schema, while a real first-ever
load (no prior non-empty value at all) still doesn't spuriously reset.

* fix(mcp): re-baseline live schema tracker fresh on every tool switch

lastNonEmptyLiveSchemaSignature carried over across a tool switch
whenever the newly-selected tool's live schema hadn't loaded yet in
that same render (still empty). When it loaded a moment later, the
comparison was against the *previous* tool's signature, so the new
tool's first schema load could be misread as a "genuine refresh" and
wipe drafts the user had already started typing against the new tool.
A tool/cached-schema change now always re-baselines the live-schema
tracker to the new tool's current signature (even if still empty), so
the "same tool" refresh comparison never bleeds across tool switches.
2026-07-10 14:37:39 -07:00
Waleed ea4f2691e3 fix(providers): correct max-tokens param and add schema guidance for NVIDIA/Z.ai (#5569)
* fix(providers): correct max-tokens param and add schema guidance for NVIDIA/Z.ai

- NVIDIA NIM and Z.ai both document max_tokens for output-length control,
  not OpenAI's newer max_completion_tokens - the latter was silently
  ignored by both vLLM-served NIM models and Z.ai's GLM models
- Z.ai has no json_schema response_format mode (only text/json_object),
  so structured-output requests now also inject the expected schema into
  the system prompt as best-effort guidance, since the request param
  alone can't enforce field names/types

* style(providers): replace inline comments with TSDoc, per CLAUDE.md

Consolidated the scattered narrative // comments in nvidia/index.ts and
zai/index.ts into a single TSDoc block per provider documenting the
provider-specific API quirks; removed the rest where they only restated
what the code already shows. Also dropped an inline comment on zai's
modelPatterns field in models.ts.

* fix(providers): scope Z.ai schema guidance to the response_format call only

- schemaGuidance now falls back to the bare responseFormat object when
  .schema is absent, matching the schema-or-format fallback used
  elsewhere in the codebase (was silently injecting nothing for callers
  that pass a bare JSON schema)
- guidance is now only appended to the messages sent alongside an
  actual response_format (the immediate call, or whichever pass
  deferResponseFormat applies it to) instead of every turn of an
  active tool loop, where it wrongly told the model to return final
  JSON instead of continuing to call tools
2026-07-10 14:20:57 -07:00
Theodore LiandClaude Opus 4.8 97bb727eeb fix(pii): install CUDA torch on amd64 so GLiNER can run on GPU (#5552)
* fix(pii): install CUDA torch on amd64 so GLiNER can run on GPU

The published pii image installed a CPU-only torch build, so GLiNER on the
ECS GPU fleet died at model load with "Attempting to deserialize object on a
CUDA device but torch.cuda.is_available() is False". The Dockerfile already
had a TORCH_INDEX_URL arg, but no CI job ever passed --build-arg, so every
image silently took the cpu default.

Select the wheel index from TARGETARCH instead: amd64 gets cu128, arm64 keeps
the cpu index (cu128 publishes no aarch64 wheel at 2.11.0, and no arm64 target
has a GPU). CUDA torch falls back to CPU when no GPU is present, so one image
still serves both the Fargate CPU tasks and the EC2 GPU tasks off the same tag
— no CI or CDK changes needed.

cu128 keeps sm_75, the compute capability of the fleet's T4s, and its CUDA 12.8
runtime needs driver >=525 via minor-version compatibility, which the ECS GPU
AMI satisfies. cu121 was not an option: that index stops at torch 2.5.1.

Verified in an amd64 build of the changed block:
  2.11.0+cu128  cuda=12.8  arch=sm_75 sm_80 sm_86 sm_90 sm_100 sm_120
arm64 still resolves to 2.11.0+cpu. A build-time assert now fails the image
if amd64 ever silently regresses to a cpu wheel.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QHNEWVrh7k89m8Wtqzhs18

* fix(pii): assert torch CUDA state after every pip install

The check sat directly after the torch install, but requirements-gliner.txt
and requirements-dev.txt are installed afterwards and resolve against PyPI
with no torch pin, so a future gliner bump could swap the wheel that
torch_index selected without tripping the assert.

Neither file changes torch today (verified: torch is 2.11.0+cu128 both before
and after the gliner install), so this guards the invariant rather than fixing
a live regression. Moving it below the last pip install makes it certify the
torch that actually ships.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QHNEWVrh7k89m8Wtqzhs18

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 16:21:45 -04:00
Waleed f0d85cb7ab fix(mcp): fix caret misalignment and tool schema contract validation (#5566)
* fix(mcp): fix caret misalignment in Add MCP Server modal fields

The Server URL and Header fields render a transparent input under a
formatted overlay div for env-var highlighting. The overlay used
font-medium/font-sans but the real input didn't, so glyph widths
diverged and the native caret drifted from the visible text as you
typed.

* fix(mcp): loosen tool schema contract to accept valid JSON Schema shapes

discoverMcpToolsContract's property schema rejected legal JSON Schema
that real MCP servers can return: array-form `items` (tuple
validation) and non-primitive `enum` values. Any server exercising
either shape failed contract validation client-side and blanked the
entire MCP tools list.

* fix(mcp): only render dropdown UI for primitive-valued enums

The MCP dynamic-args dropdown stringifies enum members for its
labels/values. Now that the tool schema contract accepts
non-primitive enum members (object/array), routing those through the
dropdown would collapse distinct values to "[object Object]" and
submit that string as the tool argument. Gate the dropdown on
primitive-only enums; non-primitive enums fall through to the
existing type-based branching (the JSON long-input editor for
object/array types), which round-trips arbitrary JSON correctly.

* fix(mcp): route non-primitive enums to the JSON editor regardless of type

isPrimitiveEnum() correctly excluded object/array enum members from
the dropdown, but the fallback only reached the long-input JSON
editor when paramSchema.type was 'array'. An object-typed (or
untyped) param with a non-primitive enum fell through to the default
short-input, which stringifies via toString() and drops the
enum-membership guarantee entirely. Any non-primitive enum now routes
straight to long-input, independent of the declared type.

* chore(mcp): fold inline comment into the existing TSDoc block

* fix(mcp): serialize non-string values before displaying in the long-input editor

The long-input JSON editor received value={value || ''} unconditionally,
so an argument already holding a parsed object/array (loaded from the
block's JSON arguments field) rendered as "[object Object]" or a
comma-joined list instead of valid JSON, and saving would overwrite the
real value with that mangled text. Serialize non-string values with
JSON.stringify before display; onChange still stores the raw text the
user edits, unchanged.

* fix(mcp): parse JSON-typed long-input edits back into real values

The long-input editor's onChange always stored the raw typed text, so
a param whose schema requires an object/array/non-primitive-enum value
(e.g. entering {"mode":"strict"}) was persisted as a string, not the
actual JSON value — the MCP tool call could receive the wrong type.
requiresJsonValue() identifies these schemas; onChange now parses the
edited text back into the real value once it's valid JSON, falling
back to the raw string mid-edit so the controlled textarea keeps
reflecting in-progress keystrokes.
2026-07-10 12:54:19 -07:00
Waleed 4952ddb73f feat(landing): add HubSpot tracking script for hosted marketing site (#5565)
* feat(landing): add HubSpot tracking script for hosted marketing site

- Loads the HubSpot loader in the landing route group only, gated by isHosted
- Not loaded for self-hosted/OSS deployments
- Adds the loader's companion scripts (analytics, form-tracking, banner) and their beacon hosts to CSP, verified against the actual scripts' network calls

* fix(landing): scope HubSpot CSP hosts to the landing route group only

Greptile flagged that the HubSpot script/connect hosts were added to the
shared CSP arrays used by every route, including /workspace, /login, and
/signup — even though the HubSpot loader only ever renders inside the
(landing) route group.

- Move the HubSpot hosts out of STATIC_SCRIPT_SRC/STATIC_CONNECT_SRC
- Add generateLandingRuntimeCSP(), which extends the shared runtime policy
  with the HubSpot hosts, mirroring the existing getChatEmbedCSPPolicy()
  pattern for route-scoped CSP variants
- Wire it into proxy.ts's catch-all branch, which is what actually serves
  the marketing/landing site; /workspace, /login, /signup keep the
  unmodified shared policy

* fix(landing): track HubSpot pageviews on client-side landing navigations

Cursor Bugbot flagged that the HubSpot loader only auto-fires a pageview
on the initial load. Since LandingLayout persists across client-side
navigations between landing routes, subsequent Link navigations never
told HubSpot about the route change, undercounting pageviews.

Add HubspotPageViewTracker, a small client component using the standard
Next.js App Router pattern (usePathname/useSearchParams in a Suspense
boundary) to push a manual pageview through HubSpot's _hsq queue on every
navigation after the first.

* simplify(landing): drop unnecessary Suspense from HubSpot pageview tracker

usePathname() alone doesn't require a Suspense boundary to preserve static
rendering — only useSearchParams() does. The tracker only needs the path,
not the query string, so drop useSearchParams and the Suspense wrapper
entirely. Simpler, and no risk to the landing site's static rendering/LCP.

* fix(landing): exclude non-landing routes from the landing CSP fallback

Greptile's second pass caught that proxy.ts's catch-all branch (which
serves generateLandingRuntimeCSP()) also handles several non-landing pages
that fall through the earlier explicit branches: /verify, /sso,
/reset-password (auth sub-pages), /resume/[workflowId] (interfaces),
/f/[token] (file shares), /playground, and the authenticated/callbackUrl
invite fallthrough. None of these render the HubSpot loader, so they
shouldn't get its CSP allowance either.

Add an explicit non-landing path prefix list and only fall back to
generateRuntimeCSP() (the tight policy) for those, keeping
generateLandingRuntimeCSP() for everything else in the catch-all.

* fix(landing): add /unsubscribe to non-landing paths, fix tracker remount bug

- Greptile correctly flagged /unsubscribe as another top-level page outside
  (landing) that reaches the CSP fallback branch; add it to the exclusion list
- Cursor caught that the per-mount useRef in HubspotPageViewTracker resets
  whenever LandingLayout remounts (e.g. leaving the landing site and coming
  back), but next/script dedupes the loader by id and won't re-fire the
  auto-tracked pageview on remount — so that return visit was silently
  dropped. Move the flag to module scope so it reflects the actual
  once-per-browser-session lifetime of the loader script, not per-mount

* fix(landing): track query-only landing navigations in HubSpot pageviews

Cursor caught that the tracker only depended on usePathname(), so
client-side navigations that change only the query string (blog/library
pagination, careers filters) never fired a pageview at all, and setPath
dropped the search string even when the path did change.

Add useSearchParams() back (the officially documented Next.js pattern for
tracking all route changes) and depend on the full path+query string.
Wrap the tracker in a local Suspense boundary, as required to keep the
route statically rendered — the fallback is null and the component
renders nothing, so this has no LCP/visual cost.

* test(proxy): add regression coverage for the non-landing path classifier

Exports isNonLandingPath and covers the exact prefix-boundary cases
(e.g. /f vs /ffoo, /resume vs /resumes) that the CSP routing fix depends
on, so this logic is verified by CI rather than my own ad-hoc checks.

* fix(landing): exclude /landing-preview from the landing CSP fallback

Greptile caught that /landing-preview calls notFound() in production
(see app/landing-preview/page.tsx) and its subroutes (marks-lab,
readme-tour-capture) don't render the (landing) layout either, so none of
them ever load the HubSpot tracker — but the CSP fallback was still
classifying the whole prefix as landing.

* chore(landing): remove the /landing-preview test scaffold

It was a temporary route for visual iteration (404s in production) — not
needed anymore, and Greptile had just flagged it as another route that
falsely inherited the landing CSP allowance. Removing it outright is
simpler than maintaining an exclusion for it.

Also removes SandboxWorkspacePermissionsProvider, which existed solely to
support the deleted readme-tour-capture page and has no other callers.

* refactor(landing): fix invalid const assertion, trim comments

- Fixed HUBSPOT_SCRIPT_SRC/HUBSPOT_CONNECT_SRC: 'as const' cannot wrap a
  ternary expression directly (TS1355) — caught by a full project typecheck
  I ran specifically to verify this PR, not by lint/tests alone. Rewrote
  using the same conditional-spread-inside-array-literal pattern already
  used by every other array in this file (e.g. STATIC_FRAME_SRC)
- Trimmed comments across all four touched files down to only the
  non-obvious 'why' (module-scope tracking flag, HubSpot CSP scoping
  rationale), matching this codebase's terse comment style elsewhere

* revert(landing): drop landing-scoped CSP, put HubSpot in the shared policy

Cursor caught a fundamental problem with the landing-scoped CSP: the
Content-Security-Policy header is fixed to the document's initial HTTP
response and is NOT re-applied on Next.js client-side (soft) navigation.

Both the landing navbar's ChipLink to /login and AuthShell's Link back to
/ are soft navigations (confirmed directly in the source, not assumed).
That means:
- /login -> / (soft nav): the browser keeps /login's CSP, which never
  allowed HubSpot hosts, so the loader gets silently blocked on landing.
- / -> /login (soft nav): the browser keeps the landing CSP, which is
  MORE permissive than /login's, undoing the tightening entirely.

A per-route CSP is fundamentally incompatible with this app's
client-side-routed navigation. Greptile's original 'CSP too broad on
/workspace' concern was valid in isolation, but the fix built across the
last several rounds doesn't actually work — it's neither reliably
tighter nor reliably functional, and each round's patch (exclusion list
entries, /landing-preview handling) was really just papering over that
core issue.

Revert to a single shared CSP for the whole app, matching exactly how
GTM/GA/ahrefs are already handled in this same file: HubSpot hosts land
in STATIC_SCRIPT_SRC/STATIC_CONNECT_SRC under the existing isHosted
gate. /workspace's CSP header technically allows origins it never
requests (same accepted tradeoff as GTM/GA), but the tracking script only
ever renders in the (landing) layout — matching the CSP scope Greptile
originally objected to. This is now provably correct because it can't
desync: proxy.ts, csp.ts, and proxy.test.ts are byte-for-byte identical
to origin/staging except for the HubSpot host list itself.

* fix(csp): drop overbroad *.hubspot.com connect-src wildcard

Greptile correctly flagged that *.hubspot.com is far broader than
anything the tracker actually needs — it covers HubSpot's entire product
surface (app, api, marketing), not just the tracking endpoints.

Re-checked my own network trace from earlier: the pageview beacon itself
is an image pixel (new Image() to track.hubspot.com/__pto.gif), which is
governed by img-src (already wide open to any https: origin), not
connect-src. The *.hubspot.com entry was an unverified guess for the
forms-API/banner fetch calls I couldn't pin down through minification —
removing it since I can't confirm what it was actually protecting,
keeping only the verified *.hscollectedforms.net entry.
2026-07-10 12:24:57 -07:00
Waleed 08320d5ab0 fix(sidebar): fix rename input losing its selection on open (#5563)
* fix(sidebar): fix rename input losing its selection on open

Radix's FocusScope defers close-time focus teardown to a setTimeout(0),
which can occasionally run after the rename input's own focus()/select()
and clobber the selection. Focus the input from onCloseAutoFocus instead,
which runs inside that same deferred teardown and always wins the race.

Affects workflow, folder, and workspace rename (all route through the
shared sidebar ContextMenu component).

* fix(sidebar): only refocus the rename input when Rename triggered the close

onCloseAutoFocus fires on every menu close, not just after selecting
Rename. Gate the refocus behind a ref set only when the Rename item
was selected, so closing the menu for an unrelated action (Delete,
Duplicate, ...) while an earlier rename is still live doesn't steal
focus back into it and delay its blur-save.
2026-07-10 10:55:34 -07:00
Waleed 8bf942407c feat(models): add latest Groq and Cerebras models (#5561)
* feat(models): add latest Groq and Cerebras models, flag near-term retirements

- Groq: add qwen/qwen3.6-27b (Preview, $0.60/$3.00, 131k ctx) - live on
  console.groq.com/docs/models, not previously in the catalog
- Groq: mark meta-llama/llama-4-scout-17b-16e-instruct and qwen/qwen3-32b
  deprecated - both have an announced shutdown date of July 17, 2026 per
  Groq's own deprecations page
- Cerebras: add gemma-4-31b (Preview, $0.99/$1.49, 131k ctx/40k max output) -
  live on inference-docs.cerebras.ai, not previously in the catalog

Every field independently verified via 2+ live sources (provider docs +
pricing pages) before adding; no code changes needed since both providers
use generic OpenAI-compatible completions with no per-model special-casing.

* fix(models): add verified releaseDate for groq/qwen/qwen3.6-27b

Greptile correctly caught that omitting releaseDate causes this model to
sort last within the Groq section in the model picker (orderModelIdsByReleaseDate
treats a missing date as Number.NEGATIVE_INFINITY) - misleading since it's
actually the newest model in the lineup. Verified 2026-04-21 (Qwen's own
upstream release date, matching this repo's existing convention of using the
model creator's release date rather than a reseller-specific date) via
llm-stats.com, cross-checked against two other independent sources.
2026-07-10 09:58:34 -07:00
2986362b38 feat(providers): add NVIDIA NIM and Z.ai providers (#5560)
* v0.6.29: login improvements, posthog telemetry (#4026)

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

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

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

* fix(signup): fix turnstile key loading

* fix(login): fix captcha header passing

* Catch user already exists, remove login form captcha

* feat(providers): add NVIDIA NIM and Z.ai providers

- NVIDIA NIM (BYOK): Nemotron model family (70B/Ultra-253B/Super-49B v1.5,
  Nemotron-3 Nano/Super/Ultra) via integrate.api.nvidia.com's
  OpenAI-compatible API
- Z.ai (hosted): GLM model family (5.2 down to 4-32B) via api.z.ai's
  OpenAI-compatible API, bare glm-* model ids with no provider prefix,
  Sim-provided key rotation (ZAI_API_KEY_1/2/3) matching openai/anthropic/google
- Z.ai tool_choice is forced to 'auto' since the API only documents auto
  support; forced/none tool_choice from prepareToolsWithUsageControl is
  ignored with a warning log instead of being sent to the API

* fix(providers): scope zai model routing to the exact catalog

Drop the /^glm/ fallback pattern - it would overmatch any unrelated
self-hosted "glm-*" model (e.g. a custom vLLM/LiteLLM deployment) and
misroute it to Z.ai's hosted, Sim-billed key. Routing now relies solely
on the exact model-id match against zai's static catalog.

* fix(providers): wire thinking/reasoning_effort into Z.ai requests

request.thinkingLevel and request.reasoningEffort were computed but
never mapped onto the Z.ai payload, so the thinking toggle and GLM-5.2's
reasoning_effort control silently no-op'd and always ran on Z.ai's
server-side default instead of the user's selection.

* fix(providers): route Z.ai through the workspace BYOK resolver too

getApiKeyWithBYOK (the resolver actually used by workspace provider
runs, per providers/index.ts) only rotated server keys for
openai/anthropic/google/mistral, so GLM calls without a user apiKey
failed even with ZAI_API_KEY_1/2/3 configured — getApiKey in
providers/utils.ts had zai wired but that's not the codepath workspace
runs go through. Add zai to the hosted-check condition, and register it
as a BYOKProviderId so a workspace can also bring its own Z.ai key.

---------

Co-authored-by: Theodore Li <theodoreqili@gmail.com>
Co-authored-by: Siddharth Ganesan <33737564+Sg312@users.noreply.github.com>
Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com>
Co-authored-by: Theodore Li <theo@sim.ai>
2026-07-10 09:15:42 -07:00
Waleed 7d1c927ab1 fix(models): correct model catalog data and Gemini thinking-config wire format (#5559)
* fix(models): correct model catalog data and Gemini thinking-config wire format

- OpenAI: remove fabricated 'max' reasoning-effort value from gpt-5.6 family
- Anthropic: fix claude-sonnet-4-6 maxOutputTokens (64k -> 128k); remove 3 fully
  retired models (claude-opus-4-0, claude-sonnet-4-0, claude-3-haiku-20240307);
  fix budget_tokens/max_tokens clamp that could send budget_tokens >= max_tokens
  for claude-opus-4-1 at its default thinking level
- Google/Vertex: un-deprecate gemini-3-flash-preview (no shutdown date announced);
  add thinking capability to gemini-2.5-pro/flash/flash-lite (google + vertex)
- Fix gemini/core.ts to send thinkingBudget (not thinkingLevel) for Gemini
  2.5-series models, which reject thinkingLevel entirely - only Gemini 3.x
  supports it
- Bedrock: mark claude-opus-4-1 deprecated per AWS's own Bedrock lifecycle
  schedule (Legacy since Jul 8 2026, separate from Anthropic's direct-API date)

* fix(models): restore retired Claude entries as deprecated instead of removing

Greptile caught a real backward-compat regression: fully removing
claude-opus-4-0/claude-sonnet-4-0/claude-3-haiku-20240307 dropped them from
getHostedModels()/shouldBillModelUsage(), so saved workflows still referencing
them would fail on a missing-API-key error instead of Anthropic's actual
"model retired" error. deprecated:true isn't consumed by the model picker
(only copilot's serializer reads it), so restoring them this way costs
nothing on hiding from new selection while preserving hosted-key resolution
for existing references.

* fix(models): restore Sol-exclusive 'max' reasoning value; fix Gemini 2.5 disable-thinking gap

Found during a final per-model audit round with independent 2-3 source
verification on every changed model:

- gpt-5.6-sol: restore 'max' reasoning-effort value. Multiple independent
  sources (OpenAI's own model-guidance docs page, launch announcement, and
  press coverage) confirm 'max' is a real, newly-launched value exclusive to
  Sol - not fabricated as originally assessed. Terra and Luna correctly do
  NOT get 'max' (confirmed Sol-exclusive), so they're unchanged.
- gemini-2.5-flash / gemini-2.5-flash-lite (google + vertex): selecting
  'none' for thinking level was sending no thinkingConfig at all, which
  falls back to the API's dynamic default (thinking stays ON for flash) -
  not actually disabling it, even though both models explicitly support
  thinkingBudget:0. Now sends an explicit budget of 0 for these two models
  specifically (gemini-2.5-pro is correctly excluded - it cannot disable
  thinking at all, floor is 128 not 0).

* chore(models): tighten inline comments in anthropic/gemini core

Trim verbose multi-line comments to single concise lines and remove
duplication with the TSDoc already on the ANTHROPIC_MIN_BUDGET_TOKENS/
ANTHROPIC_THINKING_OUTPUT_HEADROOM constants. No behavior change - verified
against the full test suite (811 files, 11141 tests, all passing).
2026-07-10 09:08:00 -07:00
Waleed 2cfa040f23 feat(brex): add transfer/budget/spend-limit/vendor write tools (#5558)
* feat(brex): add transfer/budget/spend-limit/vendor write tools

- fix get_company transformResponse reading camelCase accountType instead of Brex's account_type field (always null in prod)
- add brex_create_transfer, brex_create_budget, brex_archive_budget, brex_create_spend_limit, brex_create_vendor, brex_update_vendor
- endpoints verified against Brex's live payments_api.yaml and budgets_api.yaml OpenAPI specs
- guard required money-amount fields against blank/NaN input instead of silently coercing to 0

* fix(brex): correct status enum docs and normalize expense date filters

- get_budget/get_spend_limit output descriptions listed status enum values not in Brex's actual schema
- list_expenses now normalizes purchased_at_start/end through toBrexDateTime for consistency with list_card_transactions/list_cash_transactions

* fix(brex): preserve zero values and normalize booleans in write params

- toRequiredAmount now rejects whitespace-only input (Number(' ') coerces to 0)
- limitBufferPercentage/transactionLimitAmount used truthy checks that dropped explicit 0
- isPproEnabled now normalized to a real boolean instead of forwarding a stringified 'false' from dynamic references

* fix(brex): null-safe PPRO coercion and fail-fast on empty vendor update

- toOptionalBoolean now treats null the same as undefined (was only checking undefined), so a null isPproEnabled from a dynamic reference is omitted instead of coerced to false
- brex_update_vendor throws when no updatable field (companyName/email/phone) is provided, instead of sending an empty PUT body
2026-07-09 22:54:46 -07:00
Waleed f9a5d8b113 fix(security): close code-scanning and dependabot alerts (#5557)
* fix(security): close code-scanning and dependabot alerts

- markdown-paste.ts: strip <style>/<script> in a loop, not a single
  pass, so nested/overlapping tags can't leave a surviving <script>
  behind (incomplete multi-character sanitization)
- block-identity.ts: annotate the two SHA-1 uses as intentional
  (UUIDv5 per RFC 4122, deterministic id derivation only, not a
  security use of the hash) rather than swap algorithms, which would
  change every derived fork block id
- apps/pii: bump transformers 4.56.2 -> 5.3.0 (CVE-2026-4372 RCE via
  crafted config.json, CVE-2026-1839 RCE via Trainer torch.load),
  huggingface_hub 0.35.3 -> 1.3.0 (transformers 5.3.0's floor), and
  pytest 8.4.1 -> 9.0.3 (CVE-2025-71176 tmpdir handling); verified
  pip resolves cleanly and the unit test suite passes on 9.0.3

* fix(files): make markdown-paste sanitizer O(n) instead of O(n*depth)

Greptile flagged the repeated-replace loop from the prior commit: it
strips <style>/<script> correctly but rescans the whole string once
per nesting level, so deeply nested clipboard HTML can freeze the tab.
Replace it with a single linear pass that tracks nesting depth of the
open tag via a tag-token scan, dropping the element in one pass no
matter how deeply nested.

* style: fold inline comments into TSDoc per repo comment convention

Repo convention is TSDoc-only documentation, no non-TSDoc explanatory
comments. Moved the uuidV5 SHA-1 rationale and the stray-close-tag note
into the existing TSDoc blocks above each function. Left the two
lgtm[...] annotations as trailing comments since those are functional
CodeQL suppression directives (must sit on the flagged line), not
documentation.

* test(files): lock in nested-tag stripping regression for markdown paste

Covers the case Greptile flagged: nested and 50-deep <script> tags
must strip in one pass without leaking a dangling tag.

* fix(files): drop unterminated <script>/<style> instead of leaking it

Cursor Bugbot caught two related bugs in the single-pass rewrite: if
pasted HTML ends while a script/style element is still open (truncated
or malformed clipboard HTML), cursor never advanced past the open tag,
so the final flush re-appended the untouched tag/content (leaking an
unstripped <script>) and duplicated the prefix already copied into
result.

Fix: advance cursor the moment a tag opens, not when it closes, and
only do the final flush when we end at depth 0. An element that never
closes has cursor already past its open tag, so it and everything
after it is dropped instead of reappearing.
2026-07-09 20:54:29 -07:00
Waleed e2cb3b29ba fix(files): fix savingRef mutex integrity race after discard correction (#5554)
* fix(files): fix savingRef mutex integrity race, anchor discard un-suppress to captured target

An independent 4-agent audit (beyond the bot review loop) converged on a real
race in the round-11 discard-correction fix:

- save()'s deferred MIN_SAVING_DISPLAY_MS status timer resolves as a macrotask
  well after the save's own promise (used to sequence discard's correction)
  has already settled as a microtask. That timer unconditionally reset
  savingRef/triggered a trailing resave, even after discard's correction had
  since claimed the save slot for its own in-flight write — letting a fresh
  debounced save start concurrently with the correction. Now guarded with
  `if (inFlightRef.current) return` before touching savingRef, so it only
  acts when nothing else has claimed the slot since.

- The render-time un-suppress check keyed off isDirty, which a stale save's
  markSavedContent(next) landing after discard can transiently corrupt
  (overwriting savedContent with the pre-discard value while content has
  already been reverted), causing a spurious "genuinely new edit" signal.
  Now keyed off a discardTargetRef captured at discard time instead, which
  that corruption doesn't touch.

Also hardened recovery to key its once-per-mount guard on the specific
draft key rather than a bare boolean, so a hypothetical future caller that
reuses a hook instance across files would still get correct recovery
(today's real callers already remount per file, so this is defense in
depth, not a behavior change for any current caller).

* fix(files): fix discard status masking and cross-key discard suppression leak

Greptile round-1 review on the follow-up fix PR caught 3 real gaps in the
discard/correction flow:

- The display timer's !discardedRef guard (added to stop a stale save's
  status update from clobbering a running correction) also silently
  suppressed the idle-timer reschedule, and discard()'s own correction never
  set a terminal status on settle — so saveStatus could stick on 'saving'
  forever after a successful correction, and a failed correction surfaced
  only via the onDiscardCorrectionFailed callback with no status change.
  Now the correction's own .then()/.catch() sets 'idle'/'error' once it
  owns the flow (only when nothing has since un-suppressed discard).

- The discard-suppression un-suppress check compared content against the
  captured discard target, but never reset if a hook instance were reused
  across draftKeys — a coincidental content match with the previous file's
  target would keep discard permanently suppressing saves for the new file.
  Reset discardedRef whenever the effective draftKey changes.

* fix(files): re-chain autosave after a discard correction settles

Cursor Bugbot found the discard correction's finally() cleared the save
mutex but never rechecked for dirty content: an edit made while the
correction was in flight bailed out of the debounce effect (savingRef was
held) and, since content isn't a savingRef dependency, was never
rescheduled once the mutex freed — the edit could sit unsaved indefinitely.
The same gap explained a related report that a failed correction which had
already been superseded by a newer edit left saveStatus stuck, since
nothing else was driving it forward.

Fix is one line: call save() in the finally() when content is still dirty.
save()'s own guards (savingRef/discardedRef/content-equality) make this
safe to call unconditionally, and it naturally hands status ownership to
the newer edit's own save cycle.

* fix(files): key discard state to raw draftKey, make failed corrections retryable

Cursor Bugbot round 3 caught 2 more real gaps:

- The document-change reset added last round compared effectiveDraftKey
  (draftKey gated by enabled), so toggling enabled alone for the SAME
  document — e.g. a streaming lock — looked identical to switching files.
  That cleared discardedRef mid-correction, which skipped the correction's
  own setSaveStatus('error'/'idle') (gated on discardedRef to avoid
  clobbering a newer edit's status), silently stranding the hook on
  'saving' with no visible retry affordance. Now keyed off the raw
  draftKey, which enabled toggling never touches.

- After a failed correction, content already equals savedContent (that's
  what discard reverted to), so saveImmediately()'s retry going through
  save() hit its dirty-check and was a complete no-op — the error toast's
  Retry button did nothing. Extracted the correction logic into
  runCorrection(target), shared by discard() and by saveImmediately when a
  failed correction is pending, so retry pushes the reverted baseline
  again instead of bailing on a check that assumes retries are always for
  dirty content.
2026-07-09 20:04:48 -07:00
Waleed 896d15b666 fix(helm): close blocker/real-gap findings from Helm chart best-practices audit (#5555)
* fix(helm): close blocker/real-gap findings from Helm chart best-practices audit

Verified every finding against the official Helm docs and Kubernetes Pod
Security Standards docs before fixing, and validated each fix with
helm lint/template plus the chart's own helm-unittest suite (65 -> 79
tests, all new tests confirmed to fail on the pre-fix code):

- Blocker: values.schema.json documented "minimum 32/8 characters" on
  BETTER_AUTH_SECRET/ENCRYPTION_KEY/postgresql.auth.password but never
  enforced it. Added anyOf minLength-or-empty constraints (empty stays
  legal for existingSecret/ESO modes) — verified negative/positive cases
  live, no regression for any secret-delivery mode.
- Real gap: copilot didn't support the External Secrets Operator mode the
  rest of the chart offers (app/postgresql/externalDatabase). Added
  external-secret-copilot.yaml, remoteRefs.copilot, and extended
  sim.copilot.validate with the same "map it or remove it" fail-fast
  guard app.env/realtime.env already have. Verified byte-identical
  rendering for the existing non-ESO path.
- Real gap: the OpenTelemetry Collector was the only workload missing the
  shared Restricted-profile securityContext helpers (no container-level
  hardening at all). Wired sim.podSecurityContext/containerSecurityContext
  in, preserving the collector's original UID/GID/fsGroup.
- Real gap: copilot templates hand-rolled label/selector blocks instead of
  using the chart's established sim.<component>.labels/selectorLabels
  pattern. Added sim.copilot.*/sim.copilotPostgresql.* helpers and
  refactored every consumer — confirmed byte-identical helm template
  output before/after (selector labels are immutable on upgrade, so this
  was verified, not assumed).
- Documented (README): the ingressFrom default and readOnlyRootFilesystem
  posture, both real but intentional tradeoffs the audit flagged as
  underdocumented. Added extraVolumes/extraVolumeMounts to copilot's
  Deployment (realtime/pii already had it) so the readOnlyRootFilesystem
  guidance is actually actionable for all three stateless services.

Deferred (nice-to-have, not blocking): pinning the two floating Postgres
image tags, values.schema.json stubs for ~13 uncovered top-level sections,
and an OTel collector image version bump — none are correctness issues.

* fix(helm): move copilot's static config out of the ESO-required Secret

Greptile caught a real bug: copilot.server.env shipped with non-empty
static defaults (PORT, SERVICE_NAME, ENVIRONMENT, LOG_LEVEL), unlike
app.env/realtime.env which ship fully empty. The new ESO validation
correctly required every non-empty env key to be mapped in
externalSecrets.remoteRefs.copilot — but that meant a default install
with copilot + ESO enabled failed demanding secret-store paths for
values that were never secrets.

Fixed by applying the chart's own existing pattern for this exact
problem: moved the 4 static keys into copilot.server.envDefaults
(mirroring app.envDefaults) and inlined them as plain container env,
bypassing the Secret/ExternalSecret system entirely — same rationale
already documented for app.envDefaults. Verified live that Greptile's
exact repro (default copilot env + ESO enabled, only the 7 real secrets
mapped) now renders cleanly and the four values still reach the
container. Added a regression test that fails on the pre-fix code.

* fix(helm): don't shadow copilot's existingSecret with envDefaults

Greptile and Cursor Bugbot both independently caught this: in
copilot.server.secret.create=false (existingSecret) mode, the chart
still unconditionally inlined copilot.server.envDefaults as explicit
container env. Kubernetes gives explicit env precedence over envFrom,
so a pre-existing Secret's PORT/LOG_LEVEL/etc values were silently
overridden by the chart defaults — the exact shadowing bug
app.envDefaults already guards against via its own $useExistingSecret
skip, which I forgot to mirror when copying the pattern to copilot.

Skip envDefaults entirely in existingSecret mode (matching app's
existing behavior — the pre-created Secret is the sole source of
truth), while still rendering extraEnv. Verified live: existingSecret
mode now renders no env: block at all when extraEnv is unset, and
still renders extraEnv without envDefaults leaking in when it is set.
Added two regression tests, confirmed both fail on the pre-fix code.

* fix(helm): key copilot's existingSecret check off its own secret.create, not the global ESO flag

Round 2's fix (which I copied nearly verbatim from Greptile's own
suggested diff) used $useExistingSecret := and (not
externalSecrets.enabled) (not copilot.server.secret.create) — Greptile
caught its own suggestion's remaining bug on round 3: when
externalSecrets.enabled=true globally (for app/postgresql) but copilot
itself uses copilot.server.secret.create=false with its own
pre-created Secret, that condition evaluated to non-existingSecret mode,
so envDefaults still inlined and shadowed the user's Secret values —
same bug, different trigger condition.

envFrom always points at the user-provided Secret name whenever
secret.create=false, independent of what other components do with ESO,
so the check should key on that alone. Verified live: global ESO
enabled + copilot's own existingSecret now renders no env: block and
envFrom correctly points at the pre-created secret name; the two
scenarios that should still inline (copilot itself on ESO, plain
inline mode) still work. Added a regression test, confirmed it fails
against round 2's guard.

* fix(helm): checksum/secret annotation on copilot ignores ESO-sourced secret

Cursor Bugbot caught a real bug: checksum/secret only hashed
secrets-copilot.yaml's rendered output, but under
externalSecrets.enabled=true that template renders nothing (env
credentials come from external-secret-copilot.yaml instead). Result:
changing externalSecrets.remoteRefs.copilot mappings wouldn't change
the pod template hash, so Kubernetes would never restart the copilot
pod to pick up the new mapping — stale envFrom values until a manual
restart.

Fixed by hashing the concatenation of both templates' rendered output:
whichever mode is active, only one renders non-empty content, but the
concatenated hash still changes on a mode switch or a remoteRefs
change. This can't reach into the live secret store value ESO syncs
(Helm only sees the ExternalSecret manifest at render time) — that's
an inherent ESO limitation, not something a checksum annotation can
close; documented as such in the template comment.

Note: deployment-app.yaml and deployment-realtime.yaml have this same
latent limitation in ESO mode (checksum/secret only hashes
secrets-app.yaml), but that's pre-existing code outside this PR's
diff — not fixed here to stay scoped to what Cursor actually flagged.

Verified live: the checksum differs across two different
remoteRefs.copilot.LICENSE_KEY mappings, and still changes correctly
in plain inline mode. Added a regression test.
2026-07-09 20:00:52 -07:00
Theodore Li 5b7513f15d feat(blocks): add block visibility gating (preview blocks + AppConfig reveals) (#5526)
* fix(deps): install xlsx from @e965/xlsx npm mirror

The dependency was pinned to a direct tarball on cdn.sheetjs.com, which
now returns 403 (Cloudflare bot-challenge) to automated clients, breaking
bun install in CI. npm's own xlsx is frozen at 0.18.5, so switch to the
@e965/xlsx mirror which republishes the identical 0.20.3 CDN build to the
npm registry. No code changes needed — all imports use bare 'xlsx'.

* feat(blocks): add block visibility gating (preview blocks + AppConfig reveals)

* fix(blocks): reset visibility to fail-closed empty state on workspace switch

* fix(blocks): carry kill-switch entries across workspace-switch visibility resets

* chore(deps): revert stray local xlsx-mirror commit (keep staging's pinned source)

* chore(skills): rename gate-block skill to add-block-preview
2026-07-09 22:38:01 -04:00
Waleed a3487da8a1 improvement(files): remove Save UI in favor of silent autosave with local-first draft recovery (#5549)
* improvement(files): remove Save UI in favor of silent autosave with local-first draft recovery

Files editor no longer shows a Save/Saving/Save failed button - autosave
already ran in the background, the button was vestigial. Cmd+S still works.

Adds local-first draft persistence to the shared useAutosave hook (opt-in via
draftKey): edits mirror into IndexedDB on a 400ms debounce, independent of the
1.5s network save, and flush best-effort on visibilitychange/pagehide. On
reopen, a newer local draft is silently recovered and resynced. This replaces
the beforeunload "leave site?" warning, which only blocked navigation - it
never actually saved anything.

A toast (with a Retry action) surfaces a save failure, since there's no more
persistent status indicator to show it.

* improvement(files): simplify autosave draft persistence and dedupe editor resync

Structural cleanup after the local-draft feature: draftKey is now ANDed with
enabled inside useAutosave itself (rather than trusting every caller to
replicate that gating), the combined dirty-transition/debounce effect is
split into two single-purpose effects, redundant back-to-back IndexedDB
writes on visibilitychange+pagehide are deduped, a dead identity wrapper
around setDraftContent is removed, and the three near-identical
"resync editor body if changed" blocks in rich-markdown-editor.tsx collapse
into one local helper.

* fix(files): flush pending local draft on unmount, fix stale Retry target

Two real bugs from Greptile's first review pass:
- Unmounting before the 400ms local-draft debounce fired cancelled the
  pending timer without ever writing the draft, so if the network flush
  also failed on the way out, the edit had no backup anywhere. The unmount
  cleanup now calls persistLocalDraft() synchronously before attempting
  the network flush.
- The save-failure toast's Retry action read saveRef.current lazily at
  click time, so navigating to a different file before clicking Retry
  would retry-save the wrong file. It now captures the failing file's
  save function at the moment the toast is created.

* fix(files): Discard Changes now actually resets editor content

Discard previously only cleared the parent's mirrored isDirty/saveStatus
state; the editor's own content was never reset to match the server
baseline. On unmount, useAutosave's flush logic saw content still
diverged and (a) re-saved the "discarded" edit to the server, and (b)
after this PR's local-draft addition, also persisted it to IndexedDB —
so even a future fix to (a) would still have the draft resurrect the
discarded text on next open.

Adds a discardRef bridge (mirrors the existing saveRef pattern) so
Discard resets the editor's draft content back to savedContent before
navigating away, closing both paths at the root.

* fix(files): make Discard deterministic, independent of state-update timing

The previous discard fix (setDraftContent(savedContent) before navigating)
relied on that dispatch landing before the FileViewer unmounts. If unmount
raced ahead of it, the autosave cleanup would still see stale dirty content
and could resurrect the discarded edit via the local draft.

useAutosave now exposes discard(): it flags the instance as discarded,
cancels any pending timers, and clears the local draft immediately. Every
write path (persistLocalDraft, save, the unmount flush) checks that flag
first, so nothing written after discard() can bring the edit back,
regardless of whether the content-reset render has committed yet.

* fix(files): correct in-flight save after discard, fix IndexedDB write/delete ordering

Two more real races from round 4 of review:
- discard() couldn't stop a save that had already started (discardedRef
  only blocks saves not yet begun). Once that in-flight save lands, it
  now schedules a corrective save to push the reverted content, rather
  than leaving the discarded edit on the server permanently. Only fires
  when a save was genuinely in flight at discard time.
- persistLocalDraft's set() and clearLocalDraft's del() were independent
  promises with no ordering guarantee. A slow write starting before
  discard could resolve after discard's delete and resurrect the draft.
  Both now go through a single serialized queue per hook instance, so a
  delete queued after a write always runs after it completes.

* fix(files): make discard's corrective save use an explicit baseline

The corrective save (from the previous fix) relied on the caller's
setDraftContent(savedContent) having landed by the time it ran — a real
race, not a guarantee: React commits that render on its own schedule,
and if the correction's continuation runs first, onSave still reads the
ambient (still-dirty) content ref and re-persists the discarded edit.

onSave now accepts an optional override content; discard() captures
savedContentRef.current as an explicit target at the moment it's called
and passes it through, so the correction always pushes the true reverted
baseline regardless of render timing. Widened the shared onSave type is
backward compatible — the other useAutosave caller (chunk-editor) ignores
the extra optional param.

* fix(files): retry no longer depends on a remount-able shared ref, purge stale drafts

Two more from round 6:
- The failure toast's Retry action captured saveRef.current inside the
  effect reacting to saveStatus='error' — but if the user switched files
  between the failure occurring and that effect committing, the keyed
  remount could have already repointed saveRef at the new file's save
  function first. onSaveStatusChange now passes the failing instance's
  own saveImmediately alongside the 'error' status directly from the
  hook that owns it, so retry can never be sourced from the wrong file
  regardless of remount timing.
- A local draft with a stale (mismatched) baseline was left in IndexedDB
  after being correctly skipped for recovery, so it could resurrect later
  if the server baseline ever coincidentally matched it again. It's now
  purged as soon as it's identified as stale.

* fix(files): clear inFlightRef once a save settles

inFlightRef.current was never reset after a save resolved or rejected —
it stayed pointing at the (now-fulfilled) promise indefinitely. discard()
reads it to decide whether a save is genuinely in flight; since a
resolved promise is still truthy, discard() treated any prior completed
save as still pending, captured savedContentRef.current as the
"corrective" target, and could push a stale baseline if that capture
happened before the save's own dispatch had updated it.

Now cleared to null as soon as the save settles, so discard()'s
in-flight check reflects reality regardless of how long ago the last
save finished.

* fix(files): surface a failed discard correction, resume autosave after discard if editing continues

Two more from round 8:
- If discard()'s corrective save failed, it was only logged — the server
  could permanently keep the discarded edit with zero user-facing signal.
  Now surfaced via a dedicated onDiscardCorrectionFailed callback, which
  closes over the specific file's own name rather than routing through
  the shared onSaveStatusChange path (that path reads whichever file is
  currently selected, which by the time this fires is already the file
  the user navigated to, not the discarded one).
- discardedRef never cleared once set, so if the editor stayed mounted
  briefly after discard (before navigation completes) and the user typed
  again, every save path silently no-op'd forever for that new edit too.
  It now clears itself as soon as a genuinely new edit (content diverging
  from savedContent again) is observed.

* fix(files): serialize local drafts by key across mounts, not just within one

idbQueueRef was a per-instance ref, so it only ordered IndexedDB ops issued
by the same hook instance. A slow write queued by an unmount's flush lived
on as a bare promise after that instance was gone, with nothing sequencing
it against a freshly-mounted instance for the same file — its del() or
recovery read could run first, and the late write would land afterward and
resurrect a draft that was supposed to be gone.

Replaced the per-instance ref with a module-level queue keyed by draft key,
shared by every useAutosave instance (past or present) touching that key,
so ordering holds across a fast unmount+remount of the same file.

* improvement(files): consolidate autosave hook after 9 rounds of incremental fixes

Cosmetic-only pass, no behavior change:
- Hoisted MIN_SAVING_DISPLAY_MS to module scope alongside LOCAL_DRAFT_DELAY_MS
  (was declared inside the hook body, re-allocated every render, inconsistent
  with its sibling constant).
- Grouped the ~15 refs by concern (save/network, content mirrors, draft-key +
  callbacks, local-draft persistence, discard) instead of the chronological
  order they were added across nine review rounds.
- Removed a provably-dead re-check in the unmount cleanup: content/savedContent
  can't change between the outer guard and the inner one (no renders happen
  post-unmount), so only the discardedRef half of the inner check was live.
- Removed an unnecessary useCallback around onDiscardCorrectionFailed — its
  reference is never observed by anything (useAutosave copies it into a ref
  every render regardless of identity), unlike handleSaveStatusChange in
  files.tsx, which is correctly memoized because it flows through
  React.memo-wrapped TextEditor/RichMarkdownEditor.

Validated against external research: the two-tier debounce (network + local
IndexedDB draft) matches how Tiptap/Notion describe their own local-first
persistence; the discardedRef+corrective-save approach over AbortController
is a deliberate, justified choice (onSave has no signal parameter, and an
abort can't undo a write that's already landed server-side); the module-level
per-key promise queue is a recognized idiomatic pattern. Splitting this hook
into three smaller ones (useDebouncedSave/useLocalDraft/useDiscard) is a
legitimate future refactor, deliberately deferred given the risk of touching
this heavily-interdependent, already-hardened state this late in review.

* fix(files): serialize discard correction against newer saves, recover local drafts only once per mount

Two more real races, both interactions between earlier fixes:
- discard()'s corrective save and a genuinely new edit made right after
  could race independently: if the user typed again before the correction
  fired, and that correction landed after the new edit's own save, the
  server would end up with the discarded baseline instead of the user's
  latest content. The correction now shares the same inFlightRef/savingRef
  mutual exclusion normal saves use, and skips entirely once content has
  moved on to something that's neither the discarded baseline nor what it
  was at the moment discard() was called.
- The local-draft recovery effect re-ran every time draftKey toggled
  through enabled (e.g. autosave turning off during agent streaming and
  back on once it settles), re-scanning IndexedDB as if freshly mounted.
  If the settled content coincidentally matched the stale draft's stored
  baseline, a pre-stream local edit could silently overwrite the agent's
  work. Recovery now attempts exactly once per mount.
2026-07-09 18:09:41 -07:00
Waleed cbe5e0f3f4 fix(uploads): fix Azure Blob connection-string-only auth and document self-host parity (#5553)
* fix(uploads): fix Azure Blob connection-string-only auth and document self-host parity

Every Azure Blob operation (upload/download/delete/head/presigned URLs) threw
when only AZURE_CONNECTION_STRING was set, despite that being the documented
alternative to AZURE_ACCOUNT_NAME/KEY across .env.example, env.ts, and the
Helm chart. createBlobConfig required accountName unconditionally, and the
upload-SAS path had no fallback to derive credentials from the connection
string. Fixed both, verified end-to-end against a live Azurite emulator
(upload, download, head, delete, multipart/block-blob upload, and real
HTTP PUT/GET through generated SAS URLs), and added regression tests.

Also closes the remaining self-host Azure documentation gaps: AZURE_ACS_CONNECTION_STRING,
OCR_AZURE_*, KB_OPENAI_MODEL_NAME, and WAND_OPENAI_MODEL_NAME are now documented in
the Helm chart (values.yaml, values.schema.json, values-azure.yaml) and
.env.example alongside their AWS/S3 counterparts.

* fix(uploads): map new Azure keys in the ESO remoteRefs example

Greptile flagged that the values-azure.yaml External Secrets example
didn't map KB_OPENAI_MODEL_NAME, WAND_OPENAI_MODEL_NAME, OCR_AZURE_ENDPOINT,
and OCR_AZURE_MODEL_NAME, so a user who fills those in and switches to ESO
would hit a Helm template render failure. Added the remoteRefs entries and
verified with an isolated helm template render.
2026-07-09 18:09:27 -07:00
Theodore Li cc7a6a82f8 fix(custom-blocks): stop draft-load sanitization deleting consumer-typed input values (#5551) 2026-07-09 19:47:21 -04:00
Waleed bbde749dcb fix(rich-markdown-editor): remove the hover block drag handle and + button (#5550)
The hover handle (drag-to-reorder grip + insert button) added surface area and
edge cases for marginal value in a file editor. Block reordering is covered by
the keyboard shortcut (Mod-Shift-Arrow) and block insertion by the slash menu,
so the handle and + are redundant. Removes the component, its styles, the editor
wiring, and the now-unused @tiptap/extension-drag-handle-react dependency. The
highlight text-shift fix from the same feature branch is unaffected.
2026-07-09 16:09:04 -07:00
Waleed aa5b1d5569 fix(suggested-actions): swap unaudited filled icons for EMCN outline set (#5548)
* fix(suggested-actions): swap unaudited filled icons for EMCN outline set

Suggested-action template icons on the home page mixed filled/solid
icons in with the app's outline icon convention. Swapped them for
consistent outline icons and removed the unused filled Card icon.

- gmail.ts: Card -> ClipboardList
- clickhouse.ts, sftp.ts: Trash -> TrashOutline
- ssh.ts: TerminalWindow (emcn) -> SshTerminalIcon (moved to
  components/icons.tsx alongside the other block/brand icons)
- deleted unused packages/emcn/src/icons/card.tsx
- regenerated docs

* fix(docs): revert jira.mdx regen regression

generate-docs.ts is dropping the Configuration and generic-webhook
Output tables for Jira triggers even though the trigger schemas still
define those fields (caught by Greptile review). Unrelated to the
icon changes in this PR, so reverting jira.mdx to its prior content
rather than debugging the generator here.

* fix(build): remove last Card icon consumer in playground gallery

apps/sim/app/playground/page.tsx imported Card from the top-level
@sim/emcn barrel for the icon showcase grid, which I missed when
auditing @sim/emcn/icons consumers. Broke the production build after
card.tsx was deleted. Removed the import and its gallery entry.
Verified with a local `bun run build`.
2026-07-09 15:55:58 -07:00
Waleed ce32993c76 fix(knowledge): fix chunk_index race and storage-quota check/increment race (#5544)
* fix(knowledge): fix chunk_index race and storage-quota check/increment race

- createChunk now serializes concurrent writes to the same document via a
  transactional advisory lock before computing the next chunk_index, fixing
  the unique-constraint collision behind the ITSM 'Failed to create chunk'
  incident (two overlapping workflow runs appending to the same shared KB
  document).
- Document uploads now check and increment storage quota atomically in a
  single conditional UPDATE inside the insert transaction, instead of
  check-then-insert-then-increment-after-commit, closing the window where
  two concurrent uploads could both pass the quota check and over-commit
  storage.

* fix(knowledge): bound the chunk advisory-lock wait with lock_timeout

Address Greptile P1: the advisory lock in createChunk could wait
indefinitely on a stalled same-document transaction while holding a pooled
connection. Set a 5s lock_timeout before acquiring it, matching the
set_config + pg_advisory_xact_lock pattern already used by every other
advisory lock in this codebase (org membership, table schema/row-ordering,
BYOK keys, workspace env, usage-log flush, execution-log reconciliation).
2026-07-09 14:54:46 -07:00
Waleed ff5353cd83 fix(rich-markdown-editor): highlight text-shift + hover block drag handle (#5543)
* fix(rich-markdown-editor): keep highlight from shifting text

The highlight <mark> added horizontal padding, which pushed the highlighted text (and the
text after it) to the right when the mark was applied. Cancel the padding with an equal
negative margin so the amber tint still bleeds slightly past the text but the text never
moves as a highlight is applied or removed.

* feat(rich-markdown-editor): hover block drag handle (+ / grip)

Adds a left-margin block handle revealed on hover (only when the editor is editable): a + that
inserts a paragraph below the hovered block and opens the slash menu, and a grip that drags to
reorder blocks (via @tiptap/extension-drag-handle) or, on a plain click, selects the block. The
keyboard equivalent of the reorder is Mod-Shift-Arrow (block-mover). The control buttons reset
their own chrome (no default border/background/padding) so they render consistently regardless
of the surrounding reset.
2026-07-09 14:54:13 -07:00
Waleed 6168cff79a fix(icons): fix Meta icon clipping in model dropdown (#5542)
MetaIcon's viewBox (265x165) was smaller than the true bounding box of
its three path elements (287.56x191, computed from the actual path
geometry), so the bottom-right of the mark was silently clipped by the
SVG viewport. It was also non-square, so the dropdown row's forced
14x14 icon slot letterboxed it asymmetrically instead of filling it
like every other provider icon.

Fixed the viewBox to the exact computed bounding box, padded to a
square and vertically centered (0 -48.28 287.56 287.56) - matches the
sibling icon convention (GeminiIcon, etc.) of a square viewBox filling
the icon slot edge to edge. Verified by rasterizing both the old and
new viewBox at the actual deployed 14px size.
2026-07-09 13:57:18 -07:00
Waleed e802da1075 feat(rich-markdown-editor): round-trip gate, VSCode/style paste, highlight, block reorder (#5539)
* fix(rich-markdown-editor): tighten read-only gate for uppercase entities and orphan ref-defs

Two silent-corruption cases the idempotency probe can't see:

- The HTML-entity safe-list used a case-insensitive regex, so `&AMP;`/`&LT;`/`&GT;` were
  treated as the round-trippable canonical entities and let through as editable, but the
  serializer only round-trips the lowercase forms and mangles the uppercase ones. Make the
  safe-list case-sensitive.
- An unused link/image reference definition (`[x]: url` with no `[x]` reference) is dropped
  entirely on serialize, a deletion the idempotency probe misses. Detect orphan definitions
  and open read-only; used definitions still inline losslessly and stay editable. The use
  check tolerates bracket-internal padding (`[ x ]`), and GFM footnote definitions (`[^id]: …`)
  are excluded since they round-trip verbatim.

* feat(rich-markdown-editor): VSCode code paste and strip <style>/<script> from pasted HTML

- Code copied from VSCode carries a `vscode-editor-data` payload with the source language,
  but its text/html is per-token colored spans that ProseMirror flattens into plain
  paragraphs. Read the payload and paste a real fenced code block with the mapped language.
  markdown/plaintext modes resolve to no language and fall through, so markdown copied from
  VSCode still parses as rich content rather than a fenced block.
- Google Sheets and Word prepend a `<style>` block of CSS that PM's DOM parser walks into
  the document as literal text. Strip <style>/<script> in transformPastedHTML before parsing.
- Add a `==…==` inline-mark hint (allowing a lone interior `=`) so a plain-text paste of
  `==highlight==` routes through the markdown parser and becomes a highlight mark.

* feat(rich-markdown-editor): add highlight (==mark==) support

Adds a highlight mark rendered as <mark> and serialized to/from ==text== (Pandoc/Obsidian
syntax). A custom inline tokenizer parses ==text== (inner text parsed as inline markdown so
nested marks like ==**bold**== survive; the body allows a lone `=` so ==a=b== round-trips).
`==` cannot be encoded in the delimiter, so an appendTransaction guard strips the mark from
any text that ends up containing `==` (e.g. a toolbar highlight over a==b), keeping the text
and never emitting the corrupting ==a==b==. Comparison operators (x == y) stay literal.
Wired with an input rule, paste rule, Mod-Shift-H, a bubble-menu button, and themed <mark>
styling. Also locks in mark-stacking round-trips across contexts.

* feat(rich-markdown-editor): keyboard block reordering (Mod-Shift-Arrow)

Mod-Shift-ArrowUp/ArrowDown swaps the current top-level block with its neighbour, carrying
the caret at its original offset (newBefore + offset, no off-by-one), and no-ops at the
document edges. Exposed as moveBlockUp/moveBlockDown commands (the keyboard shortcuts call
them). Pure UI interaction, no schema change. Covered by tests (order, caret offset, edge
no-op, list stays intact).
2026-07-09 13:31:16 -07:00
Waleed 29c01fe40a feat(providers): add Meta Muse Spark 1.1 provider (#5538)
* feat(providers): add Meta Muse Spark 1.1 provider

- New BYOK-only meta provider for Meta's Model API (launched today)
- muse-spark-1.1: 1M context, streaming, tool-calling, reasoning_effort
  (minimal->xhigh), structured output
- Meta icon mark only (no wordmark), theme-safe gradient IDs via useId()
- Not added to hosted models list - BYOK only, no auto-billing

* fix(providers): stop sending unsupported tool_choice values to Meta

Meta's Chat Completions endpoint only supports tool_choice: "auto" -
"none", "required", and named-function choices all return HTTP 400
(confirmed against the official meta-model-cookbook tool-calling
recipe). Never set tool_choice on the request (auto is already the
default; forced-tool usage control degrades gracefully to auto with a
warning log instead of failing every tool-using run), and drop `tools`
entirely from the two post-tool-loop tool-free completion calls
instead of trying to force tool_choice: "none".
2026-07-09 11:40:11 -07:00
Waleed b117758eaa fix(agent): scope nested tool canonical-mode overrides by instance, not type (#5534)
Two tool entries of the same type inside an Agent block's tool-input array
(e.g. two Table tools) shared a single canonical-mode override keyed by
${toolType}:${canonicalId}, so switching basic/advanced mode on one field
silently switched it on every other instance of the same tool type -
including at execution time, where the wrong basic/advanced value could be
resolved for the second tool.

Rescope the override key to the tool's position in its tool-input array
(${toolIndex}:${canonicalId}) instead of its type, and thread that index
through every consumer: the editor (read + write), execution
(agent-handler/providers), search-index, and fork/promote remapping.
2026-07-09 11:21:05 -07:00
Waleed b09e0c0d3b feat(providers): add OpenAI GPT-5.6 Sol, Terra, and Luna models (#5537) 2026-07-09 11:05:46 -07:00
Waleed 5db62b8bdc feat(observability): extend audit log, PostHog, and storage metering coverage (#5269)
* feat(observability): extend audit log, PostHog, and storage metering coverage

Instruments previously-uncaptured resources and actions across the three
observability layers (audit log, PostHog product analytics, usage metering):

- Exfiltration audit: file downloads (workspace/public-share/v1), table,
  workflow, and workspace exports.
- Credential access audited at the token-issuance boundary (success-only).
- Full revenue trail: invoice paid/failed, overage billed, disputes, credit
  fulfillment, subscription lifecycle, plan/seat changes.
- Session/login lifecycle audit via Better Auth hooks (login, blocked sign-in,
  logout, session revoke, account delete).
- v1/admin programmatic surface and copilot tool handlers instrumented.
- Dead audit/PostHog constants wired (lock/unlock, table, custom tool, etc.).
- Storage metering extended to KB documents and copilot files.
- PostHog person identify + workspace/organization group hygiene.

Audit package hardened to null the actor FK for system actors (admin-api) and
adds an awaitable recordAuditNow for pre-delete hooks. All instrumentation is
fire-and-forget and never blocks or breaks the primary operation.

* fix(observability): audit file/credential egress only on success

Address Cursor Bugbot review:
- GET OAuth token: emit CREDENTIAL_ACCESSED/credential_used after
  refreshTokenIfNeeded succeeds (matches POST), not before.
- File export: audit on each actual success exit via a shared helper —
  including the non-markdown serve redirect (previously unaudited) and
  after the zip is generated (previously before asset fetch).

* fix(audit): record null actor for anonymous public-share downloads

Address Greptile P1: setting actorId to the file owner made every anonymous
external download read as a self-download, undermining the exfiltration trail.
recordAudit now accepts a null actor; the public content/inline routes record
actorId=null with the owner in metadata.sharedByUserId and rely on ip/user-agent
for the forensic trail. The misleading owner-attributed file_downloaded PostHog
event is dropped on these anonymous paths.

* fix(observability): audit admin exports after zip; tidy comments

- Admin workflow/workspace ZIP exports now audit only after the archive is
  built (via a local helper), so a zip/build failure no longer logs an export.
- Remove redundant 'success-only placement' inline comments and tighten the
  remaining design-rationale notes (export helper now TSDoc).

* fix(billing): complete the chargeback + overage financial trail

Address Cursor review:
- handleDisputeClosed now records CHARGE_DISPUTE_CLOSED for every closed
  dispute (won/lost/warning_closed), unblocking only on favorable outcomes.
  dispute.status in the metadata distinguishes the outcome, so lost
  chargebacks are no longer missing from the trail.
- Threshold overage now emits OVERAGE_BILLED + overage_billed even when credits
  fully cover the overage (settledVia: 'credits' vs 'stripe'), so credit-settled
  overages are audited instead of silently returning null.

* fix(storage): decrement copilot quota before deleting metadata

Address Greptile P1: deleting the metadata row before the decrement meant a
decrement failure left the quota permanently inflated with no record to retry
from. Decrement first; only remove the metadata row once it succeeds.

* fix(observability): atomic copilot storage release; signup-blocked action

- Copilot file delete now releases storage via a single transaction
  (releaseDeletedFileStorage): the soft-delete is the idempotency claim and the
  decrement shares the transaction, so neither a partial failure (inflated
  counter) nor a retry (double-decrement) can desync the quota. Resolves the
  conflicting Greptile/Cursor ordering findings.
- Policy-blocked sign-ups now record USER_SIGNUP_BLOCKED instead of
  USER_SIGNIN_BLOCKED, so account-lifecycle events aren't mislabeled.

* fix(storage): meter copilot ingest centrally for path symmetry

Address Cursor review: only uploadCopilotFile incremented storage, but copilot
files also enter via the generic upload route and presigned uploads — all of
which persist metadata through insertFileMetadata. Move the increment into
insertFileMetadata (scoped to context='copilot', on genuine insert/restore) so
every ingest path is symmetric with the delete-time decrement, and drop the now
redundant per-path increment in uploadCopilotFile. Other contexts are metered by
their own managers and remain unaffected.

* fix(audit): skip WORKFLOW_EXPORTED when admin export is empty

Address Cursor review: when every requested workflow fails to load, the admin
export still returned 200 but recorded a successful export of zero workflows.
Guard auditExport so an empty result records nothing.

* fix(storage): settle copilot accounting before deleting the blob

Address Cursor review: the blob was removed before releaseDeletedFileStorage, so
a release failure left the counter inflated and the metadata active with the blob
gone. Now the atomic soft-delete + decrement runs first and the blob is deleted
only if it succeeds, so a failure leaves the file fully intact and retryable.

* fix(storage): decrement KB document storage atomically with deletion

Address Cursor review: hardDeleteDocuments deleted the rows then decremented
best-effort, so a decrement failure left billed storage inflated with no row to
reconcile. Resolve each owner's subscription up front, then decrement inside the
same transaction that deletes the embeddings/documents (decrementStorageUsageInTx,
also now shared by releaseDeletedFileStorage), so the counter and the content
commit or roll back together. Connector docs remain excluded.

* fix(audit): don't treat email verification as a login

Address Cursor review: /verify-email could emit USER_LOGIN when verification
mints or refreshes a session, mislabeling (or double-counting) a non-sign-in as
a login. Restrict isLoginPath to genuine sign-in entrypoints.

* fix(audit): null actor FK when the user lookup throws

Address Greptile P1: the catch branch left the original actorId, so a system
actor like 'admin-api' (or a since-deleted user) would FK-violate the insert and
lose the row when the existence lookup errored. Mirror the not-found branch —
null the FK with a readable label — so the audit row always persists.

* revert(audit): drop session/account-lifecycle auth instrumentation

Remove the Better Auth login/logout/session-revoke/account-delete/blocked-signin
audit hooks from auth.ts — they touch sensitive auth paths and are noisy. Also
removes the now-unused taxonomy (USER_LOGIN/_FAILED, USER_SIGNIN_BLOCKED,
USER_SIGNUP_BLOCKED, USER_LOGOUT, SESSION_REVOKED, ACCOUNT_DELETED,
ACCOUNT_EMAIL_CHANGED actions; SESSION/USER resource types) and the awaitable
recordAuditNow helper that only the pre-delete hook used. auth.ts is back to the
staging baseline.

* fix(storage): harden copilot+KB delete accounting against read errors and concurrency

Final-audit follow-ups:
- deleteCopilotFile: a failed metadata *read* (vs a genuine missing row) now
  blocks the blob delete too, so a transient read error can't leave an active
  row un-decremented with the blob gone.
- hardDeleteDocuments: drive the per-user decrement from the delete's
  returning() (the rows this tx actually removed), so two concurrent deletes of
  the same ids can't both decrement.

* chore(observability): drop two unused definitions

Final-audit cleanup: remove the orphaned knowledge_base_searched PostHog event
(never wired; KB search analytics already flow through the OpenTelemetry channel)
and the redundant AuditAction.SUBSCRIPTION_UPDATED (subscription/plan changes are
audited via ORG_PLAN_CONVERTED). Every remaining new action/event has a real
emit site.

* fix(knowledge): key hard-delete result off rows actually deleted

Address Cursor review: hardDeleteDocuments returned existingIds.length (the
requested count) and cleaned storage for the full pre-tx set, even though the
decrement was driven by the rows the transaction actually deleted. Under a
concurrent delete that claimed some ids first, that overstated the result and
re-touched storage for rows this call didn't delete. Drive the storage cleanup,
log, and return value from deletedDocs (the returning() rows) so all four are
consistent.

* fix(storage): gate copilot quota on all ingest paths; tidy inline comments

- Copilot uploads via the generic /api/files/upload route and presigned URLs now
  run checkStorageQuota before writing (matching uploadCopilotFile), so no copilot
  ingest path can grow usage past the plan limit. The central increment stays in
  insertFileMetadata.
- Trim/remove redundant inline comments across the diff; keep only concise notes
  on non-obvious decisions (left pre-existing comments untouched).

* fix(analytics): omit empty workspace_id on workspace-less file downloads

Address Greptile P1: the generic key-based /api/files/download route and the
no-workspace markdown-export path emitted file_downloaded with workspace_id:''
creating a phantom '' bucket in PostHog. Make workspace_id optional on the event
and omit it when there is no workspace (workspace-scoped routes still pass it).

* fix(analytics): omit empty workspace_id/workflow_id on copilot_chat_sent

Final-sweep nit: copilot_chat_sent sent '' for workspace_id/workflow_id in the
agent (workspace-less) branch, same phantom-bucket issue as file_downloaded.
Make both properties optional and omit them when absent.

* fix(analytics): clear stale org PostHog group on personal workspaces

Address Cursor review: switching from a team workspace to a personal one left
the previous organization group set, so later events kept rolling up under it.
When the active workspace has no organizationId, resetGroups() to drop the stale
org group, then re-apply the workspace group.

* fix: address review — export audit timing, copilot delete signal, group reset

- Table export (Cursor MED): audit fires before streaming begins, not after
  controller.close(), so a mid-stream failure still records the partial export.
- deleteCopilotFile (Greptile P1): throw when storage accounting can't be settled
  instead of silently returning, so callers can detect the file was not deleted.
- workspace-scope-sync (Cursor MED): only resetGroups() once workspace metadata is
  loaded (activeWorkspace present), so a team workspace doesn't transiently lose
  its org group while organizationId is still null during load.

* refactor(observability): final line-justification cleanup

Address final audit flags:
- Table import: move the 'columns added' audit OUT of the import transaction
  into a post-commit auditTableColumnsAdded() helper called by the three
  tx-owning callers, so a mid-import row-batch rollback no longer logs a false
  'added N columns' (matches the PR's success-only discipline).
- Omit empty-string analytics dimensions on workflow_lock_toggled (workspace_id)
  and organization_created (name), consistent with file_downloaded/copilot_chat_sent.
- Restore an unrelated capacity-check comment removed incidentally.
- Move copilot_chat_sent emit above the traceparent comment so the comment sits
  with the code it documents.

* fix(table): attribute import column audit to the importing user

Address Cursor review: addImportColumns (async createColumns import path) now
threads the importing userId into auditTableColumnsAdded instead of falling back
to table.createdBy, so column additions are attributed to the actual member who
ran the import rather than the table creator.

* feat(observability): drop copilot from storage accounting

Per design decision: copilot files are working/conversational artifacts, so
gating their materialization on storage quota would fail an agent operation
mid-flow when a user is over limit, and metering them would inflate usage enough
to block KB/workspace uploads indirectly. Remove copilot quota gates + copilot
ingest metering entirely (revert copilot-file-manager, metadata, and the upload
route's copilot branch to baseline) and drop the now-unused releaseDeletedFileStorage.
KB document metering + quota enforcement (the deliberate, persistent storage path)
is unchanged.

* fix(analytics): switch workspace + org PostHog groups together

Address Cursor review: gate the group-sync effect on workspace metadata being
loaded (activeWorkspace present) so the workspace and organization groups always
update atomically. Acting during the load window paired the new workspace group
with the previous workspace's org group; until metadata loads, events stay
consistently attributed to the previous workspace.

* fix(table): audit async export at authorization, not job completion

Address Cursor review: async exports only emitted TABLE_EXPORTED when the
background job reached 'ready', so an authorized export whose job later failed or
was abandoned left no audit trail — inconsistent with the sync export route,
which audits before streaming. Move the audit + analytics to the async route's
authorization point (after the job is claimed/dispatched) and remove it from the
runner. Drop the now-unused userId from TableExportPayload.

* fix(billing): never let payment_failed instrumentation skip user blocking

Address Greptile P1: the payment_failed audit hoisted an unguarded
isSubscriptionOrgScoped DB read (for entity_type) directly before the
attempt-count user-blocking block. A transient failure of that read would throw
out of the handler and skip blocking. Wrap the whole audit/analytics block in
try/catch (best-effort), and let the blocking compute its own isSubscriptionOrgScoped
as it did originally — instrumentation can no longer abort payment processing.

* chore(observability): trim verbose inline comments to concise notes

* fix(billing): wrap new dispute/enterprise/subscription instrumentation in Stripe webhook idempotency

recordAudit/captureServerEvent calls added for charge disputes, enterprise
subscription provisioning, and free->paid subscription creation ran
unconditionally with no idempotency guard, unlike their sibling handlers
in the same files. Stripe redelivers webhooks at-least-once, so a retry
would double-record the audit row and PostHog event even though the
underlying DB writes were already idempotent.

* fix(observability): tag org-scoped audit metadata with organizationId, guard concurrent table delete

The org-scoped self-service audit-log endpoint matches org-level rows via
metadata.organizationId (or resourceType=organization). Six recordAudit
call sites (subscription create/cancel, admin credit issuance, threshold
overage billing, charge disputes, credit purchase, invoice payment
succeeded/failed) tagged org-scoped events with a differently-named key
(referenceId/entityId/targetOrgId), making them invisible to org admins
querying their own audit trail despite being stored in the DB. Add the
missing organizationId key everywhere the pattern was missed.

Also guard deleteTable's archive UPDATE with isNull(archivedAt) so a
concurrent duplicate delete request is a no-op instead of re-archiving
and re-firing a duplicate TABLE_DELETED audit row.

Adds test coverage for the ORG_MEMBER_ADDED audit/analytics emission in
acceptInvitation, which previously had none.

* fix(test): queue resolveBillingActorId's owner lookup in payment-failure email test

handleInvoicePaymentFailed's new payment_failed audit instrumentation
resolves the billing actor via an extra db.select before
sendPaymentFailureEmails runs. The test's fixed select-response queue
didn't account for it, so the org-admin lookup consumed the wrong
queued row and the assertion saw zero email sends. Production behavior
is unaffected — each query is independent; this was a mock-queue
ordering issue only.

* fix(billing): don't let a post-commit usage-limit sync failure suppress the seat audit

Cursor Bugbot flagged: reconcileOrganizationSeats committed the seat
change and Stripe outbox enqueue in a transaction, then called
syncSubscriptionUsageLimits outside it before recording the audit/
PostHog events. A thrown sync left a genuinely-changed seat count with
no ORG_SEAT_PROVISIONED/DEPROVISIONED trail. Wrap the sync in its own
try/catch (log + continue) so the events always fire for a committed
change, matching the fire-and-forget instrumentation pattern used
elsewhere in this PR.

* fix(billing): guard the new subscription-created instrumentation block

Greptile P1: the org-scope check I added for the audit-metadata fix
(isSubscriptionOrgScoped) was a raw DB call with no error guard, unlike
resolveSubscriptionActorId next to it. Since it ran inside the
idempotency lambda with an unconditional rethrow, a transient DB error
would abort and retry the whole webhook after the free -> paid usage
reset had already committed. Wrap the actor/org-scope resolution +
audit + analytics block in try/catch, matching the same guarded
instrumentation pattern already used in handleInvoicePaymentFailed.
2026-07-08 23:23:59 -07:00