Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
465bdbd
fix(settings): keep billing header stable (#7010)
waleedlatif1 Aug 23, 2026
f386769
fix(files): allowlist the schemes a markdown link may target (#7012)
waleedlatif1 Aug 23, 2026
ed335d4
improvement(pricing): list Sandboxes as a Max and Enterprise feature …
icecrasher321 Aug 23, 2026
b44d285
fix(files): download a markdown file as a zip only when it really has…
waleedlatif1 Aug 23, 2026
1cb9c86
improvement(chat): speed up conversation navigation (#7011)
waleedlatif1 Aug 23, 2026
9cecf0b
improvement(provenance): aggregate and attribute unrecorded durable r…
icecrasher321 Aug 23, 2026
a20a546
perf(db): optimize recurring query paths (#7014)
waleedlatif1 Aug 24, 2026
fc7aa66
fix(api): withhold internal failure messages from internal route resp…
waleedlatif1 Aug 24, 2026
49593b3
refactor: replace hand-rolled utilities and dead code with the shared…
waleedlatif1 Aug 24, 2026
cc08749
refactor(utils): add slugify and adopt it at the eight sites that han…
waleedlatif1 Aug 24, 2026
95d08d2
fix(ci): stop the migration safety audit from passing on a branch it …
waleedlatif1 Aug 24, 2026
297e970
refactor: delete code nothing reaches (#7019)
waleedlatif1 Aug 24, 2026
0cbff0e
fix(settings): report a failed settings write instead of reporting su…
waleedlatif1 Aug 24, 2026
7e0d868
fix: surface an unbilled run, and clamp the google-docs page cap (#7025)
waleedlatif1 Aug 24, 2026
cc16d23
fix(react-query): close the lint's blind spots, and the drift they hi…
waleedlatif1 Aug 24, 2026
f37c24e
fix(ci): walk every route entry the workspace app composes, not just …
waleedlatif1 Aug 24, 2026
1aa714c
fix(ci): stop failing the API audit for adding a compliant route (#7027)
waleedlatif1 Aug 24, 2026
82b02fb
improvement(ui): standardize modal default actions (#7029)
waleedlatif1 Aug 24, 2026
67fc2ae
fix(ci): require a default export before treating a file as a route e…
waleedlatif1 Aug 24, 2026
528b34f
fix(workflow): hide idle nested subflow end handles (#6976)
BillLeoutsakosvl346 Aug 24, 2026
efe8a14
fix(ci): give push builds a base their audits can actually read (#7033)
waleedlatif1 Aug 24, 2026
fbeea53
fix(files): normalize encoded embedded ids (#7035)
waleedlatif1 Aug 24, 2026
edf07ec
fix(vllm): support LM Studio endpoints (#7036)
waleedlatif1 Aug 24, 2026
cae80c5
chore(lint): turn on the rules that would have caught the dead code (…
waleedlatif1 Aug 24, 2026
04380b7
fix(webhooks): requeue deliveries dropped by retryable setup infrastr…
icecrasher321 Aug 24, 2026
cc61118
improvement(provenance): attribute stored-envelope display reads to t…
icecrasher321 Aug 24, 2026
6d313a4
refactor(renderer): drop the isWorkflowRunning prop the views never r…
waleedlatif1 Aug 24, 2026
472532e
feat(tables): confirm view deletion (#7041)
j15z Aug 24, 2026
445ef62
fix(oauth): bind update access to selected credential (#6999)
BillLeoutsakosvl346 Aug 24, 2026
aba6811
refactor: remove five more dead prop chains (#7046)
waleedlatif1 Aug 24, 2026
3251bf1
fix(ci): make the utils gate see the wrapped forms of what it bans (#…
waleedlatif1 Aug 24, 2026
0a5b380
feat(secrets): let workspace secrets opt out of redaction (#7045)
icecrasher321 Aug 24, 2026
626f6e5
fix(consent): enforce consent-aware analytics (#7049)
waleedlatif1 Aug 24, 2026
3ce9953
chore(legal): update privacy policy (#7050)
waleedlatif1 Aug 24, 2026
49e2bb5
fix(copilot): show custom block names in read tool rows (#7044)
j15z Aug 24, 2026
7b761ba
feat(setup): add incremental Chat setup (#7043)
TheodoreSpeaks Aug 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
10 changes: 10 additions & 0 deletions .claude/rules/emcn-components.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,16 @@ The menu surface intentionally diverges from the pill: `dropdown-menu.tsx` items
- **`ChipTimePicker`** — minute-granular time sibling of `ChipDatePicker`, a `ChipInput` that leniently parses typed input (`9:47`, `947`, `2:05pm`, `14:30`), commits on Enter/blur, and re-renders the canonical `9:47 AM` label.
- **`DropdownMenu`** — the canonical context/action menu (Radix-backed). Not a chip, but the standard menu for command/action lists; reach for it instead of a hand-rolled popover. Its surface intentionally diverges from the chip pill (`text-small`, `gap-2`) — keep them distinct. For a pill that opens a value picker, use `ChipDropdown`/`ChipSelect` instead.

## Modal keyboard defaults

Declare keyboard intent on the action-owning primitive; never add document-level or per-callsite Enter listeners.

- `ChipModalFooter` defaults to `defaultAction='primary'`. A plain Enter in a canonical single-line field or a custom plain input invokes the enabled primary action. Use `'none'` when submission must require an explicit click, such as an irreversible destructive action or an editor whose nested control owns Enter. Use `'dismiss'` only when dismissal is genuinely the modal's default decision.
- `ChipConfirmModal` fails safe with `defaultAction='dismiss'`. Opt into `'confirm'` only for an audited, low-impact reversible or non-destructive decision. Deleting an aggregate resource such as a workflow, table, knowledge base, or folder remains `'dismiss'` even when it can be restored, because the action takes a broad dependent graph offline. Use `'none'` for typed confirmations and severe account, ownership, or access changes. Button color never determines keyboard behavior.
- Textareas, native forms, buttons, links, comboboxes, menus, listboxes, tag/email inputs, IME composition, modified Enter, and disabled or pending actions retain their native behavior. A native form remains the sole submission path so browser validation is not bypassed.
- A custom field containing a search, token editor, or another input that owns Enter must set `submitOnEnter={false}` on `ChipModalField`. Do not attach a duplicate `onKeyDown` handler merely to call the footer action.
- Initial focus goes to the first visible editable text control. With no text control, the declared real button receives focus; `'none'` focuses the dialog surface. A safe dismiss default never turns Enter in a text field into data loss—the field simply does not publish a submit action.

## Authoring principles

- **One source of truth for shared chrome.** Compose from `chip-chrome.ts` / `chipVariants`; never duplicate the chrome string.
Expand Down
13 changes: 13 additions & 0 deletions .claude/rules/sim-react-performance.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,19 @@ const [{ id }, { kbName }] = await Promise.all([params, searchParams])

Only keep awaits sequential when a later call genuinely uses an earlier result, or when the ordering is deliberate (rate-limited batches, retry loops, write-then-read).

## Prefetch dynamic destination lists on intent

For long lists of dynamic destinations, do not viewport-prefetch every row and do not assume
`router.prefetch()` warms the full route: in Next 16 it uses the automatic/PPR strategy. Gate
`<Link prefetch={true}>` behind deliberate hover or keyboard focus, and prefetch destination
server state with the consumer's shared React Query options. A short, cancelable hover dwell
avoids drive-by downloads. Do not treat `touchstart` as intent because it also begins scrolling;
let the actual unmodified click start the data request.

If a continuity-focused surface intentionally omits `loading.tsx` so the current view remains
mounted until its peer is ready, the intent path must warm both the full route and its critical
data. Otherwise keep the loading boundary so dynamic navigation remains responsive.

## Local feature barrels are the convention — do not "fix" them

Tooling (e.g. react-doctor's `no-barrel-import`) will flag imports from local `index.ts` barrels as a bundle cost. In this repo that is a **false positive**: barrel imports for 3+ export folders are mandated by `.claude/rules/sim-imports.md`. Leave them.
8 changes: 6 additions & 2 deletions .claude/rules/sim-settings-pages.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ The Next.js `settings/[section]/layout.tsx` owns all settings page chrome via
`SettingsHeaderShell` — a fixed header bar (a left back chip + right-aligned
action chips), a scroll region, and a centered `max-w-[48rem]` content column led
by a **title + description from navigation metadata**. The chrome stays mounted
across section navigation (it never re-renders or re-lays-out). Each section
renders through the **`SettingsPanel`** registrar
across section navigation. Its routed title and description are available before
the section body resolves. Each section renders through the **`SettingsPanel`**
registrar
(`@/app/workspace/[workspaceId]/settings/components/settings-panel`), which feeds
the shell its header data and renders only the section body. Sections supply
**data**, never chrome.
Expand Down Expand Up @@ -82,6 +83,9 @@ return (
`children` instead and omit the prop.
- `title?` / `description?` — overrides for the nav-driven defaults. **Only** for a
detail sub-view that needs a different heading; normal pages never pass these.
A top-level page's header identity must remain stable while its data loads:
never replace navigation metadata with client-fetched copy after first paint.
Put data-dependent context in the page body instead.
- `scrollContainerRef?: React.Ref<HTMLDivElement>` — forwards a ref to the scroll
region (e.g. programmatic scroll-to-bottom).

Expand Down
5 changes: 5 additions & 0 deletions .claude/rules/sim-url-state.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,11 @@ import KnowledgeBaseLoading from '@/app/workspace/[workspaceId]/knowledge/[id]/l

Reference: `apps/sim/app/workspace/[workspaceId]/knowledge/[id]/page.tsx`.

The narrow exception is a continuity-focused peer switch that deliberately keeps the current
view mounted and follows the full-route plus critical-data intent-prefetch rule in
`sim-react-performance.md`. It still needs a real in-page Suspense fallback; it only omits the
route-level `loading.tsx` that would replace the current peer before the destination is ready.

This applies to **page entries**. An inner `<Suspense>` wrapping a `lazy()` component is the exception: there `fallback={null}` is correct, precisely so the suspend resolves at the nearest boundary instead of flashing the whole route — see `sim-imports.md`, "Code-splitting through barrels".

## Debounced text inputs
Expand Down
54 changes: 41 additions & 13 deletions .github/workflows/test-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,18 @@ jobs:
timeout-minutes: 15

steps:
# The diff-based audits below need a base commit to read, and the default
# depth of 1 clones a single commit with no parent. They normally fetch
# their base by SHA (see "Resolve base ref"), so this depth only covers the
# `HEAD~1` fallback — but without it that fallback resolves to nothing.
#
# Worth stating because the failure was invisible for so long: the migration
# audit read the resulting `git diff` failure as "no migrations changed" and
# exited 0, so it had never actually run on a push build.
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
fetch-depth: 2

- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
Expand Down Expand Up @@ -104,15 +114,40 @@ jobs:

echo "✅ All env flags are properly configured"

- name: Check block registry invariants
# One fetch for both base-ref audits, and no `|| true`: a swallowed fetch leaves
# the base ref absent, which neither audit can tell apart from a branch that
# changed nothing. The block-registry check at least degrades to a visible
# `⚠ … skipping` line; the migration audit printed `✓ No new migrations to
# check` and exited 0, clearing the only guard on production DDL.
#
# Depth stays at 1 — without a merge-base the migration audit diffs the two
# tips, which under `--diff-filter=AM` is exactly the migrations new here.
# Resolved once for both diff-based audits, and never with `|| true`: a
# swallowed fetch leaves the base absent, which neither audit can tell apart
# from a branch that changed nothing.
#
# On push the base is `github.event.before`, the tip the branch had before
# this push — not `HEAD~1`, which names only the last commit and would let a
# multi-commit push slip every earlier commit's migrations past the audit.
# It is fetched by SHA at depth 1; the audits diff two tips and need no
# common ancestry. An all-zero `before` means the branch is new and has no
# predecessor to diff, so `HEAD~1` remains the fallback there.
- name: Resolve base ref for diff-based audits
id: audit_base
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
BASE_REF="origin/${{ github.base_ref }}"
git fetch --depth=1 origin "${{ github.base_ref }}" 2>/dev/null || true
git fetch --depth=1 origin "${{ github.base_ref }}"
echo "ref=origin/${{ github.base_ref }}" >> "$GITHUB_OUTPUT"
elif [ -n "${{ github.event.before }}" ] &&
[ "${{ github.event.before }}" != "0000000000000000000000000000000000000000" ]; then
git fetch --depth=1 origin "${{ github.event.before }}"
echo "ref=${{ github.event.before }}" >> "$GITHUB_OUTPUT"
else
BASE_REF="HEAD~1"
echo "ref=HEAD~1" >> "$GITHUB_OUTPUT"
fi
bun run apps/sim/scripts/check-block-registry.ts "$BASE_REF"

- name: Check block registry invariants
run: bun run apps/sim/scripts/check-block-registry.ts "${{ steps.audit_base.outputs.ref }}"

- name: Lint code
run: bun run lint:check
Expand All @@ -127,14 +162,7 @@ jobs:
run: bun run docs-manifest:check

- name: Migration safety (zero-downtime) audit
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
BASE_REF="origin/${{ github.base_ref }}"
git fetch --depth=1 origin "${{ github.base_ref }}" 2>/dev/null || true
else
BASE_REF="HEAD~1"
fi
bun run check:migrations "$BASE_REF"
run: bun run check:migrations "${{ steps.audit_base.outputs.ref }}"

# Every workspace, not just realtime. packages/emcn, packages/utils,
# apps/desktop and apps/docs had no type check in CI at all; apps/sim's
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ npx sim-setup add sandbox
npx sim-setup add jobs
npx sim-setup add cache
npx sim-setup add knowledge
npx sim-setup add chat
npx sim-setup add llm
npx sim-setup add integration slack
```
Expand Down
2 changes: 2 additions & 0 deletions apps/docs/components/workflow-preview/docs-container-node.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ interface DocsContainerData {
name: string
blockType: string
size?: { width: number; height: number }
parentId?: string
}

/**
Expand All @@ -24,6 +25,7 @@ export const DocsContainerNode = memo(function DocsContainerNode({
name: data.name,
width: data.size?.width,
height: data.size?.height,
parentId: data.parentId,
isPreview: true,
}

Expand Down
63 changes: 63 additions & 0 deletions apps/docs/components/workflow-preview/workflow-data.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* @vitest-environment node
*/
import { BLOCK_Z_BASE, CONTAINER_CHILD_Z_BASE, getEdgeZIndex } from '@sim/workflow-renderer'
import { describe, expect, it } from 'vitest'
import { type PreviewBlock, type PreviewWorkflow, toReactFlowElements } from './workflow-data'

const block = (
overrides: Partial<PreviewBlock> & Pick<PreviewBlock, 'id' | 'type'>
): PreviewBlock => ({
name: overrides.id,
bgColor: '#000000',
rows: [],
position: { x: 0, y: 0 },
...overrides,
})

const workflow: PreviewWorkflow = {
id: 'nested-subflows',
name: 'Nested subflows',
blocks: [
block({ id: 'start', type: 'starter' }),
block({ id: 'loop', type: 'loop', size: { width: 500, height: 300 } }),
block({
id: 'parallel',
type: 'parallel',
parentId: 'loop',
position: { x: 24, y: 64 },
size: { width: 400, height: 200 },
}),
block({ id: 'agent', type: 'agent', parentId: 'loop', position: { x: 24, y: 140 } }),
],
edges: [
{ id: 'start-loop', source: 'start', target: 'loop' },
{ id: 'loop-parallel', source: 'loop', target: 'parallel' },
{ id: 'loop-agent', source: 'loop', target: 'agent' },
],
}

describe('toReactFlowElements layering', () => {
it('places incoming edges on their container target layer', () => {
const { nodes, edges } = toReactFlowElements(workflow, false, {
highlightEdge: 'loop-parallel',
})
const nodeById = new Map(nodes.map((node) => [node.id, node]))
const edgeById = new Map(edges.map((edge) => [edge.id, edge]))

expect(nodeById.get('loop')?.zIndex).toBe(0)
expect(nodeById.get('parallel')?.zIndex).toBe(1)
expect(edgeById.get('start-loop')?.zIndex).toBe(0)
expect(edgeById.get('loop-parallel')?.zIndex).toBe(1)
})

it('keeps ordinary cards above normally layered edges', () => {
const { nodes, edges } = toReactFlowElements(workflow)
const nodeById = new Map(nodes.map((node) => [node.id, node]))
const edgeById = new Map(edges.map((edge) => [edge.id, edge]))

expect(nodeById.get('start')?.zIndex).toBe(BLOCK_Z_BASE)
expect(nodeById.get('agent')?.zIndex).toBe(CONTAINER_CHILD_Z_BASE)
expect(edgeById.get('loop-agent')?.zIndex).toBe(getEdgeZIndex(0))
})
})
37 changes: 36 additions & 1 deletion apps/docs/components/workflow-preview/workflow-data.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
import {
BLOCK_Z_BASE,
CONTAINER_CHILD_Z_BASE,
getEdgeZIndex,
getEdgeZIndexForTarget,
} from '@sim/workflow-renderer'
import { type Edge, type Node, Position } from 'reactflow'

/**
Expand Down Expand Up @@ -61,6 +67,24 @@ export interface HighlightOptions {
selectedBlock?: string
}

/** Semantic container depth used for z-order while docs positions stay flattened. */
function getNestingDepth(block: PreviewBlock, blocksById: Map<string, PreviewBlock>): number {
let depth = 0
let parentId = block.parentId
const visited = new Set<string>()

while (parentId && !visited.has(parentId)) {
const parent = blocksById.get(parentId)
if (!parent) break

visited.add(parentId)
depth += 1
parentId = parent.parentId
}

return depth
}

/**
* Converts a {@link PreviewWorkflow} to React Flow nodes and edges.
*
Expand All @@ -81,6 +105,7 @@ export function toReactFlowElements(

const nodes: Node[] = workflow.blocks.map((block, index) => {
const isContainer = Boolean(block.size)
const nestingDepth = getNestingDepth(block, blocksById)
// Nested blocks are authored relative to their container; render them at
// absolute coordinates (not React Flow parentNode children) so the edges
// between a container and its nested blocks render reliably and on top.
Expand All @@ -92,7 +117,7 @@ export function toReactFlowElements(
id: block.id,
type: isContainer ? 'previewContainer' : 'previewBlock',
position,
zIndex: isContainer ? 0 : 1,
zIndex: isContainer ? nestingDepth : block.parentId ? CONTAINER_CHILD_Z_BASE : BLOCK_Z_BASE,
...(block.size ? { style: { width: block.size.width, height: block.size.height } } : {}),
data: {
name: block.name,
Expand All @@ -103,6 +128,7 @@ export function toReactFlowElements(
tools: block.tools,
hideTargetHandle: block.hideTargetHandle,
size: block.size,
parentId: block.parentId,
index,
animate,
isHighlighted: highlightBlock === block.id || selectedBlock === block.id,
Expand All @@ -127,6 +153,14 @@ export function toReactFlowElements(
// so edges into and out of Loop/Parallel containers still connect.
const sourceBlock = blocksById.get(e.source)
const targetBlock = blocksById.get(e.target)
const parentContainer = blocksById.get(sourceBlock?.parentId ?? targetBlock?.parentId ?? '')
const baseZIndex = getEdgeZIndex(
parentContainer ? getNestingDepth(parentContainer, blocksById) : undefined,
{ isHighlighted: isEdgeHighlight }
)
const targetContainerZIndex = targetBlock?.size
? getNestingDepth(targetBlock, blocksById)
: undefined
const sourceHandle =
e.sourceHandle ?? (sourceBlock?.size ? `${sourceBlock.type}-end-source` : 'source')
const targetHandle = targetBlock?.size ? undefined : 'target'
Expand All @@ -142,6 +176,7 @@ export function toReactFlowElements(
},
sourceHandle,
targetHandle,
zIndex: getEdgeZIndexForTarget(baseZIndex, targetContainerZIndex),
data: {
animate,
delay: animate ? sourceIndex * BLOCK_STAGGER + BLOCK_STAGGER : 0,
Expand Down
15 changes: 15 additions & 0 deletions apps/docs/content/docs/en/platform/credentials.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -119,12 +119,27 @@ Click **Details** on any secret row to open its detail view.
From here you can:

- View the **Key** and edit the **Value**
- Toggle **Visibility** — show the value unmasked in run output; see [Visibility](#visibility)
- Edit the **Description** — an optional note telling teammates what the secret is for. Workspace secrets only; a personal secret is not shared, so it has none
- Manage **Members** — invite teammates by email and assign them an **Admin** or **Member** role
- Open **See usage** — where this secret has actually been used

Click **Save** to apply changes, or **Back** to return to the list.

### Visibility

By default, a secret's resolved value is masked everywhere Sim shows run output (see [Execution log protection](#execution-log-protection)). For values that aren't actually sensitive — a staging key, a shared base URL — that masking makes your own logs harder to read.

**Show value in logs and Chat** turns masking off for one workspace secret. With it on:

- Run logs, Chat, and code output show the real value instead of `{{KEY}}`
- Files a run writes with the value in them stay readable and attachable
- The Secrets API list includes the value for this secret, so external agents can read it directly instead of scraping logs

The value becomes visible to **anyone who can see this workspace's runs** — including publicly shared log links and log exports, and regardless of member restrictions on the secret itself. Only turn it on for values you'd be comfortable printing in a log.

The switch applies to future runs only. Logs written while the secret was masked stay masked, and anything written while it was visible keeps the value even if you turn masking back on. If another secret holds the same value, that value stays masked — masking always wins a conflict. Workspace secrets only; the same people who can edit the description can flip it.

### See usage

**See usage** lists the runs that resolved this secret: when it was last used, what used it (a workflow, the Sim agent, or an MCP server), how it was triggered, who it resolved under, and a link to the most recent run in Logs. Rows are grouped by day, so a workflow on a schedule reads as one row per day rather than thousands.
Expand Down
20 changes: 20 additions & 0 deletions apps/docs/content/docs/en/platform/self-hosting/docker.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,26 @@ OLLAMA_URL=http://192.168.1.100:11434 docker compose -f docker-compose.prod.yml
Inside Docker, `localhost` refers to the container, not your host. Use `host.docker.internal` or your host's IP.
</Callout>

### LM Studio

[LM Studio exposes an OpenAI-compatible API](https://lmstudio.ai/docs/developer/openai-compat). Start its local server, load a model, and enable **Serve on Local Network** so the Docker container can reach it. [Enable API authentication](https://lmstudio.ai/docs/developer/core/authentication), then set the endpoint and token in the `.env` file next to your Compose file:

```bash
# macOS/Windows
VLLM_BASE_URL=http://host.docker.internal:1234

# Linux - use your host IP instead
# VLLM_BASE_URL=http://192.168.1.100:1234

VLLM_API_KEY=your_lm_studio_api_token
```

Both the server root shown above and a URL ending in `/v1` are accepted. After recreating the `simstudio` service, its models appear in the model picker with a `vllm/` prefix; Sim removes that prefix before sending the model identifier to LM Studio.

```bash
docker compose -f docker-compose.ollama.yml up -d --force-recreate simstudio
```

## Commands

```bash
Expand Down
Loading
Loading