Commit Graph
3573 Commits
Author SHA1 Message Date
Theodore Li e08fb022e6 perf(trigger): cap concurrency on background DB tasks (#5231)
* perf(trigger): cap concurrency on background DB tasks

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

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

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

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

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

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

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

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

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

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

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

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

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

The trigger credential fields (RevenueCat/Rootly apiKey, Twilio authToken) are
user-only, but the same-id tool-level block fields were not, so the stored
secret stayed reachable as an LLM-visible block parameter. Mark those block
credential fields paramVisibility 'user-only' too (matching instantly.ts) so the
secret is user-only on every path. accountSid is an identifier, not a secret, so
it is left as-is.
2026-06-26 18:50:32 -07:00
Waleed 3143a15dde feat(uptimerobot): add UptimeRobot v3 integration (#5229)
* feat(uptimerobot): add UptimeRobot v3 integration

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

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

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

* fix(uptimerobot): spec-conformance audit fixes

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

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

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

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

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

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

Reject successful PSP responses that lack a positive numeric id and non-empty
friendlyName (a {} or metadata envelope) with a controlled 502, instead of
mapping a phantom status page.
2026-06-26 18:38:50 -07:00
Waleed 35acc42d2b feat(downdetector): add Downdetector outage-monitoring integration (#5228) 2026-06-26 18:25:31 -07:00
Waleed c7eda5b217 feat(rich-editor): rich markdown field + @ mentions for skill & deploy modals (#5215)
* feat(rich-editor): rich markdown field + @ mentions for skill & deploy modals

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

- RawMarkdownField now honors onPasteText (e.g. skill SKILL.md destructuring), so a full-document
  paste is intercepted in the raw fallback too, not only the WYSIWYG path.
- Bound the @-mention list while filtering (MAX_WHEN_FILTERED) so lifting the per-group cap for search
  can't render thousands of rows in the non-virtualized menu on a broad query; search still reaches
  deep matches well before the bound. Adds a test.
- Tighten an extensions.ts doc comment (the headless path omits the registry + node-view construction,
  not React itself).
2026-06-26 17:44:24 -07:00
Waleed 954de0559b improvement(docs): align components with the platform design system (#5227)
* improvement(docs): align components with the platform design system

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

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

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

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

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

- add a canonical `primary` chip variant (inverse fill: dark in light
  mode, white in dark mode), mirroring the emcn chip's primary action
- "Get started" and the 404 "Go home" now use variant='primary' instead
  of the green brand surface
- retire the now-unused `brand` chip variant (no parallel path left behind)
- fix navbar right-cluster spacing: gap-2 to match the landing navbar and
  drop the asymmetric ml-1 on the CTA
2026-06-26 17:24:09 -07:00
Theodore Li a33c173146 fix(copilot): strip hosted apiKey on type-less edit ops + guard hosting.enabled (#5220)
* fix(copilot): strip hosted apiKey on type-less edit ops + guard hosting.enabled

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Pass 1 recorded any non-empty type into finalType, but apply skips type changes to unknown types (keeps the existing block). An unknown type on an earlier op could poison a later type-less apiKey edit. Only advance finalType to a getBlock-resolvable type so the fallback matches what apply persists. Test covers empty and unknown types.
2026-06-26 12:56:17 -07:00
Waleed 02d254e267 refactor(emcn): consolidate date pickers onto the chip Calendar (range support + retire legacy DatePicker) (#5222)
* improvement(knowledge): use canonical ChipDatePicker in document tags modal

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Normalize all nav descriptions to a consistent voice and length.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

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

Generalize the strip to key off tool.hosting — the same canonical signal injectHostedKeyIfNeeded uses at execution. Resolve the block's active tool via its tools.config.tool selector, honor the per-provider enabled gate, and strip the exact hosting.apiKeyParam field (no hardcoded 'apiKey' assumption). Surfaces the existing non-fatal note to the agent. Self-hosted and non-hosted providers untouched.
2026-06-25 19:03:30 -04:00
Will ChenandClaude Opus 4.8 d20deedf99 improvement(docs): add Academy learning surface (#5213)
Adds the Academy section to the docs: video-first lessons (self-hosted MP4 on
Vercel Blob), organized into Workflows, Agents, Tables, Files, and Knowledge
Bases, each linking back to the reference docs. Lessons use a course layout
(hero video with chapter seek, "what you'll learn", block diagrams).

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

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

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

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

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

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

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

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

Addresses review feedback that one shared status dropdown mixed user
lifecycle values (active/inactive/expired/new) with enrolment values
(archived/complete/open/...). Use separate userStatus and enrolmentStatus
dropdowns (no canonicalParamId) remapped to the tool's 'status' param so each
operation only offers valid options.
2026-06-25 15:26:13 -07:00
Siddharth Ganesan 7640af0f71 improvement(mothership): add workflow lint for custom tool/skills/mcp tool additions to agent block (#5199)
* improvement(workflow-linter): added custom tool validation to workflow linter

* fix(comments): address pr comments

* improvement(validation): ensure type is known
2026-06-25 14:47:58 -07:00
Theodore Li a68d38ae75 feat(db): attribute Postgres connections by runtime via application_name (#5211)
* feat(db): attribute Postgres connections by runtime via application_name

* improvement(db): label migration-runner connection sim-migrate; trim DB_APP_NAME comment

* fix(db): label realtime's shared @sim/db connections sim-realtime too

The realtime process uses both its own socketDb pool and the shared @sim/db
client (handlers, preflight, permissions). Only socketDb was labeled, so the
shared client defaulted to sim-app, mislabeling much of realtime's DB traffic.
Set DB_APP_NAME=sim-realtime at the process level (bootstrap before the dynamic
@/index import for prod; dev/start scripts for local) so both clients report it.
2026-06-25 17:11:23 -04:00
Waleed 9e1a4ac5d1 perf(frontend): bound logs DOM, kill editor re-render storms, lazy-load heavy deps (#5212)
* perf(logs): virtualize the resource list to bound DOM + memory

* perf(editor): narrow per-block store subscription to kill structural-edit re-render storm

* perf(realtime): move presence state out of the socket context to stop cursor-frame re-renders

* perf(editor): lazy-load NoteBlock so Streamdown is off the editor's critical path

* perf(emcn): defer prismjs in Code so it's off the shared barrel's static graph
2026-06-25 13:58:51 -07:00
Waleed 34d32b97dd feat(salesforce): add Tooling API schema tools (custom field/object) + metadata query (#5209)
* feat(salesforce): add Tooling API schema tools (custom field/object) + metadata query

Add salesforce_create_custom_field, salesforce_update_custom_field,
salesforce_delete_custom_field, salesforce_create_custom_object, and
salesforce_tooling_query so the connector can make schema/metadata changes
(e.g. create a custom field on Account). Previously the integration only did
record CRUD via the REST Data API. Existing `api` OAuth scope covers the
Tooling API; metadata creation is profile-permission gated, so no scope change.

Also: fix Opportunity closeDate being wrongly required on update_opportunity,
make list_reports/list_dashboards descriptions honest (recently-viewed scope),
and document run_report's includeDetails default.

* improvement(salesforce): non-destructive custom field update + align metadata param types

- update_custom_field now does a read-modify-write (GET existing Metadata,
  overlay only provided changes, PATCH) so omitted properties are preserved
  instead of being reset by the Tooling API's full-metadata PATCH; no more
  fabricated label or injected create-time defaults on update
- fieldType is now optional on update (kept from the existing field unless changed)
- widen length/precision/scale/visibleLines param types to number | string to
  match the tool param configs (type: number)

* improvement(salesforce): preserve picklist values and clear stale metadata on field type change

- custom field update now unions provided picklist values with the field's
  existing values instead of replacing the whole valueSet (no data loss)
- when fieldType changes on update, drop the prior type's type-specific
  metadata (length/precision/scale/visibleLines/valueSet/defaultValue/unique/
  externalId) and backfill the new type's required defaults

* improvement(salesforce): scope custom field update to attributes, never the type

update_custom_field no longer changes a field's data type: Salesforce treats a
type change as a separate conversion operation, and a stale forwarded fieldType
could otherwise trigger an unintended destructive migration. The merge keeps the
field's existing type and overlays only the other provided properties, dropping
the type-change/stale-metadata-stripping logic entirely.
2026-06-25 10:37:05 -07:00
Waleed e748a64ff4 refactor(realtime): type the socket event-handler boundary with @sim/realtime-protocol (#5208)
* refactor(realtime): type the socket event-handler boundary with @sim/realtime-protocol

Replace the (data: any) event-handler types in socket-provider.tsx with
precise broadcast types that mirror the exact payloads emitted by the
realtime Socket.IO server (apps/realtime/src/handlers/** and rooms/**).

Add @sim/realtime-protocol/events with the canonical wire types for the
broadcast/confirmation events the server emits: WorkflowOperationBroadcast,
SubblockUpdateBroadcast, VariableUpdateBroadcast, CursorUpdateBroadcast,
SelectionUpdateBroadcast, the four workflow-lifecycle broadcasts, and
OperationConfirmed/Failed. Typing change only; zero runtime/logic changes.
Store-internal any (rehydrate state, subblock map, emit payloads) is left
untouched as out of scope.

* fix(realtime): type cursor-update broadcast cursor as nullable

The client emits 'cursor-update' with { cursor: null } when a remote user's
cursor leaves the canvas, and the server re-broadcasts it verbatim, so receivers
genuinely get cursor: null. Type CursorUpdateBroadcast.cursor as
CursorPosition | null to match the wire. (selection stays non-null — it signals
absence via type: 'none', never null.)
2026-06-24 20:43:52 -07:00
Waleed 9bd8f149a5 fix(workspace): add granular error boundaries to 7 more workspace segments (#5207)
Adds error.tsx (reusing the shared ErrorState) to home, integrations,
knowledge/[id], skills, settings, scheduled-tasks, and chat/[chatId] so a
crash in any of these panels stays scoped to the panel and offers a retry,
instead of bubbling to the generic workspace-level boundary.
2026-06-24 20:38:04 -07:00
Waleed 6260eda226 fix(ssr): harden credential query-key factory + fetchers against the 'use client' stub bug (#5206)
* fix(ssr): move credential query-key factory + fetchers to non-client modules

Preventively closes the same 'use client' SSR client-reference-stub class that
crashed the tables page. Server-evaluated modules (the credential block def, the
workflow-comparison helpers) imported workspaceCredentialKeys /
fetchWorkspaceCredentialList / fetchCredentialSetById from 'use client' hook
modules, where they resolve to client-reference stubs on the server (a future
server call path would throw 'X is not a function').

Extract them into non-client hooks/queries/utils/{credential-keys,
fetch-workspace-credentials,fetch-credential-set}.ts (mirroring folder-keys.ts /
fetch-workflow-envelope.ts) and import from there. No behavior change — these
values were only ever called from browser paths.

* docs+ci: codify the 'use client' server-import rule + add check:client-boundary

Document the Next.js rule that server code can only render a 'use client'
export as a component, never call it (server imports resolve to client-reference
stubs that throw — the tables-page crash). Add the rule to
.claude/rules/sim-queries.md + a cross-ref in sim-architecture.md.

Add scripts/check-client-boundary-imports.ts (wired into CI as check:client-boundary)
that flags any value import from a 'use client' module in a server-evaluated,
non-JSX surface (prefetch / route handler / trigger / block definition), so this
class can't silently recur. Escape hatch: // client-boundary-allow: <reason>.
2026-06-24 18:46:18 -07:00
Theodore Li cff7a49310 feat(file): workspace-scoped inline images + public-share cascade (#5203)
* feat(file): workspace-scoped inline images + public-share cascade

Embedded markdown images now resolve only within the document's workspace,
and public file shares cascade to the images the shared document embeds.

- New /api/workspaces/[id]/files/inline (in-app, workspace-scoped) and
  /api/files/public/[token]/inline (public cascade) routes; the public one
  serves an embed only when it is referenced-by-doc, same-workspace, and
  passes a magic-byte image sniff
- Embed srcs (serve-key and view-id forms) rewrite through one scoped inline
  route; one shared isomorphic parser owns the embed grammar for both the
  frontend renderer and the server doc scan
- Accept wf_ file ids on the view/export routes (were 400ing on .uuid())

* feat(file): add Image command to the markdown editor slash menu

- New /Image slash command uploads an image via a file picker and inserts
  it at the caret (same upload+insert path as paste/drop)
- Inserted src is the workspace serve URL, so it renders in-app and
  cascades to public shares like any other embed
- Per-editor handler wired through slash-command storage (the extension
  set is a shared singleton); only active when the editor is editable

* fix(file): export rewrites all embed forms; cap embedded refs combined

Addresses PR review:
- Markdown export now rewrites the in-app `/workspace/<ws>/files/<id>`
  embed form too (not just `/api/files/view/<id>`), so a bundled asset
  never leaves a broken link in an offline export (Bugbot)
- extractEmbeddedFileRefs bounds total references (keys + ids) to 50
  combined rather than 50 each, matching MAX_EMBEDDED_IMAGES intent
2026-06-24 21:07:39 -04:00
Waleed ae4bc05e60 feat(gitlab): add repository, code-review, and CI job tools + validation fixes (#5205)
* feat(gitlab): add repository, code-review, and CI job tools + validation fixes

Expand the GitLab integration with 12 new tools (all host-aware via
getGitLabApiBase, wired through types/index/registry/block):
- Repository: list_repository_tree, get_file, create_file, update_file,
  create_branch, list_branches, list_commits
- Code review: get_merge_request_changes, approve_merge_request
- CI jobs: list_pipeline_jobs, get_job_log, play_job

Validation fixes from /validate-integration:
- Correct the block inputs key (credential -> accessToken) so it matches the
  subBlock id and the params the block reads
- Trim projectId before encoding in all tool request URLs (input hygiene)

/validate-connector and /validate-trigger passed clean against the GitLab REST
API v4 docs — no changes required.

* fix(gitlab): address review feedback + regen docs

- get_merge_request_changes: use the /diffs endpoint (/changes was removed in
  GitLab 18.0); return the diff array + count (drops the MR envelope that /diffs
  no longer provides), fetch max page size in a single call
- create_file/update_file: send explicit `encoding: 'text'` for clarity
- Remove `// =====` separator comments from types.ts (repo convention)
- Regenerate GitLab integration docs + catalog for the 12 new tools
2026-06-24 18:05:40 -07:00
Waleed c3a09694ab fix(tables): SSR crash from tableKeys in a 'use client' module + drop redundant flushChunks (#5204)
* fix(tables): move tableKeys to a non-client module so the SSR prefetch works

The tables list page crashed at SSR ('tableKeys.list is not a function') because
tables/prefetch.ts (a server component) imported tableKeys from
hooks/queries/tables.ts — a 'use client' module whose exports resolve to
client-reference stubs on the server. Extract the key factory into
hooks/queries/utils/table-keys.ts (no 'use client'), mirroring folder-keys.ts,
and import it from there in the prefetch, hook, trigger, and consumers.

* refactor(chat): drop redundant flushChunks on the SSE error path

On an error 'final' event the reader stops via return true, so the post-loop
flush is the single flush point. Defer the error append to after that flush
(single flush, correct ordering) instead of flushing inside onEvent and again
post-loop. No behavior change.

* fix(sse): process the final unterminated line on stream end

readSSELines broke out of the read loop on 'done' without flushing the
TextDecoder or processing the trailing buffer, so a final 'data:' line not
terminated by a newline (and any buffered multi-byte character) was dropped.
Flush the decoder on end-of-stream and process the remaining buffer.
Addresses a Cursor Medium finding on the consolidated SSE reader.
2026-06-24 17:37:37 -07:00
Theodore Li 5a938e5d56 improvement(sandbox): mount workspace files by presigned URL instead of buffering bytes (#5202)
* improvement(sandbox): mount workspace files by presigned URL instead of buffering bytes

Files and directories mounted into the function_execute sandbox were downloaded into the web process, re-encoded, and shipped inline. Mirror the table-snapshot path: under cloud storage, presign each file and let the sandbox curl it directly (no web-heap transit). Local storage keeps the buffered fallback.

Add a count cap on the inputFiles list and a generous aggregate URL-mount byte ceiling so oversized requests fail fast instead of filling sandbox disk.

* improvement(sandbox): use mount path in size-limit errors, display GB, add directory local-fallback test
2026-06-24 19:29:00 -04:00
Theodore Li e1c3c7f6c9 feat(secrets): ingest env secrets at container runtime instead of fanning into ECS taskdef (#5189)
* feat(secrets): ingest env secrets at container runtime instead of fanning into ECS taskdef

The app/socket ECS taskdefs were ~42KB, ~93% of which was the secrets[] array:
268 pointer entries each restating the full ~78-char secret ARN, marching toward
the 64KB taskdef limit and growing ~150 bytes per hosted key added. The secret
blob itself is only ~18KB/268 keys.

Move secret delivery to container boot: new @sim/runtime-secrets loadRuntimeSecrets()
reads SIM_ENV_SECRET_ID, fetches the combined secret once, and hydrates process.env
(no-clobber, no-op when unset, fail-fast). Bootstrap entrypoints for app + realtime
await it before importing the real server (env-flags reads env at module load). The
app bootstrap is bun-bundled in the Dockerfile builder stage since it runs outside
the Next standalone bundle; realtime keeps full node_modules and runs the TS entry.

Backward-compatible: with the current fan-out taskdef the loader no-ops and the app
reads the injected env vars unchanged. The matching infra change (empty secrets[] +
SIM_ENV_SECRET_ID) ships separately, after this image is live.

* fix(runtime-secrets): address review feedback

- Move the binary-secret guard outside the retry loop (sendWithRetry) so a
  missing SecretString throws immediately instead of burning 3 attempts + backoff.
- Bound each Secrets Manager request with AbortSignal.timeout(5s) so a stalled
  response can't hang boot indefinitely.
- Drop the redundant @aws-sdk/client-secrets-manager pin from apps/realtime; it
  resolves transitively via @sim/runtime-secrets.
- Add a test for the non-retriable binary-secret path.
2026-06-24 16:23:37 -04:00
Waleed 067f9e9dc9 feat(gitlab): support self-managed GitLab host across tools, block, triggers, webhook, and connector (#5200)
* feat(gitlab): support self-managed GitLab host across tools, block, triggers, webhook, and connector

Add an optional `host` so the GitLab integration can target a self-managed
instance (e.g. gitlab.example.com) instead of gitlab.com. Defaults to
gitlab.com everywhere, so existing workflows, blocks, triggers, and stored
webhooks are unchanged.

- Shared host helper (normalizeGitLabHost/getGitLabApiBase) used by all 19
  tools, the block, triggers, the webhook provider, and the connector
- SSRF hardening: reject structurally unsafe hosts (userinfo `@`, whitespace,
  control chars, embedded path/query, empty labels) before the token-bearing
  request is built; allow self-managed hosts, ports, and IDN punycode
- Route the webhook provider's previously-raw fetches through
  secureFetchWithValidation (DNS + private-IP rejection + IP pinning), matching
  the tool and connector paths
- Add regression tests for the host validator

* fix(gitlab): handle unsafe-host errors gracefully in webhook provider

Address review feedback:
- Validate the optional self-managed host up front in createSubscription and
  deleteSubscription so a structurally unsafe value surfaces as a clear error
  (create) or a graceful non-strict skip (delete) instead of an unhandled
  UnsafeGitLabHostError, mirroring the connector's handling
- Document the layered SSRF defense: bare IP literals pass the structural host
  guard by design and are rejected at the fetch layer (validateUrlWithDNS); add
  a confirming test group making that intent explicit

* fix(slack): drop assistant:write scope pending app review approval

Requesting assistant:write before Slack approved it fails the OAuth/install
flow for users. Remove it from both request paths until approval lands:
- Remove from the user OAuth scope list (oauth.ts), matching the existing
  users:read.email TODO pattern
- Remove the action_assistant capability from the bot manifest generator
  (capabilities.ts), leaving a TODO to restore it after approval

The set_status/set_title/set_suggested_prompts tools remain and surface their
existing graceful "reconnect with assistant:write" message until re-enabled.
2026-06-24 11:37:06 -07:00
Waleed 038e8f0d84 refactor(sse): consolidate client SSE readers behind a single typed primitive (#5195)
Replace four hand-rolled client SSE decode loops with two layered
primitives in lib/core/utils/sse.ts:

- readSSELines: the single byte-stream decode engine. Splits on \n,
  strips trailing \r, tolerates data: with/without a leading space,
  skips the [DONE] sentinel, honors an AbortSignal before each chunk and
  between events, and releases the reader lock only when it acquired it.
- readSSEEvents<T>: a thin JSON layer that parses each payload and routes
  unparseable lines to onParseError (default: skip).

An SSESource union accepts a Response, a ReadableStream, or an
already-acquired reader so callers that must stash the reader for
external cancellation keep ownership of the lock.

Migrates use-execution-stream, chat use-chat-streaming, home use-chat
(via readSSELines for schema-validated decode), and the workflow chat
panel. Legacy server/wand exports (encodeSSE, SSE_HEADERS,
readSSEStream) are untouched. Behavior is preserved across abort, RAF
batching, TTS, [DONE], delimiter tolerance, and reader-lock ownership.

Tests in sse.test.ts pin the prior behavior: \n and \n\n framing,
mid-chunk splits, [DONE], data: with/without leading space, \r\n
stripping, sync/async early-stop, pre-aborted and mid-stream abort,
lock release/non-release per source, lock release on a throwing
handler, and Response/stream/reader sources.
2026-06-24 10:46:18 -07:00
Waleed b52fcc094e refactor(stores): model execution and workflow-diff state as status enums (#5197) 2026-06-24 10:45:33 -07:00
Waleed 86dc04d789 perf(workspace): server-prefetch home, knowledge, tables, and files list pages (#5196) 2026-06-24 10:44:57 -07:00
Waleed b212a5d37d improvement(mistral): update OCR pricing to OCR 4 rate ($4/1,000 pages) (#5193)
* improvement(mistral): update OCR pricing to OCR 4 rate ($4/1,000 pages)

* docs(mistral): document mistral-ocr-latest alias resolves to OCR 4
2026-06-24 00:12:20 -07:00
Vikhyath Mondreti 5d7f7e900e improvement(pi): minor improvements to docs (#5192) 2026-06-24 00:04:32 -07:00
Theodore Li 8c5da02761 feat(file): include public share status in File read output (#5191)
* feat(file): include public share status in File read output

The read operation now attaches each workspace file's public share status as a
"share" field (the share record, or null when not shared), batch-fetched via
getSharesForResources to avoid N+1. Picker/upload input files have no canonical
id and carry share: null.

* refactor(file): read share status uses visibility vocabulary, no row internals

Read's per-file "share" is now { visibility, url, allowedEmails } using the
same visibility vocabulary as Manage Sharing: 'private' when not shared (url
null, no config) instead of null, otherwise public/password/email/sso with the
link. Drops row internals (id, token, resourceType, resourceId, isActive,
hasPassword).

* fix(file): mark picker files private without a share lookup

Input (picker/upload) files only have a synthetic id (storage key/URL), so
looking them up in the shares map could collide with a canonical file id and
attach the wrong share. Give them an explicit private share instead.

* docs(file): terser share-status output descriptions

* chore(file): drop verbose comment in read share enrichment
2026-06-24 02:06:31 -04:00
Theodore Li 4554df9f77 feat(file): add Manage Sharing operation to the File block (#5177)
* feat(file): add Set File Sharing operation to the File block

Adds a new file_set_sharing operation to the File block (file_v5) that
idempotently enables/disables a file's public share link and sets its
access mode (public, password, email, SSO). The set_sharing route case
reuses upsertFileShare, requires write/admin, gates enabling through the
EE public-sharing policy, and records a share audit. Returns an empty url
when set to private so a disabled link isn't handed back as a dead link.

* fix(file): harden set_sharing — explicit isActive, agent-controllable params, policy gate + perm-check ordering

Addresses review findings:
- Make isActive explicit/required so a bare call no longer silently enables a public link
- Expose isActive/authType/allowedEmails as user-or-llm so agents can disable/configure shares (password stays user-only)
- Resolve authType from the existing share before the EE policy gate to close a re-enable bypass
- Run the write/admin permission check before the file lookup to remove a file-existence side channel

* refactor(file): rename file operation to Manage Sharing

Renames the file_set_sharing operation to file_manage_sharing (route literal
manage_sharing, tool Manage Sharing) across the contract, route, tool, block,
registry, and tests.

* fix(file): complete Manage Sharing rename in tools barrel

Prior commit's lint-staged dropped the barrel re-export update, leaving
index.ts importing the deleted set-sharing module and breaking the block
registry check. Point the barrel at manage-sharing.

* fix(file): require isActive in tool params type, reject multiple files in manage sharing

- FileManageSharingParams.isActive is now required, matching the tool param
  and contract (no compile-time gap that 400s at runtime)
- manage_sharing rejects multiple canonical file IDs instead of silently
  sharing only the first, matching decompress

* fix(file): resolve basic-picker files for manage sharing

The basic file-upload picker stores a workspace file as { name, path, key,
size, type } with no canonical id, so manage_sharing failed those picks with
'Could not determine the file to share'. The block now passes the picked
object as fileInput when it lacks an id, and the route resolves the canonical
id from the storage key via getFileMetadataByKey. Contract accepts fileId OR
fileInput (mirroring read/get content).
2026-06-23 21:22:10 -04:00
Vikhyath Mondreti 8b5d746fe2 improvement(access-controls): ui/ux improvements (#5190)
* improvement(access-controls): ui/ux improvements

* remove unused col
2026-06-23 17:35:41 -07:00
Theodore Li 43fa5eaa19 feat(data-retention): workspace-level overrides for retention and PII (#5186)
* feat(data-retention): workspace-level overrides for retention and PII

* fix(data-retention): hide unmanageable PII rows when flag off, scope override workspace IDs to org, dedupe key type

* improvement(data-retention): unify org default and workspace overrides into one policy list

* fix(data-retention): clean up overrides for workspaces deselected during edit
2026-06-23 18:41:03 -04:00
Theodore Li c20d5fc70d fix(enrichment): stop PDL billing on no-match via required-field gating (#5184)
PDL bills per matched profile, but each cascade only counts a hit when mapOutput yields a specific field. A confident profile (likelihood >= 6) lacking that field was billed yet recorded as no_match. Pass PDL's required param so it 404s (free) when the extracted field is absent, aligning PDL's billing unit with the cascade's success unit.
2026-06-23 15:37:55 -04:00
Siddharth Ganesan bf5077bf24 fix(skills): fix skills icon showing up (#5187) 2026-06-23 12:21:32 -07:00
Theodore Li 406ae92b84 fix(trigger): mark cpu-features external to fix deploy build (#5185)
ssh2 became reachable from the trigger background bundle via the Pi
local SSH backend (#5178). esbuild then tried to bundle ssh2's optional
native dep cpu-features and failed on the missing .node binary. ssh2
requires cpu-features inside a try/catch, so externalizing it is safe.
2026-06-23 11:49:25 -07:00
Waleed 77976bcb8b feat(billing): unify upgrade routing with reason context + storage/tables limit emails (#5171)
* feat(billing): unify upgrade routing with reason context + storage/tables limit emails

* fix(billing): re-arm limit-notification dedup on usage drops (prior-usage + decrement)

* fix(billing): isolate per-admin email failures in org limit notifications

* fix(billing): re-arm limit dedup at zero usage and zero prior usage (full clear / wipe-rebuild)

* fix(billing): make storage-decrement notification re-arm only (never send on a shrink)

* fix(billing): resolve recipients before claiming so opt-outs don't burn the dedup threshold

* fix(billing): fire table limit emails on upsert inserts via shared notifyTableRowUsage

* chore(billing): only log a limit email as sent when a recipient actually received it

* chore(billing): match to_jsonb int cast between claim and re-arm for consistency

* fix(billing): notify table limits post-commit so a rolled-back insert never emails or burns the claim

* feat(pi): swap Pi Coding Agent icon to the pi glyph and use a black bgColor

* fix(billing): drop priorUsage re-arm to make dedup a single atomic claim (no duplicate-email race)

* docs(billing): move limit-notification rationale to TSDoc, correct tables warn-once behavior

* docs(db): note limit_notifications dedup is per-account, not per-table

* perf(billing): cut redundant subscription fetches and edge-gate notify to slash DB load

* docs(billing): drop self-explanatory inline comments from the notification path
2026-06-23 10:51:27 -07:00
Theodore Li 8f312d299b feat(guardrails): PII redaction via Presidio sidecar (native VIN, per-rule language) (#5174)
* fix(logs): run PII redaction over HTTP and fix Presidio provisioning

- resolve the guardrails venv via candidate paths and fail fast instead of
  silently falling back to system python3 (the misleading "Presidio not
  installed" that broke redaction and the guardrails block in deployed runtimes)
- install the en_core_web_lg spaCy model in setup.sh and app.Dockerfile
- route log redaction through an internal /api/guardrails/mask-batch endpoint
  so Presidio always runs in the app container, including async executions that
  persist inside the trigger.dev runtime

* fix(guardrails): chunk + time-bound internal PII mask requests

- chunk maskPIIBatchViaHttp by count (2000) and bytes (256KB) so large
  executions split across requests and never hit the contract's 100k cap
- add AbortSignal.timeout(45s) per request so a slow/unreachable app container
  aborts and the caller scrubs, instead of hanging the trigger.dev job
- catch maskPIIBatch failures in the route: log and return a structured 500
  (broken venv fails loudly server-side; caller still scrubs, no leak)
- add mask-client tests (order across chunks, count split, non-2xx, empty)

* fix(guardrails): mint internal token per mask request

A single token (5min TTL) could expire mid-batch when a large execution
fans out into many sequential chunk requests; mint one per request instead.

* feat(guardrails): run PII via Presidio sidecars + TS recognizer registry

- replace the per-call python3 subprocess (cold spaCy load every call) with
  two long-lived Presidio sidecars (analyzer + anonymizer) reached over HTTP;
  the app image no longer carries Python/Presidio/venv
- add PRESIDIO_ANALYZER_URL / PRESIDIO_ANONYMIZER_URL
- move VIN out of Python into a TS recognizer (check-digit validated) behind a
  CUSTOM_RECOGNIZERS registry so new custom detectors are one entry; masking is
  handled uniformly by the anonymizer
- drive the guardrails block's PII type picker from the shared pii-entities
  catalog (adds VIN, fixes drift) so block + Data Retention never diverge
- delete validate_pii.py, requirements.txt, setup.sh and the Dockerfile venv step

* fix(guardrails): bound-parallelize mask batch; refresh stale comments

- maskPIIBatch runs per-string sidecar calls with bounded concurrency (8) via
  mapWithConcurrency, so a chunk of many small leaves finishes within the 45s
  request timeout instead of aborting and scrubbing; order + fail-on-error kept
- drop stale comments referencing the deleted Python venv / 30s subprocess timeout

* refactor(guardrails): single Presidio image, native VIN, per-rule redaction language

- collapse the analyzer/anonymizer URLs into one PRESIDIO_URL (combined image
  serves /analyze + /anonymize)
- remove the TS VIN recognizer (vin.ts, recognizers.ts) — VIN is now native +
  multi-language in the image; validate_pii is a thin analyze→anonymize client
- trim KR_RRN/TH_TNIN from the catalog (no Korean/Thai model in the image)
- add per-rule redaction language: PII_LANGUAGES catalog drives the contract enum,
  the Data Retention rule modal, and the guardrails block dropdown; resolver +
  logger thread it through to maskPIIBatch (default en), so non-English entity
  rules (e.g. ES_NIF) actually fire instead of silently no-op'ing under en

* fix(guardrails): correct sidecar port (5001) + README for combined image

The combined Presidio image (docker/pii.Dockerfile) serves /analyze + /anonymize
on a single port 5001 with native VIN + multi-language recognizers. Fix the
PRESIDIO_URL default (was 5002) and rewrite the README, which still described two
stock containers and a TS VIN recognizer.

* fix(guardrails): coerce stored redaction language in the resolver

The persist-path resolver accepted any stored language string, so a stale/invalid
code (e.g. a dropped locale) would reach Presidio and scrub the log even though the
admin UI shows English. Coerce against the supported set via a shared
coercePiiLanguage helper (now reused by the data-retention route too), falling back
to en for unknown values.

* fix(guardrails): rename PRESIDIO_URL env var to PII_URL

Match the infra taskdef, which sets PII_URL on the app container for the
combined Presidio sidecar.
2026-06-23 05:29:01 -04:00
Theodore Li 0191a614b6 feat(pii): build & own combined PII (analyzer + anonymizer) image (#5176)
* feat(presidio): build & own combined analyzer+anonymizer image

Replace the stock mcr.microsoft.com/presidio-* sidecar images with a single
image we build and push to ECR/GHCR. A thin FastAPI service constructs one
AnalyzerEngine + one AnonymizerEngine at startup and serves both on port 3000
(/health, /supportedentities, /analyze, /anonymize) so the app needs one
PRESIDIO_URL. English only; pinned presidio 2.2.362 + en_core_web_lg 3.8.0.

Bakes in the native check-digit VIN recognizer and registers 12 English
recognizers Presidio ships but does not load by default (UK_NINO, AU_*, IN_*,
SG_*), taking the supported English set from 19 to 32.

* feat(presidio): add multi-language support (es/it/pl/fi)

Configure a multi-language spaCy NLP engine (en/es/it/pl/fi lg models) and
explicitly register the national-id recognizers Presidio ships but does not
load by default: ES_NIF/NIE, IT_FISCAL_CODE/DRIVER_LICENSE/VAT_CODE/PASSPORT/
IDENTITY_CARD, PL_PESEL, FI_PERSONAL_IDENTITY_CODE. Verified the NLP-engine +
explicit-registration path detects in-language (Finnish id, score 1.0).

* improvement(presidio): address review feedback

- Register VIN under all served languages, not just en (Bugbot: VIN missed for
  non-English language routing).
- Bump HEALTHCHECK start-period to 180s — five lg models load at import (Bugbot).
- Drop --no-cache-dir so the pip cache mount actually works (Greptile).
- Pydantic request models for /analyze + /anonymize so missing 'text' returns
  422 not 500; default operator 'type' to 'replace' instead of KeyError->500
  (Greptile).

* refactor(pii): rename presidio image artifacts to pii

Rename the image/repo/secret/files from 'presidio' to 'pii' for clarity — the
service does PII detection + anonymization (and backs the guardrails block's
block/mask), not just redaction, and 'pii' matches existing pii-* naming.

docker/presidio.Dockerfile -> docker/pii.Dockerfile
docker/presidio/ -> docker/pii/
ghcr.io/simstudioai/presidio -> .../pii
ECR_PRESIDIO secret -> ECR_PII (infra side already renamed)
No behavior change — paths/identifiers only.

* refactor(pii): move service to apps/pii, make image ECR-only

- Move server.py + requirements.txt from docker/pii/ to apps/pii/ (source belongs
  under apps/, matching app/realtime; Dockerfile stays in docker/). Add a minimal
  @sim/pii package.json so the apps/* bun workspace glob accepts the Python service.
- Repoint docker/pii.Dockerfile COPY paths to apps/pii/; rename the container user
  presidio -> pii.
- Drop GHCR for pii — it's a private ECS sidecar pulled from ECR, never published.
  Removed it from the arm64/manifest (GHCR-only) jobs and guarded the build-amd64
  tag step to skip GHCR when no ghcr_image is set.
2026-06-23 03:41:35 -04:00
Vikhyath Mondreti ccc6954257 improvement(pi): prompting to ensure harness knows push is deterministic (#5180) 2026-06-22 22:19:41 -07:00
Vikhyath Mondreti 633391903d feat(pi): add pi coding agent harness (#5178)
* feat(pi): add pi coding agent harness

* formatting

* update docs

* change version num

* guard to prevent prs on error

* update param visibility

* address security concerns

* fix tests

* reorder:
2026-06-22 21:47:53 -07:00
Theodore Li 707c3cc214 feat(trigger): add trigger-eu-region flag to switch runs to eu-central-1 (#5173)
* feat(trigger): add trigger-eu-region flag to switch runs to eu-central-1

Global on/off feature flag routing every Trigger.dev run from the default
us-east-1 to eu-central-1 via the per-trigger region option, resolved at
each dispatch site through resolveTriggerRegion.

* test(trigger): mock resolveTriggerRegion in delete-async route test

The route now pulls in feature-flags (which imports isAppConfigEnabled from
env-flags); the test's partial env-flags mock made that access throw. Stub the
region module and assert the region option on the dispatch.
2026-06-22 18:17:24 -04:00
Waleed 844733a5ea feat(providers): add Sakana AI provider with Fugu models (#5169)
* feat(providers): add Sakana AI provider with Fugu models

OpenAI-compatible provider at https://api.sakana.ai/v1 (bearer auth).
Registers fugu (fast default) and fugu-ultra (reasoning flagship), both
1M context. BYOK-only, never hosted/auto-billed. Streaming, tool loop,
and response_format supported; attachments mirror deepseek (unsupported
in the current adapter).

* fix(providers): defer Sakana structured output until after tool loop

OpenAI-compatible backends reject a request carrying both response_format
and active tools/tool_choice. Mirror the LiteLLM pattern: withhold the
JSON schema while tools are active and apply it on a final tool-free call
(tool_choice: none) for both streaming and non-streaming paths.

* fix(providers): harden Sakana tool-loop error + final-stream tool_choice

- Rethrow tool-loop failures instead of swallowing them, so a failed run
  surfaces as a ProviderError rather than a partial success (matches LiteLLM).
- Force tool_choice: 'none' on the post-tool streaming pass so the model
  cannot emit fresh tool calls that the text-only stream adapter would drop.

* fix(providers): Sakana streaming usage + filtered-tools stream guard

- Pass stream_options: { include_usage: true } on both streaming calls so
  token/cost data is captured (the shared OpenAI-compatible stream helper
  only fills usage from chunk usage, which the API omits without the flag).
- Include !hasActiveTools in the early-stream guard so requests whose tools
  are all filtered out (e.g. usageControl 'none') still take the fast
  streaming path instead of the tool-loop path. Mirrors LiteLLM.

* fix(providers): answer every Sakana tool_call to keep message history valid

An assistant message lists all tool_calls, so a call for an unconfigured
tool must still get a matching `tool` response or the next request violates
the OpenAI message contract. Emit an error tool-result for unknown tools
instead of dropping them.

* test(session): de-flake SessionProvider normal-load test

flush() only drained microtasks, so the query->render update occasionally
lost the race and ctx.data was still null after the flush budget. Yield one
macrotask tick per flush so React Query's notifyManager and deferred renders
settle deterministically. Verified across repeated local runs.
2026-06-22 13:35:11 -07:00
Waleed e96b150bc9 refactor(frontend-arch): migrate server state to React Query, collapse duplicate workflow-state cache, granular error boundaries (#5168)
* refactor(session): migrate SessionProvider to React Query useSessionQuery

Replace the hand-rolled useState/useEffect/loadSession session loading in
SessionProvider with a useSessionQuery() React Query hook. The SessionContext
shape is unchanged ({ data, isPending, error, refetch }) so no consumer changes.

The 'upgraded' path still forces a fresh DB read via
client.getSession({ query: { disableCookieCache: true } }) (refetch() cannot
pass disableCookieCache) and writes the result via queryClient.setQueryData,
then invalidates ['organizations']/['subscription'] as before.

* refactor(workflows): collapse duplicate workflow-state cache

The registry store fetched the GET /api/workflows/[id] envelope inline via
requestJson while useWorkflowState cached the same endpoint's mapped state
under workflowKeys.state(id) — two requests, two cache shapes, never
reconciled.

Collapse to one request + one cache entry keyed by workflowKeys.state(id):

- Add hooks/queries/utils/fetch-workflow-envelope.ts: a standalone
  fetchWorkflowEnvelope(id, signal) returning the full GetWorkflowResponseData.
  Standalone (not in workflows.ts) to avoid a store -> query-hook import cycle.
- useWorkflowState/useWorkflowStates now query the envelope and derive the
  mapped WorkflowState via select (mapWorkflowState), so consumers see the
  identical mapped shape from the shared entry.
- The store's loadWorkflowState reads via getQueryClient().fetchQuery({
  staleTime: 0 }) instead of raw requestJson — always-fresh (preserving the
  prior always-fetch boot/refresh semantics, incl. the socket
  handle-resource-event refresh path that has no separate state
  invalidation), in-flight deduped, writing into the same cache entry the
  hooks read.

Request-id staleness guard, deployment-cache priming, cross-store projection,
and the active-workflow-changed event are all preserved unchanged.

* fix(workspace): add granular error boundaries to logs, knowledge, and files panels

Scope a crash in one workspace panel to that panel instead of the whole
workspace shell. Each boundary reuses the shared ErrorState component and
mirrors the existing tables/settings error.tsx convention.

* refactor(unsubscribe): migrate page to React Query

Replace the hand-rolled useState+useEffect+requestJson server-state in the
unsubscribe page with React Query hooks. Add useUnsubscribe (validation/load
query, keyed by email+token, auto-runs on mount via enabled) and
useUnsubscribeMutation (unsubscribe action, reconciles cached preferences on
success) in hooks/queries/unsubscribe.ts with a hierarchical key factory.

Export UnsubscribeData/UnsubscribeActionResponse/UnsubscribeType type aliases
from the existing user contract; loading/error/success now derive from the
query and mutation objects with no local server-state mirror.

* test(frontend-arch): cover session race fix, workflow-state cache collapse, unsubscribe, error boundary

Add targeted tests for the four frontend-architecture refactors:
- session-provider: upgrade-path ordering — fresh disableCookieCache read wins
  over a late-resolving stale mount query (proves the cancelQueries guard)
- fetch-workflow-envelope + registry store: single shared state(id) cache entry,
  always-refetch (staleTime 0), request-id staleness guard
- unsubscribe: query enable-gating + mutation cache reconcile
- logs error boundary: renders ErrorState + reset wiring (also first ErrorState coverage)

* fix(session): harden upgrade path + address review feedback

- Reconcile plan surfaces after upgrade even when the fresh disableCookieCache
  read fails: invalidate ['organizations']/['subscription'] regardless of the
  bypass-read outcome (they read server truth, not the cookie cache). The valid
  cookie-cached session is still served, so a transient failure no longer signs
  the user out or leaves the just-upgraded plan looking stale. Org-activate
  fallback stays gated on having a session.
- Use a bare return in the cancelled branch of refreshAfterUpgrade (the caller
  discards the value) for clearer intent; caller coerces with ?? null.
- Make the upgrade tests deterministic: the mount mock honors the abort signal
  like the real fetch-backed client, and assertions read the query cache (the
  state cancelQueries/setQueryData/invalidation actually govern) instead of the
  async-rendered context value.

* refactor(session): break provider<->hook type cycle, fail-fast session query

Address review feedback:
- Move the AppSession type to lib/auth/session-response.ts (the module that
  produces it) so useSessionQuery and SessionProvider both import it from there,
  eliminating the provider <-> query-hook import cycle.
- Add retry: false to useSessionQuery, restoring the prior fail-fast contract
  (the global QueryClient default is retry: 1; an auth failure should surface
  immediately rather than retry a request that won't succeed).
- Return null (not the fetched value) from refreshAfterUpgrade's cancelled
  branch to make the cancellation contract explicit.
2026-06-22 12:44:07 -07:00
Waleed d8da1e2577 fix(state): align server/client state with best practices (query-key bugs, persist hygiene, useState) (#5166)
* fix(queries): close React Query key/fetch-arg drift cache collisions

Several query hooks fetched with an identifier that was absent from their
queryKey, so distinct fetch args shared one cache entry. Thread the missing
args into the key factories and update all callsites/invalidations.

- organization: useOrganization always fetched the ACTIVE org via
  getFullOrganization() while caching under detail(orgId). Pass orgId through
  to the better-auth call (query.organizationId); active-org behavior unchanged.
- logs: logKeys.detail now keys on (workspaceId, logId) to prevent cross-
  workspace collision; updated useLogDetail, useLogByExecutionId, prefetchLogDetail,
  useCancelExecution optimistic path, and external callsites.
- inbox: inboxKeys.taskList now includes cursor/limit (pagination args were sent
  but omitted from the key); keepPreviousData pagination UX preserved.
- a2a: narrow create/update byWorkflows() invalidation to byWorkflow(ws, wf)
  since their responses reliably carry both ids; delete/publish stay broad.

Not bugs (verified, left unchanged):
- kb/connectors update/delete invalidate knowledgeKeys.detail(kbId), which is a
  prefix of connectorKeys.all(kbId) — connector list/detail are invalidated
  transitively by React Query prefix matching.

Harness: add a key-fetch-arg-drift check to check-react-query-patterns.ts that
flags a camelCase identifier the queryFn forwards into the fetch but is absent
from the queryKey (excludes the requestJson contract arg, PascalCase/SCREAMING
constants, and signal/pageParam machinery). Document the rule in sim-queries.md.
tables.useTable annotated rq-lint-allow (tableId globally unique; workspaceId is
only an authz scope).

* fix(stores): whitelist durable fields in persist partialize

chat/terminal/panel persist configs leaked actions and transient state
into localStorage. Replace the chat full-state spread with an explicit
durable whitelist, and add partialize to terminal and panel (which had
none) so isResizing and _hasHydrated are no longer persisted. Panel keeps
activeTab + panelWidth because the layout.tsx blocking script reads them
from panel-state to set data-panel-active-tab before hydration (SSR
tab-flash prevention).

Harden sim-stores doctrine: persist MUST use an explicit partialize
whitelist; never persist transient flags or _hasHydrated.

* fix(state): model component useState as single source of truth

- edit-knowledge-base-modal: reset fields on closed→open via prevOpenRef
  render idiom instead of mirroring props into state through useEffect
  (a prop change while open no longer clobbers in-progress edits)
- use-verification: collapse contradictory isLoading/isVerified/isInvalidOtp
  booleans into a single status enum + errorMessage; consumer derives flags
- contact-form / demo-request-modal: derive busy/success from the mutation
  object; delete duplicated submitSuccess local state
- sim-hooks.md: add state-shape rule (no props-into-state, status enum,
  derive mutation state)

* fix(verify): clear lingering message on complete OTP (restore parity)

* docs(state): convert inline reset comment to TSDoc

* docs(state): tighten harness rules for accuracy (queryFn forwards, partialize whitelist, mutation-flag caveat)

* fix(verify): block auto-verify while a resend is in flight (restore parity)

* fix(logs): key cancel optimistic detail by route workspaceId (not the log row)
2026-06-21 22:44:14 -07:00