Commit Graph
5875 Commits
Author SHA1 Message Date
Waleed 2ba248472e fix(editor): restore caret alignment and the intended type scale across the panel (#6297)
* fix(editor): restore caret alignment and the intended type scale across the panel

Four distinct defects, all surfaced while testing the workflow editor.

**Caret drift in the Start block's Description field.** Its overlay mirror was
built differently from every sibling field: `overflow-hidden` + `truncate` and
no scroll synchronisation, where the working fields use `overflow-x-auto` +
`whitespace-pre` + `syncOverlayScroll`. Once the value passed the visible width
the input scrolled and carried the caret with it while the overlay stayed pinned
at the first character, so the gap grew as you typed. It now has the same
plumbing as its siblings, on its own ref maps so it cannot collide with the value
overlay.

**Overlay mirrors left at 500 over 400 inputs.** #6291 dropped emcn Input and
Textarea to the inherited weight, but the canvas files that mirror them were
reverted from that PR, so 11 overlays kept a hardcoded `font-medium`. A mirror
that renders heavier than the input beneath it misaligns by the weight delta on
every character. All 11 realigned.

**Input text tracking differently from its label.** The UA stylesheet resets form
controls to `letter-spacing: normal`, so inside `.workspace-root` (0.02em) an
input diverged from surrounding text — and from its own overlay — by 0.28px per
character. emcn Input and Textarea now carry `[letter-spacing:inherit]`, which
fixes every mirrored input at the source rather than per call site.

**Weights and type sizes that changed meaning under #6241.** That PR deleted the
tailwind remap of `font-medium` (440 light / 480 dark) without migrating the
~505 call sites written against it, so untouched code jumped to a stock 500. The
panel's editor, toolbar, chat and connections surfaces are swept back to the
inherited weight. The Chat header was also visibly taller than Toolbar and Editor
purely because it used `text-[14px]` — font-size with no paired line-height —
against otherwise byte-identical containers; it and the panel's two other
arbitrary sizes now use named tokens.

Also fixes five JSX conditionals in the workflow MCP settings page that had lost
their braces, so `canManage && ()` rendered as literal text under the server
detail tab.

Verified: typecheck 0, biome clean, 18644/18645 vitest passing (the one failure
is a missing `rg` binary and predates this branch).

* fix(emcn): give chip text fields the same tracking as their mirrors

An audit of the previous commit found the letter-spacing fix was incomplete: it
landed on `Input`/`Textarea` but not on the chip family, so `ChipInput` and
`ChipTextarea` kept the UA `letter-spacing: normal` while any overlay mirroring
them inherited the ambient tracking.

The MCP server form modal is a live instance — its shared `FormattedInput` layers
a transparent `ChipInput` under a visible div, across the server URL and both
header fields, whose values are long by nature. The caret separated from the text
by roughly 0.28px per character.

Fixed on `chipFieldTextClass` rather than the call site, so every chip field
matches its mirror the way `Input`/`Textarea` already do.

* fix(editor): restore the strong-text variant a class sweep welded together

The font-weight sweep in 94c9f1f68 used a blanket sed, which turned

  [&_strong]:font-medium [&_strong]:text-[var(--text-primary)]

into [&_strong]:[&_strong]:text-[var(--text-primary)] — a chained variant
matching a <strong> inside a <strong>, so ordinary strong text in trigger setup
instructions lost its color.

The removed weight was also load-bearing rather than decorative. Preflight sets
b/strong to font-weight: bolder, so against a 400 body a bare <strong> lands near
700; the class was holding it down to 500. Deleting it made that text heavier,
the inverse of the sweep's intent — the same UA-default trap as <th>. Both
variants are restored.

Swept for the same damage: no chained [&…]:[&…] variants remain across apps/sim
or packages, and this was the only variant-scoped weight the sed touched.

Found independently by Greptile and Cursor Bugbot.
2026-08-05 15:04:30 -07:00
Waleed 0bef1c3099 fix(files): stop a cached collab snapshot resurrecting blank lines (#6293)
* fix(files): stop a cached collab snapshot resurrecting blank lines

A collaborative markdown file's cold-start seed can come from a cached Yjs
snapshot (workspace_file_collab_state.doc_state) rather than a fresh markdown
re-parse. The snapshot is a raw CRDT binary, so it preserves top-level empty
paragraphs that parseMarkdownToDoc/stripEmptyParagraphs strips from every parse
target. The static placeholder always re-parses (clean); a warm seed replays the
snapshot verbatim, so a stray blank line appears once the doc settles — and only
intermittently, since a stale/cold cache falls through to the clean re-parse.

Enforce the same no-top-level-empty-paragraph invariant on the Yjs side:
- normalize.ts: stripEmptyTopLevelParagraphs(doc) shared helper.
- seed.ts: repair the cached snapshot on read (self-heals legacy snapshots,
  preserving CRDT client ids; no data migration).
- persist.ts: normalize before caching so new snapshots are clean by construction.

* refactor(collab-doc): make COLLAB_DOC_FIELD a single canonical constant

converter.ts had a duplicate 'default' fragment-name constant; import the now-exported one from normalize.ts so the value TipTap's Collaboration binding depends on lives in exactly one place. Fold the back-to-front loop note into the helper's TSDoc.
2026-08-05 13:58:18 -07:00
Waleed dcaa118752 improvement(docs): restructure sidebar, align chrome, rename Mothership to Chat (#6296)
Sidebar: 11 separator groups become 5, with each module a collapsible folder
that auto-opens on the active page. 61 always-visible rows drop to 16. Groups
mirror the app's own nav (Chats/Workspace/Workflows) rather than inventing a
taxonomy; Enterprise and Self-Hosting are hoisted out of Platform.

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

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

Also removes the page-type badge, drops the "Next" heading from the ToC, and
lets FAQ rows open independently so expanding one no longer shifts the page.
2026-08-05 13:49:17 -07:00
Theodore Li 41572a0a6a feat(tables): support plain predicates in v2 queries (#6292)
* feat(tables): support plain predicates in v2 queries

* fix(tables): regenerate tool metadata

* fix(tables): bound predicate validation
2026-08-05 15:10:26 -04:00
Vikhyath MondretiandSim Pi Agent 884a0be574 feat(library): Sim vs Dify: Open-Source AI Workspace vs LLM App / RAG Platform (#6286)
* feat(library): Sim vs Dify: Open-Source AI Workspace vs LLM App / RAG Platform

* feat(library): Sim vs Dify: Open-Source AI Workspace vs LLM App / RAG Platform

---------

Co-authored-by: Sim Pi Agent <pi@sim.ai>
2026-08-05 11:44:47 -07:00
Justin Blumencranz 9ff7eaeaa1 feat(copilot): attribute tool metrics to invoking agents (#6266)
* feat(copilot): attribute Sim tool calls to agents

* fix(review): bound agent metric labels

* feat(copilot): attribute tool latency to agents

* feat(copilot): attribute tool spans to agents

* refactor(copilot): simplify tool attribute naming

* fix(copilot): bound tool span agent labels
2026-08-05 11:36:03 -07:00
Waleed 2d90c729c5 fix(wand): stop markdown code fences landing in generated code (#6289)
* fix(wand): stop markdown code fences landing in generated code

Strip fences from wand output for raw-value generation types, reset the
conversation history when a Function block switches language, and give the
Python prompt the worked example the JavaScript one already had.

* fix(wand): preserve nested fences and retire stale history on reset

Slice only the outermost fence delimiters so a fenced body containing
line-leading backticks keeps every interior line, and skip the history
append when a language reset retired the request mid-flight.

* refactor(wand): drop unreachable guard in fence stripper

The trimStart check made the -1 branch dead and stated "opens with a
fence" twice. Derive it once from the first fence line's position.

* chore(wand): remove unused onGenerationComplete callback

No call site ever passed it, so the branch never ran. The props interface
makes the removal compile-time verified.

* fix(wand): never treat an interior fence line as the closer

A truncated response whose body embeds line-leading backticks lost every
line after the first embedded delimiter. Only the opening line and a
final fence line are removed now. Sync the history epoch in a layout
effect so a request settling before the passive flush cannot append to
already-reset history.

* test(wand): record the trailing-fence ambiguity as a decision

A bare fence on the last line closes the wrapper in every well-formed
response and is content only when generation stopped exactly on an
embedded delimiter. Nothing separates the two, so assert the chosen
behavior instead of leaving it implicit.
2026-08-05 11:29:50 -07:00
Waleed 0583e2085f improvement(emcn): let every primitive inherit the document font weight (#6291)
* improvement(emcn): let every primitive inherit the document font weight

#6241 flattened the type scale by deleting the tailwind `fontWeight` override
that remapped `font-medium` to 440/480. Nothing was restyled, but the utility's
meaning changed: every one of ~505 call sites written when `font-medium` sat
~10 units above body snapped to a stock 500 against a body that also dropped
430 -> 400. The tab strip and table header Emir reported are two symptoms.

The same PR added the "Font Weight" section to sim-styling.md declaring the end
state (400 default, weight class only to step up) without migrating the codebase
to it, so the rule and its violations shipped together and no gate could flag it.

Removes the hardcoded weight from the ~20 pre-chip emcn primitives so they
inherit 400, matching the chip family that was already correct. Deletes the
three `font-normal` overrides that existed only to undo those defaults
(`TabStrip`, `ChipCombobox`, `ChipModalField`) — their TSDoc said as much.

`<th>` needed care in the other direction: Preflight resets h1-h6 but not `th`,
so a header keeps the UA bold 700 and `font-medium` was holding it *down*.
Deleting it made headers heavier. Rather than neutralize per call site — the
codebase had already accumulated 12 such workarounds — globals.css completes
Preflight with `th { font-weight: inherit }`, and the 8 now-dead `font-normal`
workarounds come out. The rule keeps no element-level exceptions.

Two width-measurement mirrors had to move with the text they measure:
table-grid's auto-fit canvas and tag-input's hidden sizing span. Left stale,
both would mis-measure.

Also: drops an inline `font-weight:500` from the header drag ghost, normalizes
`text-[13px]`/`text-[11px]` to `text-small`/`text-xs`, aligns the landing table
previews that clone the product header, documents AvatarFallback's deliberate
step-up, and corrects the stale AGENTS.md line claiming Button owns a weight.

Verified: typecheck 0, lint clean, full vitest run identical to origin/staging
across three runs (158 failed suites / 1 failed test / 16350 passing — all
pre-existing: a PostCSS env error and a missing `rg` binary).

* fix(tables): align the workflow-group drag ghost with the label it previews

Cursor Bugbot caught the sibling of the ghost fixed in column-header-menu:
this one kept an inline `font-weight:500` after the band label dropped to
the inherited 400, so the drag preview no longer matched the text it
represents — the same mirror drift this PR fixes for the width-measurement
spans.

It was drifted on size too: hardcoded `font-size:13px` against a label that
is `text-xs` (11px), so it never matched. Both now come from the token the
label uses, which also clears the inline fontWeight the styling rule bans.

* fix(toolbar): stop the block drag preview hardcoding a weight its label does not use

Third instance of the same mirror drift, found by sweeping rather than
waiting for it to be reported. The toolbar item label renders at the
inherited 400 (its container is `chipVariants`, which is weight-free), but
the drag preview hardcoded `font-weight: 500` — so the preview never matched
the item it previews, before or after this PR.

Drops the weight only. The 16px is left alone: a drag preview reading larger
than its source is a deliberate affordance, not a mismatch.

Not fixed here, deliberately: the `font-weight: 500` in code-editor.tsx's
highlight layer. That HTML is rendered in registration with a transparent
textarea, so weight affects glyph advance widths and the caret alignment —
it needs live in-editor verification, unlike a detached drag ghost. Its two
placeholder branches already disagree on weight, so it wants its own change.

* revert(canvas): drop the workflow-editor panel changes from this PR

The canvas panel is under active modification elsewhere, so this PR stays
out of it. Reverts the toolbar search input, the sub-block table cell and
its overlay mirror, the messages-input textarea and its mirror, and the
block drag preview — all back to staging verbatim.

Cursor Bugbot was right about that last one and I was wrong: the preview
mirrors the drag DESTINATION (its TSDoc says "looks like a workflow block",
and 250px/16px are block-card dimensions), not the toolbar chip I had
compared it against. workflow-block-view renders the title `font-medium
text-md`, so its 500 was correct. Moot now that the file is reverted, but
worth recording so the next sweep does not repeat the mistake.

The globals `th` rule still covers the sub-block table header without a
call-site class; the explicit font-medium there simply wins over it, exactly
as staging renders today.
2026-08-05 11:21:46 -07:00
Vikhyath MondretiandClaude Opus 5 402f862210 fix(forking): make the trigger URL preview reflect the user's actual picks (#6290)
Two Cursor findings on #6272, both in the preview layer - the sync's write path
was correct in each case, but the UI stated an outcome that did not match it.

- The heads-up and overwrite confirm read `triggerUrlChanges` straight off the
  diff, which the server computes with its DEFAULT resolution before the user
  chooses anything. Selecting "Generate new URL" for a trigger that would have
  adopted a URL therefore killed that URL with no warning, in the one modal whose
  job is to state irreversible consequences (it also over-warned in the reverse
  case). The diff now returns the RAW retiring set and the client subtracts the
  live choices, so the rows, the heads-up and the confirm cannot disagree.
- The picker let two triggers select the same retiring URL and showed both as
  keeping it. Two blocks cannot serve one path (`path_deployment_unique`) and the
  resolver awards it to the first slot, so the loser silently got a new URL. A
  path another row claimed is now disabled and named, and each row displays its
  RESOLVED outcome rather than its raw pick.

The choice resolution is a pure module mirroring `resolveForkTriggerPaths`
(offered-paths guard, first-claim-wins), so the preview and the server agree by
construction rather than by two hand-kept implementations.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 10:44:44 -07:00
Waleed 53c3beace1 fix(providers): name the header phase on streaming OpenAI requests (#6288) 2026-08-05 10:22:55 -07:00
Vikhyath MondretiandClaude Opus 5 6cc25f6d9b improvement(forking): make webhook url mapping clear (#6272)
* improvement(forking): make webhook url mapping clear

* fix(forking): honour drops before the unmapped gate, match provider on URL adoption

Review round 1 on #6272.

- Drop was inert for required references: `postCopyUnmappedRequired` gates before
  the cleared-ref gate that honours acknowledgments, so a source-deleted reference
  on a required field still failed with "map all required ... first". Verified drops
  are now resolved once (`verifyForkDropAcknowledgments`) and subtracted from both
  gates. Verification is not optional: an unmapped reference of a non-blocking kind
  (credential, env-var) never re-blocks downstream, so subtracting raw
  acknowledgments would let a crafted payload skip the required gate.
- URL adoption now requires provider equality. A count-only 1:1 pairing could hand a
  GitHub URL to an arriving Slack trigger, keeping the endpoint alive while every
  request failed signature verification - and reporting the URL as preserved.
- `resolveTriggerId` moved from `lib/webhooks/deploy.ts` to `@/triggers/webhook-url`
  so the deploy path and the fork's provider check share one resolution.
- Trigger URL warnings render the full public URL in the heads-up section and name
  the URL in the overwrite confirm, where identical workflow names were ambiguous.
- The Drop control renders once per resource and states how many fields it covers;
  the remapper clears by reference, so a per-row control implied a choice the write
  path cannot honour.
- Export clears `workflow-selector`: nothing on the import path remaps workflow ids
  (`import-export.ts` re-creates each workflow under a fresh id), so a preserved
  reference dangled - bundle or not.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 01:10:33 -07:00
Waleed 0fce5358b6 fix(files): stop markdown reflow on open by matching placeholder wrapping to the editor (#6285) 2026-08-04 23:24:56 -07:00
Waleed 623003e43b fix(providers): name the failing phase of a stalled OpenAI call, and reject a failed generation (#6283)
* fix(providers): name the failing phase of a stalled OpenAI call, and reject a failed generation

An agent block hung ~4.5 minutes with an empty trace and surfaced only the
runtime's own `TimeoutError: The operation timed out.` The cause was a runaway
generation: the model repeated one tool call until it consumed the whole
128,000-token output budget, which takes minutes, and `/v1/responses` withholds
its 200 until generation finishes — so the client waited, bounded only by an
undocumented runtime socket deadline, and gave up before the response existed.

Nothing in the trace could distinguish that from a request the provider never
answered, or from one whose body never arrived.

- Name the phase a transport failure died in — `awaiting-response-headers` vs
  `reading-response-body` — with status, ttfb, content-length and
  `x-request-id`. undici draws the same line as two error types
  (UND_ERR_HEADERS_TIMEOUT / UND_ERR_BODY_TIMEOUT); the OpenAI SDK captures
  `x-request-id` for the same reason. It rides the error message because that
  reaches the trace span, which survives when a task stops shipping logs.
- Carry the cause through `ProviderError` so a transport timeout still
  classifies after wrapping overwrites `name`.
- Reject a 200 that reports a failed or unusable generation instead of
  returning empty content with billed tokens, and stop truncated tool calls
  from executing. Matches `streamResponsesTurn`, which already did this, and
  `@ai-sdk/openai`, which throws on the same condition.
- Bound non-JSON error bodies so a gateway error page cannot become the
  user-facing block error.

Deliberately not included: a response-body deadline (the observed failure is in
the headers phase, and the body transfers in ~1ms) and status-based retries
(worth doing, unrelated to this, and separable).

* test(providers): pin that a structured provider error survives the error-body bound

* chore(providers): trim comments to the non-obvious why

* fix(providers): let a deadline while reading an error body propagate

* fix(providers): name the body phase when an error body read fails
2026-08-04 23:17:37 -07:00
mzxchandra 5718def4fd feat(logs): open the workflow from the log details panel (#6275)
* refactor(logs): centralize workflow-id resolution and editor path

The logs list, its context menu, and the details panel each resolved a
log's workflow id with their own copy of `workflow?.id || workflowId`,
and the list disagreed with the details panel on what counts as a
deleted workflow. Extract `resolveLogWorkflowId` and
`workflowEditorPath` so the three surfaces cannot drift.

`resolveLogWorkflowId` also returns null for Sim agent jobs, which have
no workflow of their own. Only the context menu's "Open Workflow" item
adopts that stricter predicate; cancel and retry keep using the previous
`hasWorkflow` check so their gating is unchanged.

* feat(logs): open the workflow from the log details panel

The workflow name in a log's details panel was static text, so the only
way to reach the workflow was the row's right-click context menu. Make
the label a link to the workflow editor, opening in a new tab so the
log list keeps its filters, scroll position, and open panel.

On hover or keyboard focus the leading workflow icon morphs into
SquareArrowUpRight, reusing the grid-stacked cross-fade already used by
the resource header breadcrumb. Sim agent jobs and deleted workflows
have no reachable workflow and stay static text.

Adds a `group-hover-hover` variant so the morph is gated on a real
hover-capable pointer, matching the existing `hover-hover` variant and
keeping touch devices out of a half-applied hover state.

* refactor(logs): use the existing group-hover variant for the workflow link

Drops the group-hover-hover Tailwind variant this branch added and moves
the details-panel workflow link to plain group-hover:, matching the
variant already used throughout the app. tailwind.config.ts is untouched
by the branch again; all colors, radii, and the focus ring come from
existing design tokens.
2026-08-04 22:27:50 -07:00
Waleed 020ec68cfe fix(sidebar): keep the collapsed profile chip inside the rail (#6282) 2026-08-04 21:55:13 -07:00
Waleed a3a887ad5a fix(docs): point the service-account guides at the real connect flow (#6277)
* fix(docs): point the service-account guides at the real connect flow

Fourteen of the twenty service-account guides sent admins to a workspace
Settings → Integrations tab that does not exist — Integrations is a top-level
workspace route, and there is no integrations section in the settings navigation
at all. The same fourteen then told them to search the catalog for
"<Service> Service Account", a name no catalog entry has: the list is derived
from blocks, so the entries are "Airtable", "Monday", "Wealthbox", and search
matches only name and description.

Both steps now match the six guides that were already correct (Box, Zoho Desk,
Zoom, Salesforce, Pipedrive, Atlassian), so all twenty describe one flow. The
per-guide connect labels were already right and are untouched.

Google needed a third fix: its final step said Click **Save**, but the modal's
primary button is `Add {connectNoun}`, which for Google falls back to
"Add service account". It also has no catalog entry of its own, so the search
now points at Google Drive with a note that any Google integration works.

Also adds `invalidCredentialsHelp` for Wealthbox. Its validator rejects a token
that works only over Wealthbox's documented ACCESS_TOKEN header, because Sim's
tools authenticate with Bearer — a deliberate, documented limitation whose
reason reached the server log and never the user, who saw only "Double-check it".

* fix(docs): make the Wealthbox rejection copy true for every failure path

`invalidCredentialsHelp` replaces the generic message for every
`invalid_credentials` rejection, and the Wealthbox validator raises that code on
three paths: a 402 expired trial, a 401/403 where both header styles fail, and a
401/403 where Bearer fails but the ACCESS_TOKEN probe succeeds. The copy
described only the third, so two of the three told the user their token was
valid and pointed at remediation that could not help.

Now leads with what is checkable in all three cases and makes the Bearer note
conditional on the one signal that distinguishes it — the token working
elsewhere.
2026-08-04 20:07:03 -07:00
Waleed 2c120c35fb improvement(emcn): let a modal refuse every dismissal while an action runs (#6276)
* improvement(emcn): let a modal refuse every dismissal while an action runs

ChipConfirmModal's docs promised "a single dismiss path shared by the header X /
dismiss button / Escape … and disabling dismiss while the confirm is in flight".
Only the dismiss button was ever guarded — Escape, outside-click and the header X
all still closed a confirmation mid-delete. Two knowledge-base connector modals
had the same shape: they guarded onOpenChange against a pending save, then handed
the header X a direct onOpenChange(false) that skipped the guard.

A modal now states the interlock once, as `dismissDisabled` on ChipModal or
ModalContent, and the primitive holds all four exits shut. ModalContent owns the
Radix paths because `{...props}` is spread after its own handlers, so a
consumer-passed onEscapeKeyDown/onInteractOutside would silently drop the
floating-layer guard; it publishes the flag through a context that
ChipModalHeader, ChipModalFooter and ModalHeader read. The two narrow props
compose with `||`, so an explicit `true` still disables a single button and an
explicit `false` cannot punch a hole in the root's guarantee.

Also turns on `turbo run type-check` for every workspace. packages/emcn,
packages/utils, apps/desktop and apps/docs had no type check in CI at all —
only @sim/realtime did — and apps/sim's source was covered solely as a side
effect of `next build`. All 23 workspaces pass today, so it lands green.

* fix(emcn): compose consumer dismiss handlers instead of replacing the guard

ModalContent's own onEscapeKeyDown/onInteractOutside sit before the `{...props}`
spread, so a consumer passing either replaced them — dropping both the
dismissDisabled interlock and the floating-layer guard that keeps a popper
dismissal from closing the modal and freezing the page. The TSDoc argued the
guard had to live here for exactly that reason, then left the same spread able
to defeat it.

Both handlers are now destructured out of props and invoked after the guard, so
the guard always runs and a consumer can still observe or extend the event. No
consumer passes either today, so this was latent rather than live.

* revert(ci): drop the type-check inputs allowlist

The allowlist traded correctness for a modest cache win, in a gate whose only
job is catching type errors. `resolveJsonModule` and `allowJs` are both on, so
.json and .js files participate in type checking and were absent from the list —
`lib/integrations/availability.ts` imports the generated `integrations.json`,
which means regenerating that file would not have invalidated the cache and CI
would have replayed a stale pass over a changed type.

Back to Turbo's default (every non-gitignored file in the package): conservative,
but a type-check gate that can serve a stale success is worse than a slow one.
2026-08-04 19:40:07 -07:00
Siddharth Ganesan 6c10ac25a7 chore(scheduled-tasks): remove scheduled tasks from the vfs (#6274)
* chore(copilot): remove scheduled-task remnants

* chore(copilot): remove task VFS projection
2026-08-04 18:44:56 -07:00
Waleed 0ab44c5b44 improvement(zoho-desk): pick the data center from a dropdown and trim service-account help text (#6271)
* improvement(zoho-desk): pick the data center from a dropdown and trim service-account help text

The Zoho Desk Self Client modal rendered a paragraph of setup steps as the hint
under Client secret, duplicating both the setup guide and two of its own field
hints. Cut it to the one caveat that isn't derivable from the form, and moved it
to the org-identifier field the caveats actually qualify. Data center is now a
dropdown sourced from ZOHO_DESK_DATA_CENTERS.

Same editorial pass across the other service accounts: Zoom, Salesforce,
Shopify, Webflow, Trello and Cal.com dropped setup steps in favor of caveats.

Also adds the documented Zoho Desk params that were missing (list_tickets
assignee/channel/receivedInDays, list_comments and list_threads sortBy,
get_contact and get_thread include), each gated per operation so a stale
subBlock value can't leak into an endpoint that reads the same param name.

* fix(zoho-desk): let an unsupported receivedInDays reach the tool's validation

The block mapper filtered on shape before forwarding, so a fractional or
non-numeric value was dropped and List Tickets then ran with no window at all —
returning the whole queue as though the requested filter had applied. The tool
owns that validation, so the mapper now passes the value straight through.

Adds a block-to-tool seam test: neither side's own tests could catch a value
lost between them.

* fix(zoho-desk): overwrite operation-scoped params instead of omitting them

The block mapper scoped params by destructuring them out of the spread, on the
assumption that a key left out of the return value never reaches the tool. It
does: both call sites merge the mapper's output on top of the original inputs
(`{ ...inputs, ...transformedParams }`), so an omitted key is restored.

The serializer is what actually held this together, and it has a gap — an
advanced subBlock with a retained value is emitted for every operation while the
block's advanced toggle is off, because that branch returns on isNonEmptyValue
without evaluating the subBlock's condition. So a Sort By set on List Tickets
reached List Comments, and a ticket Include reached Get Contact, each rejected
by Zoho. Out-of-range from/limit reached the wire for the same reason.

Every scoped param is now assigned unconditionally, undefined included, so the
merge cannot resurrect a stale value.

Also fixes a crash this branch introduced: clearing the Departments multi-select
stores [], which reached the comma-list normalizer and threw on .split. The
helper now takes arrays, which is what that subBlock actually stores.

The block-to-tool tests now model the real merge rather than the mapper's return
value alone — the previous version passed while production threw on the same
input. Corrects two comments that misstated where Zoho documents customFields
and errorMessage, and splits the shared include subBlock, since Get Ticket
accepts contract and skills and List Tickets does not.

* fix(zoho-desk): do not scope params on the agent-tool path

The previous commit made the mapper assign every operation-scoped param
unconditionally, so the merge could not resurrect a stale value. That is right
on the canvas path and wrong on the agent-tool path, where `operation` is a
sibling of the tool call rather than a member of params: the mapper saw
`operation === undefined`, every gate resolved to undefined, and the merge then
overwrote the model's own arguments with it. A Zoho Desk tool called by an agent
lost every parameter the model supplied.

That path needs no scoping — the tool is already chosen, and the model addresses
tool params by their real names — so it now returns early. Custom fields are
still coerced there, since parsing JSON is a type fix rather than an operation
gate, and that parsing is now shared by both paths.

* fix(zoho-desk): keep the legacy include working on Get Ticket

Splitting the shared `include` subBlock into `include` and `ticketInclude` left
workflows saved before the split reading an empty field, so their Get Ticket
calls silently stopped embedding what they asked for.

Get Ticket now reads `ticketInclude ?? include`. The fallback only goes that
direction: Get Ticket accepts every value List Tickets does plus `contract` and
`skills`, so a legacy value is always valid there, while List Tickets still
reads only `include` and can never receive the two extra tokens it does not
document.
2026-08-04 18:28:50 -07:00
Theodore Li fbd02bc176 feat(admin): add password user creation (#6267)
* feat(admin): add password user creation

* fix(admin): restrict created users to normal role

* improvement(admin): refine add user action

* fix(admin): prevent duplicate user creation

* fix(admin): reflect immediate submission state
2026-08-04 19:48:10 -04:00
Waleed bf5abc9a40 blog(agent-as-yjs-peer): "The Agent Is Just Another Peer" post + live demo (#6268)
* blog(agent-as-yjs-peer): draft post on streaming an agent into a collaborative doc as a Yjs peer

* blog(agent-as-yjs-peer): add cover image

* blog(agent-as-yjs-peer): add live two-peer editing demo component

* improvement(blog): match code blocks and inline code to the in-app editor tokens

* blog(agent-as-yjs-peer): finalize copy, streaming demo, and feature the post

* fix(blog): guard agent demo against missing/never-firing IntersectionObserver

* fix(blog): stream demo bullets and code chip per-character to match the typing cadence

* chore(blog): trim demo comments and apply cleanup (adopt cn, hoist languageMap, chip color)

* blog(agent-as-yjs-peer): swap cover image

* fix(blog): guard agent demo rAF loop against rescheduling after effect cleanup
2026-08-04 16:20:07 -07:00
Waleed b98dd8ba5e fix(emcn): normalize MoreHorizontal and TerminalWindow geometry (#6265)
MoreVertical was migrated to the house geometry; MoreHorizontal was left
on its original 0 0 12 3 fill construction. Because that box is far wider
than it is tall, a square size class scales it by 14/12 and the dots
stretch edge to edge: 3.17px dots across a 14px span, where MoreVertical
draws 1.78px dots across 8.78px. The two are the same glyph rotated 90deg
and sit in the same overflow-menu role, so the mismatch is visible on any
row carrying one.

Redraw MoreHorizontal as the exact transpose of MoreVertical about
(10.25, 9.75). Both now measure 8.80px.

TerminalWindow had the same problem in the resource registry tab strip,
where ten icons render side by side at size-[14px]: it drew 14.00px
filled against neighbours at 10.30-12.05px on a 0.90px stroke. Redraw it
on the 24-box keeping its character - a window, a title bar, three chrome
dots. Now 10.85px.

Three call sites needed updating alongside the viewBox change:

- panel.tsx passed no size and sat in a Button, which does not force-size
  its svg children, so the icon would have jumped to its new 24x24
  intrinsic size inside a 30px button.
- The two sidebar size-[9px] values were compensations for the oversized
  glyph (9px against a 12-wide box happens to yield a ~9px span). Against
  the 24-box they would render 5.64px, so they move to size-[14px], which
  lands at 8.78px - exactly MoreVertical.
2026-08-04 14:37:03 -07:00
Waleed f0e57cb73f fix(emcn/header): icon consistency — breadcrumb menus, Connections, FolderCode (#6263)
* fix(header): give every breadcrumb menu item an icon

The resource crumb's menu (table, knowledge base, document, file) rendered
Rename/Tags/Share/Download/Delete with icons, but the folder crumb's menu
rendered bare labels. The two open from adjacent segments of the same
breadcrumb, so the inconsistency was visible side by side.

Add the matching icons to the three folder-crumb menus.

* fix(emcn): redraw Connections on the house geometry

Connections was a filled glyph in a tight 0 0 13 13 box with a 0.2
hairline stroke. At size-[14px] it rendered a 21.5px glyph at 0.22px
stroke where its neighbours render ~10.5px at 0.90px, so it read as a
different weight class in the resource registry it shares with Library,
Globe and Database.

Redraw as a stroke outline on the 24-box at 1.55, preserving the
topology: a 2x2 grid with a circle top-right, linked along the top,
down the right column, and along the bottom. Now 10.9px at 0.90px.

* fix(emcn): match FolderCode geometry to the folder family

FolderCode carried the same fill-based construction Folder did: a tight
0 0 15 13 box with a 0.3 hairline stroke. At size-[14px] it rendered a
19.9px glyph at 0.28px stroke where a standard icon renders ~10.5px at
0.90px — roughly twice the size at a third the weight.

It renders in Chat's TOOL_ICONS map for glob/mv/mkdir, directly beside
Search, File and Database, so the mismatch was visible in a single list.

Reuse Folder's body path verbatim and inset code brackets centred on the
body, so the whole folder family shares one silhouette and one weight.
2026-08-04 13:41:21 -07:00
Waleed 8ca3b27f43 fix(emcn): match Folder icon geometry to FolderOpen (#6262)
Folder and FolderOpen toggle in place in the sidebar folder rows, but
carried incompatible geometry: Folder was a fill-based outline in a
tight 14.5x13 box at stroke 0.3, while FolderOpen is the house-standard
stroke outline in -1 -2 24 24 at stroke 1.55.

A square size-[16px] therefore scaled the closed folder to ~16x14.3px
where the open one renders ~11.3x10px, so expanding a folder visibly
shrank its icon.

Redraw Folder as FolderOpen's own body outline closed along the
bottom-right, so the pair shares a silhouette, a box and a stroke
weight by construction.
2026-08-04 13:13:56 -07:00
Theodore Li 35fd4ef42f improvement(self-host): simplify capability setup configuration (#6230)
* feat(self-host): add capability-aware setup

* fix(self-host): preserve capability compatibility

* fix(copilot): honor preview availability server-side

* improvement(self-host): centralize capability resolution

* fix(self-host): preserve integration availability paths

* fix(testing): align capability-aware config mocks

* improvement(self-host): simplify capability setup configuration

* fix(setup): preserve unowned storage overrides

* fix(self-host): reconcile storage and allowlists

* fix(integrations): preserve connect deep links
2026-08-04 15:47:36 -04:00
Waleed 39c3fe60d9 improvement(docs): remove Ask AI, add the missing platform surfaces, align the sidebar (#6259)
* improvement(docs): remove Ask AI, add missing surfaces, align sidebar to the app

- Removes the Ask AI widget, its /api/chat route, and the four deps exclusive
  to it (@ai-sdk/openai, @ai-sdk/react, ai, streamdown). lib/embeddings and
  docsEmbeddings stay — /api/search uses them.
- Adds --surface-7 and --surface-hover, the last two platform surfaces docs
  lacked.
- Aligns the sidebar with the app's canonical nav chrome: px-2.5 -> px-2,
  text-small -> text-sm, hover --surface-3 -> --surface-active, and an active
  hover of --surface-6, matching the Chip the app's sidebar items are built on.

* fix(docs): make the sidebar hover CSS agree with the utilities

Review caught that the sidebar hover alignment in this PR had no visual
effect. `global.css` carries !important rules for both the link and button
sidebar items — they exist to beat fumadocs' own styles — and they were still
forcing the pre-alignment values: --surface-3 on an inactive hover, and
--surface-active on an active hover.

So the Tailwind utilities were dead on arrival. The global rules now carry the
app's values instead (--surface-active inactive, --surface-6 active), matching
the utilities rather than fighting them, with a comment noting the two must
move together.
2026-08-04 12:45:10 -07:00
Theodore Li a19ae165e0 fix(landing): restore homepage view tracking (#6260) 2026-08-04 12:45:01 -07:00
Waleed c14818b7bb improvement(emcn): normalize the chevron geometry and consolidate consumers (#6258)
* improvement(emcn): normalize the chevron geometry and consolidate consumers

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

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

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

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

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

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

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

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

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

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

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

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

ChipChevronDown's TSDoc still described centring a 10x6 glyph, and the
enterprise sidebar's chip-parity comment still cited a 6x10 chevron. Both
now read 14px, matching what the components actually render.
2026-08-04 12:38:10 -07:00
Waleed fb7fd5b621 chore(copilot): remove the training controls feature (#6257) 2026-08-04 12:10:03 -07:00
Waleed 63faeb555f chore(deps): bump undici to 7.29.0 to close 5 security advisories (#6255) 2026-08-04 11:34:43 -07:00
Vikhyath MondretiandSim Pi Agent f079c72923 feat(library): Best AI Agents for Executive Assistant Tasks (#6254)
Co-authored-by: Sim Pi Agent <pi@sim.ai>
2026-08-04 11:28:36 -07:00
WaleedandBohdan Vilishchuk 47f5fee8cb fix(setup): launch the docker app the CLI is actually pointed at (#6253)
* fix(setup): detect OrbStack vs Docker Desktop before relaunching the daemon

ensureDocker() always ran `open -a Docker` to relaunch a stopped daemon on
macOS, which silently no-ops for OrbStack users (no Docker.app bundle
exists), leading to a misleading "GUI license acceptance" timeout error.
Now it checks the docker CLI's active context first (accurate regardless
of install location) and falls back to checking for OrbStack.app, so the
wizard launches and messages the app that's actually installed.

* fix(setup): don't let an installed OrbStack override an explicit Docker Desktop context

macDockerApp() fell through to the OrbStack.app existence check whenever
docker context show returned anything other than "orbstack" — including a
known, explicit context like "desktop-linux". With both apps installed but
Docker Desktop active and stopped, this launched OrbStack while daemonUp()
kept polling Docker Desktop's socket, timing out with OrbStack-flavored
guidance for a Docker Desktop problem.

The path fallback now only runs when the context command gives no answer
at all (null); any resolved context is trusted outright.

Flagged identically by Greptile and Cursor Bugbot on PR #6250.

* fix(setup): fall back to the installed app when the context isn't OrbStack

Context detection only fell back to the app bundle when `docker context
show` failed outright, so an OrbStack-only Mac sitting on the `default`
context still resolved to Docker Desktop — the same 90s hang this fix
exists to remove. Treat an explicit OrbStack selection as the only
positive context signal and otherwise pick whichever app is installed.

Read `DOCKER_HOST` first: it overrides the active context, so the
context name is not authoritative while it is set.

* fix(setup): require OrbStack to be installed before selecting it

A context or DOCKER_HOST left behind by an OrbStack uninstall selected an
app that can never launch, turning a working Docker Desktop start into a
guaranteed 90s timeout. Gate the OrbStack signal on the bundle being
present and fall through to whichever app is.

Look in ~/Applications as well as /Applications while here — Homebrew
casks honour --appdir, so a user-local install is not unusual and a
hardcoded /Applications check would misread it as "not installed".

* fix(setup): resolve the docker app through LaunchServices, not fixed paths

A Homebrew `--appdir` can put OrbStack anywhere, so enumerating install
directories will always have a tail that reads a present app as missing
and sends setup to the wrong one. Fall back to LaunchServices when the
well-known directories miss: that is the same lookup `open -a` performs,
so availability now agrees with what the launch will actually do.

* fix(setup): settle the docker app with open(1) instead of probing for it

`path to application` can raise a modal "Where is …?" picker when the name
does not resolve, which in a terminal wizard reads as a hang. Drop it: the
launch itself already answers the question, since `open` exits non-zero
when macOS knows no such app, instantly and without UI.

That inverts the design. Rather than predict which app is installed and
then launch it, pick a provider, try to start it, and let the exit code
correct a guess — so the directory probe no longer has to enumerate every
possible install location to be right.

An explicit OrbStack selection is now never redirected to Docker Desktop.
The CLI is addressing OrbStack's socket, so `docker info` keeps failing no
matter how well Docker Desktop starts; the earlier fallback only replaced
a 90s timeout with a differently worded one. Say the context is stale and
how to fix it instead.

* fix(setup): honour `required` when the docker app fails to launch

db.ts and redis.ts call ensureDocker(false) and branch on the boolean to
offer an external Postgres or Redis instead. Throwing past that aborts the
whole wizard when a working non-Docker path was on the table, so every
post-confirm failure now warns and returns false unless Docker is required.

That covers the 90s-timeout throw too, which ignored `required` before this
branch existed — leaving it as the one path that still aborts would make
the flag mean two different things in one function.

Also name DOCKER_CONTEXT in the stale-selection hint. It overrides the
config context, so `docker context use` alone leaves the CLI pointed at
OrbStack and the next run fails identically.

* improvement(setup): don't tell CLI-runtime users to install Docker Desktop

Having the docker CLI but neither GUI app is exactly what a colima or
Rancher Desktop user looks like, and the failure told them to install
Docker Desktop — advice for a problem they don't have. Name the situation
accurately and add starting an existing runtime as an option.

---------

Co-authored-by: Bohdan Vilishchuk <iamtheflex@gmail.com>
2026-08-04 11:21:27 -07:00
Waleed 3d8b2ed050 improvement(docs): inherit the platform border and font-weight scales (#6252)
* improvement(docs): inherit the platform border and font-weight scales

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

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

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

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

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

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

* improvement(docs): finish the platform token sweep

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

Co-authored-by: Waleed Latif <walif6@gmail.com>
2026-08-04 10:28:00 -07:00
Waleed 2977db5133 fix(deps): revert next to 16.2.12, its 16.3.0 optimizer deletes live code (#6242)
Next 16.3.0's Turbopack optimizer models a bare `return <asyncCall>()` tail
call inside an async function as returning the promise object, then propagates
that always-truthy fact through the caller's `await`. Where the result feeds an
`if (x)` whose every branch returns, it concludes the branch is always taken and
deletes everything after it from the emitted bundle.

Two sites shipped to production that way:

- `POST /api/credentials` lost its entire create path — the transaction, the
  org locks, the insert, the audit, the 201. A first-time create fell into the
  existing-credential branch and threw on `existingCredential.id`, so every new
  credential 500'd.
- `upsertAsyncToolCall` collapsed to `async () => await getAsyncToolCall(id)`.
  The insert is simply gone; it returns null for every new async copilot tool
  call. Silent — no error, no failed request.

A differential scan of 71,266 source string literals across `.next/server` and
`.next/static`, comparing images built from the same commit on 16.2.12 and
16.3.0, found exactly these two and nothing else. That scan cannot see dropped
branches with no distinctive string literal, which is why the version goes back
rather than the two sites being patched alone.

Both are also hardened with `return await`, verified to defeat the miscompile in
a minimal reproduction. The TypeScript toolchain cleanup from the original bump
(dropping @typescript/native-preview, `useTypeScriptCli`) is kept.
2026-08-03 22:31:43 -07:00
Theodore Li 40cbee4934 improvement(skills): ask for feature flag granularity (#6238) 2026-08-03 23:45:20 -04:00
Waleed 856fe0ffb6 fix(docker): upgrade bun to 1.3.14 (#6236)
* fix(docker): upgrade bun to 1.3.14 to unbreak the Next 16.3.0 server

Bun 1.3.13 cannot load Next 16.3.0's compiled server runtime. The app container
runs the Next server under Bun (`oven/bun:1.3.13-slim`, `bun apps/sim/bootstrap.js`),
so every app-page render threw and `/api/health` returned 500:

  ⨯ Error: Failed to load external module
    next/dist/compiled/next-server/app-page-turbo.runtime.prod.js:
    TypeError: Expected CommonJS module to have a function wrapper.
    If you weren't messing around with Bun's internals, this is a bug in Bun

Isolated to Bun, not Next, by loading that exact module in the real images:

  Next 16.2.12 + Bun 1.3.13 -> loads (why staging was fine before)
  Next 16.3.0  + Bun 1.3.13 -> CJS wrapper error
  Next 16.3.0  + Bun 1.3.14 -> loads

Bun 1.3.14 is the current stable and already fixes it, so this bumps every pin
rather than reverting the framework upgrade, which would only defer the same
latent Bun bug to the next attempt.

Why no gate caught it: local dev machines and this bump's own verification run
Bun 1.3.14, while the container and CI pinned 1.3.13 — and CI only *builds* the
image, it never boots one and probes `/api/health`. A container smoke test in CI
would have caught this before merge; that is worth adding separately.

* fix(docker): align the remaining bun pins with 1.3.14

Two pins were missed in the first pass because the search was scoped to
docker/, package.json and .github/workflows/:

- .devcontainer/Dockerfile still built on oven/bun:1.3.13-alpine
- PI_BUN_VERSION in apps/sim/scripts/pi-sandbox-packages.ts was still 1.3.13,
  despite being documented as mirroring the root packageManager field, so Pi
  sandbox images would have kept installing the Bun release that cannot load
  the Next 16.3.0 server runtime.

Fixed surgically rather than with a repo-wide replace: "1.3.13" also appears
inside SVG path data in apps/sim/components/icons.tsx and
apps/docs/components/icons.tsx, which a blind sed would have corrupted.
2026-08-03 19:23:52 -07:00
Waleed 894083390e improvement(agent): allow variable references in reasoning effort, verbosity, and thinking level (#6233)
* improvement(agent): allow variable references in reasoning effort and verbosity

Reasoning Effort and Verbosity were select-only dropdowns, so a workflow could
not sweep them from a variable or an upstream block the way it already can with
the model. Both become editable comboboxes, matching the model field directly
above them and the managed-agent selectors.

- switch both subblocks to `combobox`, keeping their fetched per-model option
  lists intact
- keep them visible when `model` itself holds a reference, since the concrete
  model id is only known at execution time and cannot be matched against the
  static capability list
- normalize the resolved level in the provider chokepoint so a reference that
  resolves to `"High"` or to nothing behaves sanely instead of hitting a
  provider 400

* improvement(agent): allow variable references in thinking level

Extends the same treatment to Thinking Level so all three model-tuning fields
behave consistently, and logs a level a model does not declare.

- switch `thinkingLevel` to `combobox` with the reference-aware condition
- normalize it alongside the other two; an empty resolve now takes the
  deliberate "send nothing" path rather than the incoherent half-state it hit
  before, and stays distinct from an explicit `none`
- warn when a level is not one the model declares, still forwarding it: Sim's
  per-model lists drive the pickers and can lag a provider, and a sweep needs
  the provider's own error rather than a silent fallback to the default

* improvement(agent): report a model level the sanitizer discards

A model bound to a variable or block reference only resolves at execution time,
so a run whose reference landed on a model outside Sim's catalogue had its
requested level cleared with no signal and quietly fell back to that model's
default. Dropping stays the safe default — a provider with no such parameter
rejects the whole request — but it is now reported.

- log the field, model, and value whenever an unsupported-field level is cleared
- cover both diagnostics, including that they stay quiet for a declared level
  and for the `auto` / `none` sentinels

* fix(providers): redact resolved level content from sanitizer diagnostics

The model-level fields accept environment and block references, so an
unrecognized level is not necessarily a mistyped level — it is whatever the
reference resolved to, which can be secret content. The diagnostics added for
dropped and undeclared levels echoed it straight into server logs.

- log a level only when the catalogue declares it somewhere, or it is an
  `auto` / `none` sentinel; anything else is reported by length alone
- stop discarding levels for a model the catalogue has never seen. Absent is
  unknown, not known-incapable, and a reference is exactly how a newly released
  model arrives before Sim catalogues it — the provider decides instead. Models
  the catalogue knows, and every dynamic-provider id, keep the protective drop

* fix(providers): redact the level in Anthropic's unsupported-thinking warning

Forwarding an undeclared level is deliberate, but it means the Anthropic adapter
receives it and interpolates it straight into its "not supported, ignoring"
warning. Since the field is reference-bound, that value can be whatever a
mistyped `{{ENV_VAR}}` or block reference resolved to — so the redaction added
for the sanitizer's own diagnostics was leaking one layer downstream.

- promote the level renderer to `providers/utils` as `describeModelLevel`, the
  single gate every site echoing a caller-supplied level goes through
- use it in Anthropic's warning and in both sanitizer diagnostics

* refactor(providers): drop the sanitizer's level diagnostics

The two warnings logged server-side, where the workflow author who set the level
never sees them, and the surprising case they described — a level discarded for
a model newer than the catalogue — is now fixed at the source rather than
narrated. They also carried the redaction that leaked resolved content before it
was caught, so removing them removes that surface entirely.

Levels still normalize, and still drop for a catalogued model that does not take
the field. `describeModelLevel` stays for Anthropic's unsupported-thinking
warning, which is a pre-existing log this feature newly exposes to resolved
reference content.
2026-08-03 18:56:12 -07:00
Waleed d05289cfa2 fix(providers): route 6-10MB attachments to the provider large-file path (#6232)
* fix(providers): route 6-10MB attachments to the provider large-file path

The inline base64 cap was 10 MB of raw bytes, but the execution payload store
refuses a single value above 8 MiB and base64 inflates by 4/3. Every raw file
over 6 MiB therefore produced a base64 string the store rejected — and the
rejection came from the base64 *cache* write, which threw and failed the run
with "Execution memory limit exceeded" even though the bytes had already been
read successfully. Because shouldUseLargeFilePath only fires above the inline
cap, 6-10 MB attachments had no path at all on any provider: they never reached
the OpenAI or Gemini Files API upload they were supposed to take.

Derive the cap from the payload-store ceiling instead of hardcoding it, and
degrade a refused cache write to "not cached" rather than failing a request
whose bytes are in hand. Every other size guard in the chain compares raw bytes
against maxBytes; only the Redis write sees the encoded size, which is why this
went unnoticed — and why it failed only where Redis is configured.

Also correct the provider ceilings against the vendors' current documentation:

- openai: 50 MiB -> 50,000,000. The gate is `size > maxBytes`, so 50 MiB
  admitted 52,428,800 bytes; the docs say each file must be *under* 50 MB.
- bedrock: had no entry and inherited the inline cap, which is above what
  Converse accepts (3.75 MB per image, 4.5 MB per document).
- groq: 20 MiB -> 20,000,000, and modelled as the request cap the docs
  actually describe rather than a per-file MiB ceiling.
- fireworks: had no entry; its 10 MB budget is on the base64 total, so the
  raw-byte equivalent is 7.5 MB.

Add perRequestMaxBytes for the combined ceilings, enforced before any upload
spend, and cover the OpenAI upload path end to end — it had no test at all.

* chore(deps): upgrade @google/genai to 2.13.0 and @anthropic-ai/sdk to 0.115.0

@google/genai 2.x reworks the Interactions API, which the Gemini deep-research
provider is built on. Migrate it:

- `Interaction.outputs` (a flat content array) is now `steps`, a discriminated
  timeline; the report text lives in the `model_output` steps' text content,
  alongside thought and tool steps we skip.
- `Usage.total_reasoning_tokens` is now `total_thought_tokens`. The old code
  already fell back to that name through a cast, so this just makes the field
  the SDK actually returns the typed one.
- SSE events renamed: `content.delta` -> `step.delta`, `interaction.start` ->
  `interaction.created`, `interaction.complete` -> `interaction.completed`.
  The new event types are discriminated, so the payload casts are gone.

Both `interactions.create` calls also stop annotating their params with
`Interactions.CreateAgentInteractionParams{,Non}Streaming`. In 2.13.0 those
namespace aliases resolve to `CreateAgentInteraction`, whose `stream` is a
plain `boolean` rather than a literal — annotating with them erases the
discriminant and the call resolves to the union-returning overload, so the
result is typed as `Interaction | Stream` at every use. An inline
`stream: true as const` keeps the correct overload.

Neither upgrade required a `minimum-release-age` waiver: 2.15.0 and 0.115.0
were checked and 2.13.0 is the newest genai release clearing the 7-day window.

* chore(deps): upgrade openai to 7.0.0

v5 is the only major with real breaking changes for us; v6 widened a Responses
output type and v7 only raised the Node floor to 22, which apps/sim already
requires. Three things needed fixing:

`ChatCompletionMessageToolCall` became a union of function and custom tool
calls, and the custom variant has no `function` field — 43 unguarded `.function`
accesses across the OpenAI-compatible providers. Narrow once at each
`message.tool_calls` read through a shared `isFunctionToolCall` guard rather
than casting at every use.

That guard deliberately tests for the `function` payload instead of
`type === 'function'`. Many OpenAI-compatible vendors omit `type` on tool calls
entirely — our own fixtures do — so discriminating on it type-checks perfectly
and then silently drops every tool call those providers return.

`ChatCompletionCreateParams.verbosity` narrowed from `string` to a literal
union, and the Responses API's output and input item unions now diverge on
members Sim never emits (computer-use call outputs, whose `status` admits
`failed`, and the `AdditionalTools` escape hatch). Echoing output back as input
is what a tool loop is supposed to do, so that conversion is asserted once in
convertResponseOutputToInputItems and the streaming loop now routes through it
instead of pushing raw output items.

The hand-rolled multipart upload in file-attachments.server.ts can now be
replaced with the SDK's typed `expires_after` — left for a follow-up so this
commit stays a pure upgrade.

* fix(providers): correct defects found auditing the attachment and SDK changes

The mechanical rewrite that added `isFunctionToolCall` to every `tool_calls`
read also rewrote three truthiness guards, where the filtered array was
computed, discarded, and the unfiltered value used in the body. Filter once and
use that value. The helper also landed between `trackForcedToolUsage`'s TSDoc
block and its declaration, leaving that block documenting the wrong function.

Raise the Bedrock ceiling from 3.75 MB to 4.5 MB. Converse caps an image at
3.75 MB and a document at 4.5 MB, and a single `maxBytes` cannot express both.
Taking the lower bound looked conservative but regressed 3.75-4.5 MB documents,
which Converse accepts and which work today. At the document bound every size
that works now still works, and only genuinely-too-large files are rejected
early; oversized images in that band keep surfacing as a Bedrock API error,
exactly as they do without the entry.

Both limits re-verified verbatim against the primary docs: Converse's Message
reference ("Each image's size ... no more than 3.75 MB", "Each document's size
must be no more than 4.5 MB") and Fireworks' vision guide ("Total base64-encoded
images must be less than 10MB").

* fix(providers): keep every attachment that works today working

Auditing the routing change for backwards compatibility turned up two bands it
silently broke.

Lowering the single inline cap made the upload path mandatory above ~6 MB, but
every large-file path reads its bytes back out of cloud object storage. A
deployment without it — local dev, any disk-backed self-host — inlines those
files as base64 today and would have started failing outright with "requires
cloud file storage". Split the one number in two: the inline ceiling stays at
10 MiB, and a separate threshold marks where an upload becomes *preferable*
because the base64 copy no longer fits the payload store. Where no upload path
is reachable, base64 hydration now runs to the inline ceiling as before, and a
missing cloud-storage backend leaves the file for the inline path instead of
throwing.

The two strategies also cross over at different sizes now. `files-api` carries
every type the provider already accepts, so it takes over at the lower
threshold. `remote-url` only fetches images and PDFs, so switching early would
have started rejecting 6-10 MB text documents that inline fine today; it takes
over only once inlining is genuinely impossible.

Revert the Groq ceilings. Its published "20MB" governs a request carrying an
image URL, and on this path the body holds only the URL, so it cannot bind on
the files maxBytes guards. Groq documents no limit on the image it fetches, so
tightening the per-file cap to 20,000,000 and summing raw bytes against the
request cap would both reject uploads that work today on no documented basis.

* fix(providers): drop the provider ceiling changes and close the audit findings

A six-agent line-by-line audit against the vendors' live docs found the
`models.ts` ceiling work was not the strict improvement it was written as, so
all of it is reverted:

- bedrock's 4.5 MB cap broke video. Converse takes image, document AND video
  blocks, and video is allowed 25 MB base64 — a single `maxBytes` cannot
  express three content classes, and every 4.5-10 MiB `.mp4` that works today
  would have started failing.
- openai's combined 50 MB cap is the FILE-input limit. Image inputs are
  governed separately at 512 MB / 1500 images, so summing every attachment
  rejected eight 8 MB PNGs that OpenAI documents as legal.
- fireworks' per-file ceiling was unreachable behind the request budget, while
  the upload picker went on advertising it — a size the UI accepts and
  execution always rejects.
- The whole `perRequestMaxBytes` feature goes with them: it summed raw bytes
  against caps that are variously on encoded bytes, on one content class, or on
  a body that carries only URLs, and it double-counted a file referenced from
  several messages even though the uploader dedupes by key.

Only openai's per-file `maxBytes` stays corrected, to decimal 50,000,000 — the
one number a vendor states unambiguously and writes no MiB against.

Also fixed, all found by the same audit:

The hydration cap stopped short of where `remote-url` actually switches over,
so 6-10 MiB attachments on anthropic/openrouter/xai/groq/together/baseten/vllm
had neither base64 nor a handle and failed outright — the very band this branch
exists to fix. Both decisions now come from one function so they cannot drift.

Eight more sites where the mechanical rewrite computed a filtered array and
then read the unfiltered one (deepseek, sakana, nvidia, kimi), leaving those
providers without the narrowing they appear to have.

`isFunctionToolCall` threw on a null or primitive `tool_calls` entry, because
`in` requires an object — reachable exactly on the self-hosted gateways this
filter was added for. It is now total, and all 32 test mocks match it rather
than being quietly more permissive.

`checkForForcedToolUsage` in utils/litellm/mistral evaluated the response before
the `tool_choice` test, turning a tolerated malformed body into a TypeError on a
path that never used to touch it.

Gemini: `satisfies` restores the excess-property checking the dropped
annotations removed, the poll loop recognises the terminal statuses v2 added
instead of spinning for an hour and reporting a timeout, and the streaming doc
block no longer names five events that were renamed six lines below it.

* fix(providers): report attachment limits in the unit vendors publish

The size ceilings are decimal MB — that is how OpenAI, AWS and Fireworks all
write them — but the error messages divided by 1024², so OpenAI's 50 MB cap
was reported to the user as "48MB". Someone shrinking a 49 MB file to get
under it was chasing a limit that does not exist.

One formatter, used by all three messages, so the file size and the ceiling in
the same sentence are always in the same unit.

* fix(providers): derive the limit unit from the ceiling it belongs to

The previous commit fixed OpenAI's "48MB" by dividing every ceiling by 10⁶ —
which broke the other seven. Only OpenAI's constant is decimal; anthropic,
google, together and openrouter are 50 MiB, baseten and vllm 25 MiB, groq and
xai 20 MiB. Rendering those as decimal MB overstated each by ~5%, so a 21 MB
file on groq was rejected with "(21MB) exceeds the 21MB limit" — a sentence
that contradicts itself and sends the user to shrink a file to a size that is
still over. Same class of bug as the one being fixed, sign flipped.

Both figures now render through one unit taken from the ceiling, so the number
a user is told is the number the vendor publishes and the two sizes in a
sentence are always comparable. Tested against every ceiling in the registry
rather than only the values that happened to round cleanly.

Two more from the same audit:

A file with a missing or zero declared size was stranded on a files-api
provider: hydration bailed on the real byte length while `shouldUseLargeFilePath`
saw `0 > threshold` as false, so it got neither base64 nor a handle and failed
as "may no longer be accessible" — a size failure wearing an access failure's
message. Uploads read the real bytes and enforce the ceiling themselves, so an
unknown size now routes to one.

The oversized-attachment error blamed the provider for a deployment problem:
a files-api provider on a host without cloud storage reported that the provider
"has no large-file upload path", which is not true of the provider.

`isFunctionToolCall` only proves `function` is present, never that it is well
formed, so the trace enricher is defensive again about a hollow payload without
giving up the compile-time gate. The 32 test mocks now match production exactly.

* fix(providers): stop an over-limit size rendering as the limit itself

Deriving the unit from the ceiling fixed the 5% error but left the precision
fixed at two decimals, so a file one byte over a 20 MiB cap still printed
"(20.00MB) exceeds the 20MB agent attachment limit" — the same self-contradicting
sentence, now in a ~5 KB band above every ceiling in the registry. The size
rounds up and the ceiling rounds down, so the two can no longer collide.

The test that was supposed to guard this asserted a file 0.03MB over and an
OpenAI file that was under the limit — neither anywhere near the band — so it
passed while the bug was live. It now walks `limit + 1` for every ceiling, and
goes red against the old rounding.

The reason clause added last commit also claimed a deployment had no cloud file
storage whenever the strategy was not inline. A generated document on a
remote-url provider reaches that same error with storage fully configured,
because a signed URL points at the generation source rather than the rendered
artifact — so it was told something false about its own deployment. That case
now names itself.

* fix(providers): order the attachment failure reason by how general the cause is

The generated-document arm was checked first, so it won over both other causes
and told users two things that were not true.

On an inline-strategy provider — bedrock, mistral, ollama, fireworks, litellm,
vertex, kimi — there is no upload path for any file, generated or not, but the
message blamed the document format and implied a plain PDF would go through.

On openai or google with cloud storage unconfigured it was simply false: a
generated document does take the Files API path there, and that exact file
uploads fine once storage exists. The one actionable fix was hidden from the
operator.

A provider with no upload path cannot be helped by changing the file, and a
deployment with no object storage cannot reach any upload path whatever the
file is, so both now outrank the format-specific case — which is left saying
only what is true of it: a signed URL points at the generation source rather
than the rendered file.

The formatter is unchanged. It was brute-forced over every real ceiling and
three million random pairs with no collision or inversion, but the test's six
ceilings all divide to exact integers, so floor, round and ceil are
indistinguishable on them and the limit-side rounding was unpinned. A ceiling
with a fractional remainder now covers it.
2026-08-03 18:39:27 -07:00
Waleed ed17bb2bac chore(deps): upgrade next to 16.3.0 and clean up the TypeScript toolchain (#6235)
Bumps next, @next/env and the @next/swc-* optional deps to 16.3.0 across the
root overrides, apps/sim, apps/docs and packages/emcn.

Two config notes worth keeping:

- `experimental.turbopackFileSystemCacheForBuild`'s default flipped false ->
  true for stable in 16.3.0, so our explicit `false` is now load-bearing rather
  than defensive. Without it this bump would have silently re-enabled a build
  cache measured 3.2x slower on this codebase (#6078). Comment updated to say so.
- `experimental.useTypeScriptCli: true` is now pinned. TypeScript 7 ships no
  JavaScript compiler API until 7.1, so Next's default checker cannot run and
  needs the project-local `tsc` CLI instead. 16.2.12 was silently skipping
  build-time type checking entirely because it detected @typescript/native-preview
  and short-circuited the stage ("Finished TypeScript in 138ms"); pinning the flag
  keeps that from drifting back.

TypeScript toolchain cleanup that the upgrade makes possible:

- Drop @typescript/native-preview from apps/sim and apps/docs. It was the
  pre-release channel for TS 7 and is superseded by typescript@7 (nightlies now
  ship as typescript@next), and its presence is what suppressed build type checks.
- Align packages/browser-protocol and packages/terminal-protocol from
  typescript ^5.7.3 to ^7.0.2 so every workspace is on one compiler version.

apps/sim keeps @typescript/typescript6 as a production dependency: the function
sandbox at app/api/function/execute dynamically imports the TS 6 compiler API to
transpile user code, and TS 7 has no API to replace it yet.

Build and dev were benchmarked 3x per version on a byte-identical tree; the
upgrade is performance-neutral (build median 100s -> 99s, dev:full warm 10s -> 9s).
2026-08-03 18:30:11 -07:00
Waleed f210a6edc0 fix(execution): offload buffered event values under budget pressure (#6231)
* fix(execution): offload buffered event values under budget pressure

An execution buffers EVENT_LIMIT events inside a per-execution byte budget, so
a full ring only fits if events average under budget/EVENT_LIMIT. Values were
only offloaded to object storage at the shared 8 MiB cap, far above that, so a
run emitting large block outputs exhausted its budget within a few dozen events
and stayed pinned at its ceiling for the rest of its life.

Applying that ceiling to every run would be worse than the problem: the SSE
stream carries the compacted event and the terminal renders a ref only as a
preview, so ordinary block outputs would stop being readable live, and every
value would cost an object-storage write on the hot path. Engage the tight
ceiling only once a run has actually buffered past half its budget. A short run
keeps full-fidelity output and pays nothing; a runaway one stops accumulating.

Both bounds derive from the existing budget rather than being asserted, and
preserved UserFile base64 is exempt — it is an explicit request for inline
delivery, already bounded by its own cap and the strip-and-recompact fallback.

Also stop a failed resume-path buffer write from failing the run: it was
awaited bare, so the rejection propagated into the executor callback and failed
work that had already completed. The buffer only backs reconnect replay, so
degrade to live-only delivery the way the execute route does.

* fix(execution): measure pressure at write time and keep terminal status last

Pressure was read from bytes counted once a flush succeeded, but a burst is
compacted long before the scheduled flush runs — so the very batch that
exhausts the budget went through at the loose ceiling and was dropped instead
of offloaded. Count bytes as each event is compacted.

Separately, the terminal-alone retry stamped terminal status while entries
queued ahead of it were still unwritten. Terminal status is the reader's
end-of-run signal: a reconnecting client drains what is in Redis and closes, so
those entries were stranded behind a stream it had already finished with. Drain
the backlog first, then publish the terminal event.

* fix(execution): do not lose the backlog or publish terminal status early

Draining the backlog ahead of the terminal event left the terminal status armed,
so whichever chunk emptied the queue stamped the run complete before its
terminal event was written — the inverse of the ordering the drain was added to
guarantee. Disarm the status for the drain and restore it afterwards.

The drain's result was also discarded: a transient Redis failure requeues its
batch, and the unconditional reassignment that followed dropped those events
even though the budget never rejected them. Keep whatever could not be
persisted, and publish the terminal event alone only once nothing earlier is
still queued — failing otherwise lets the caller degrade, which records the
status without claiming the missing events arrived.

Leave eventId unset on a failed resume-path write. Assigning 0 was persisted by
clients as a reconnect cursor and rewound them to the start of the run.

* fix(execution): keep an event in the buffer when a pressure offload fails

Durable compaction runs before an event is queued, so a storage or metadata
failure dropped it from replay entirely — a reconnecting client would never see
it, even though the live path carried on. Offloading under pressure is only an
optimization that keeps a heavy run from exhausting its budget, so when the
value cannot be persisted, fall back to buffering it inline: exactly what the
run would have done before pressure engaged.
2026-08-03 17:23:14 -07:00
Waleed 41592dfbf1 fix(execution): stop the event buffer retaining a run-length backlog (#6229)
The Redis byte-budget branch in doFlush requeued the rejected batch and
rethrew, skipping the MAX_PENDING_EVENTS trim every other failure path
applies. The backlog then grew for the rest of the run and each retry
re-serialized it, so a wide parallel fan-out could drive unbounded heap
growth and stall the event loop.

Drop rejected chunks instead of requeueing, pace retries through the
existing backoff, and split batches that exceed the single-write cap so
an oversized batch can make progress instead of stalling forever.
Terminal status is now writer-scoped, since a concurrent scheduled flush
can be the loop that drains the final chunk, and a terminal event whose
batch was dropped is retried on its own rather than lost with it.

Record terminal stream meta when the terminal event cannot be buffered,
so reconnecting readers stop polling an active stream until their
deadline. Drop the unused reserve/release budget helpers.
2026-08-03 16:19:07 -07:00
Siddharth Ganesan 5ab5f2c7ed feat(browser, terminal): implement browser driver, password manager, terminal features (#6196)
* icon styling

* feat(desktop): isolate chat browser and terminal sessions

* feat(desktop): uncap browser and terminal tabs

* feat(desktop): polish browser and terminal resources

* improvement desktop

* fixes

* updates

* fixes

* fix

* update tests
2026-08-03 16:00:04 -07:00
Waleed 3de63c94e3 feat(self-host): align Docker Compose with Helm and overhaul self-hosting docs (#6225)
* feat(self-host): align Docker Compose with Helm and overhaul self-hosting docs

Docker Compose shipped no scheduler, so scheduled workflows, every polling
trigger, connector syncs, the outbox, and data drains silently never ran.
Adds a cron service running the same 18 jobs the Helm chart schedules as
CronJobs, and closes the remaining behavioral gaps between the two paths:
bundled Redis in the chart, no hosted plan caps in chart defaults, pinned
image tags, and fail-fast secrets. A CI check keeps the schedulers in sync.

Also rewrites the self-hosting docs: 14 new pages, 8 updated, reorganized
into Install / Configure / Operate.

* fix(self-host): drop bun install from chart CI, remove air-gapped and backup docs

The scheduler-parity check pulled a full dependency install into the
chart-validation job, which fails building isolated-vm on that runner.
Rewritten to use only node builtins so the job installs nothing.

Also removes the air-gapped and backup/restore pages, and stops pinning a
concrete release in the docs so the examples do not go stale each release.

* fix(helm): bundle Redis in secret-manager modes unless the URL is supplied

Suppressing Redis whenever a secret mode was active left those deployments
with no Redis at all — REDIS_URL is optional there and both shipped examples
omit it. The chart now steps aside only on a detectable signal: an explicit
app.env.REDIS_URL, an ESO remoteRefs.app.REDIS_URL mapping, or the new
redis.provideUrl=false opt-out for a pre-created Secret it cannot read.

* fix(compose): derive realtime BETTER_AUTH_URL from NEXT_PUBLIC_APP_URL

realtime read BETTER_AUTH_URL directly and fell back to localhost while
simstudio derived it from NEXT_PUBLIC_APP_URL, so setting only the public
origin left realtime authenticating against http://localhost:3000.

* fix(helm): deliver bundled REDIS_URL via ConfigMap so an operator value always wins

Injecting REDIS_URL as an inline container env made it beat every envFrom
source, so a REDIS_URL held in a pre-created Secret or synced by External
Secrets was silently shadowed and traffic moved to a fresh in-cluster Redis.

Kubernetes resolves duplicate envFrom keys by letting the last source win, so
the bundled URL now ships as a ConfigMap listed before the app Secret. Any
operator-supplied value overrides it without the chart needing to read it,
which also removes the redis.provideUrl flag the previous attempt required.

* docs(helm): spell out the egress rule external datastores need

The default NetworkPolicy allows 443 plus the bundled Postgres and Redis by
pod selector. Anything you run outside the chart on another port needs its own
rule, which is easiest to miss when REDIS_URL arrives via a Secret the chart
cannot inspect. Adds a copyable example to the production checklist and the
security guide.

* feat(helm): add networkPolicy.allowExternalEgress for managed datastores

The default policy allows 443 plus the bundled Postgres and Redis by pod
selector, so a managed datastore on another port needs a hand-written CIDR
rule — awkward when REDIS_URL arrives via a Secret the chart cannot inspect.

Adds an opt-in switch that drops the port restriction while still blocking the
cloud metadata endpoints. Defaults to false, keeping this chart stricter than
the common chart default of unrestricted egress.
2026-08-03 14:52:48 -07:00
Waleed 1242301bd3 improvement(emcn): share one emails/domains chip input across share and deploy modals (#6226)
* improvement(emcn): share one emails/domains chip input across share and deploy modals

Extracts the emails chip lifecycle out of ChipModalField type='emails' into a
standalone ChipEmailsInput, and points both the file share modal and the deploy
modal's chat tab at it instead of their hand-rolled TagInput wiring.

- add ChipEmailsInput (dedupe, normalize, format gate, paste, per-chip errors)
  with an allowDomains opt-in for bare @domain.tld entries
- share modal and deploy modal chat tab now use it; drop both hand-rolled
  add/remove/validate implementations and the dead emailError state
- move the shared allowlist policy into validateAllowlistEntry
- drop the "Add specific emails or whole domains" hint text
- give OutputSelect a size prop; the deploy modal chat tab uses the 30px chip
  trigger so it lines up with the Title field above it
- drop overflow-y-auto from the chat deploy form, which was promoting overflow-x
  to auto and rendering a stray horizontal scrollbar

* improvement(utils): one email syntax gate, drop the backtracking placeholder regex

Audit follow-ups on the emails chip input.

- move EMAIL_SYNTAX_REGEX and the new @domain pattern into @sim/utils/string as
  isValidEmailSyntax, so emcn and lib/messaging/email/validation.ts stop keeping
  byte-identical copies of the RFC 5322 regex
- allow single-label domains (@intranet) again — requiring a dot rejected
  entries the old startsWith('@') check accepted, which self-hosted
  deployments use. A lone @ and malformed labels stay rejected
- replace derivePlaceholderWithTags' /^Enter\s+(.+?)s?$/i with string ops;
  CodeQL flagged it as polynomial backtracking (js/redos). Verified identical
  output across the placeholder shapes in use
- forward the emails control's props explicitly instead of underscore-discard
  destructuring, matching ChipModalFileControl in the same file

* test(email): pin the allowlist entry rules

Covers isValidEmailSyntax's allowDomains branch (single-label domains stay
valid, malformed bare domains that the old startsWith('@') check accepted do
not), the 254-character cap, the DNS label limit, and validateAllowlistEntry
waiving address-level policy for bare domains. Verified both new rules fail
when the behavior is reverted.
2026-08-03 14:40:41 -07:00
Waleed 67e355b1ec chore(landing): remove the Russell Investments logo (#6228) 2026-08-03 14:34:59 -07:00
Waleed c759a889d8 refactor(voice): load STT availability through React Query (#6224)
* refactor(voice): load STT availability through React Query

useSpeechToText fetched `/api/settings/voice` inside an effect and stored
the result in useState behind a hand-rolled mountedRef guard: no cache, no
dedupe across mounts, and no AbortSignal, so the response was fetched and
parsed even after unmount. Two simultaneously mounted consumers issued two
requests. It also bypassed hooks/queries/**, which is where every other
server read in the app lives — and it escaped `check:react-query`, whose
audit only covers useQuery/useMutation call sites.

The value is server env read at request time, so it cannot change within a
session; the new hook uses an infinite staleTime and a caller-controlled
`enabled` so clients without the audio APIs never issue the request.

Hydration is unchanged: SSR renders unavailable, and the first client
render still resolves unavailable because `data` is undefined until the
fetch settles. No initialData, deliberately — adding it would break that.

mountedRef stays; it is still load-bearing for the streaming lifecycle.

* test(queries): unmount rendered roots between tests

renderHookWithClient created a React root per test but never tore it
down, so trees stayed mounted with live QueryClient observers until
worker teardown and async notifications could cross test boundaries.

Audited every test in the repo using createRoot: 51 of 53 already
unmount. The two that did not were both mine — voice.test.tsx here and
chats.test.tsx from #6223 — so both are fixed and the pattern is now
uniform.

* fix(voice): let a failed STT probe recover on a later mount

The app QueryClient sets retryOnMount: false and retry: 1, and
refetchOnWindowFocus only refetches stale queries — which an infinite
staleTime never becomes. So one transient failure cached the error for
the life of the client and hid the mic until a full page reload.

The effect this replaced refetched on every run, so retryOnMount: true
restores parity: no refetch after success, a retry per mount after
failure. Test asserts recovery under the app's real query defaults and
fails without the override.
2026-08-03 13:19:08 -07:00
Theodore Li 3f70096841 improvement(self-host): gate email verification on a mail provider, add self-host settings, land setup on signup (#6216)
* fix(auth): skip email verification when no mail provider is configured

Signup pushed /verify unconditionally, stranding self-hosted deployments
with no mail provider on a screen no email could ever satisfy. Derive one
server-side effective value (verification enabled AND deliverable) and read
it from Better Auth enforcement, signup routing, and the verify page.

* feat(settings): add a self-host section with the managed Chat keys link

Self-hosters had no in-app pointer to the managed service that issues their
Chat keys. New Settings > System > Self-host section, gated on `requiresSelfHosted`
so it is absent on hosted Sim, containing only that link.

* improvement(setup): land the wizard handoff on signup

A freshly provisioned deployment has no accounts and / renders the marketing
landing page, so the bare origin left operators hunting for the CTA. Single-source
the URLs and point every open-Sim handoff at /signup across all three modes.

* improvement(settings): mark the self-host section with a sprout

Server was already doing double duty for MCP servers and Mothership, and the
icon set ships no botanical glyph, so the mark is a text emoji.

* improvement(settings): draw the sprout as an emcn line icon, move to Platform

The emoji rendered in the platform's own colors, so it was the one glyph in the
nav that ignored --text-icon. Replaced with a hand-drawn emcn Sprout (24 grid,
1.55 stroke, currentColor) matching the house style, renamed the tab to
Self hosting, and regrouped it under Platform — self-hosting is deployment-wide,
not per-workspace. Still self-hosted-only.

* improvement(settings): drop the section header from self hosting

One row does not need a section label, and removing it takes the divider with
it. The body is now the Chat keys row and its managed-keys link, nothing else.
2026-08-03 15:58:15 -04:00
Waleed 1708173970 refactor(chat): clean up the deployed chat surface (#6220)
* refactor(chat): clean up the deployed chat surface

Eight-angle cleanup pass over the full contents of the chat surface and
the speech code that survived the voice-mode removal.

Dead code
- enforceChatRateLimit: added for the TTS relay in #6212, orphaned when
  #6215 deleted that route. Zero consumers.
- ChatToolCallStatus, ChatErrorType, and six unused CHAT_ERROR_MESSAGES
  keys (only GENERIC_ERROR and CHAT_UNAVAILABLE are read).
- scrollToMessage was declared and destructured by ChatMessageContainer
  but never used in its body; removing the prop also made the
  scrollToShowOnlyMessage branch unreachable, since the sole caller
  passed true.
- permissionState and the language prop on useSpeechToText: both
  write-only across the repo.
- The image branch in ChatFileDownload's renderIcon returned the same
  DefaultFileIcon at the same size as the fallback.
- chatKeys.status/detail: aliases of deploymentKeys nothing imported,
  and misleading since they root under a different key namespace.

Redundant state
- password-auth and email-auth each kept a boolean in lockstep with
  `errors.length > 0`; email-auth also validated on every keystroke and
  then immediately hid the result.
- file-download tracked hover in state to drive one opacity class; now
  group-hover. Verified emcn Button sets no `group` class of its own.

Memoization
- ChatMessageContainer's memo() could never bail: chat.tsx passes an
  inline arrow for scrollToBottom and displayMessages is a fresh array.
  Four of the five things that re-render ChatClient are its props
  anyway, so the memo is dropped rather than propped up.
- ClientChatMessage keeps its memo — it blocks markdown re-parsing —
  but loses the custom comparator, which compared proxies (a
  key:status fingerprint, files by length) and ignored attachments and
  type entirely. Default shallow compare on its single prop is both
  simpler and stricter.
- Six useCallbacks whose consumers are native DOM handlers or inline
  arrows, so nothing observed their identity.

Effects
- The scroll listener attached in an effect keyed on [chatConfig,
  authRequired] — values it never reads, standing in for "the container
  has mounted". It now attaches via a ref callback, so it no longer
  re-attaches on every config refetch.

Design system and a11y
- z-[100] -> z-[var(--z-dropdown)] (same value), shadow-lg ->
  shadow-medium, list styles from inline style to Tailwind classes,
  hover: -> hover-hover: on touch-reachable targets, Check sourced from
  emcn alongside its Duplicate pair.
- Accessible names on the remove-attachment, stop, and send buttons,
  which announced only as "button".
- Dropped a keyboard handler on a role='group' div with no tabIndex,
  where target === currentTarget was unreachable, and the Tooltip
  Provider wrappers and delayDuration, which emcn documents as
  no-op passthroughs.

* fix(chat): restore markdown list markers

The design-system pass swapped inline `listStyleType` for Tailwind
classes, but the edit that added `list-disc`/`list-decimal` silently did
not apply while the one removing the inline style did. With Preflight
setting `list-style: none`, every bullet and number in an assistant
response disappeared. `list-item` on the `li` sets display only, not the
marker type.
2026-08-03 12:53:04 -07:00
Waleed b0491f1161 fix(chat): invalidate deployment queries after a chat mutation (#6223)
PATCH /api/chat/manage/[id] calls performFullDeploy when the workflow has
drifted from its active deployment, so editing a chat can mint a new
deployment version. useUpdateChat invalidated only chatStatus and
chatDetail, leaving the deployment panel showing the previous version and
a stale "needs redeployment" indicator until the staleTime expired.

Both mutations now route through invalidateDeploymentQueries, the shared
helper the rest of the deployment surface uses. That also picks up
deployedState, which useCreateChat's hand-rolled list had omitted even
though performChatDeploy replaces the deployed workflow state.

Tests cover both mutations and were verified to fail against the previous
invalidation.
2026-08-03 12:52:48 -07:00