Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
33f9d64
fix(careers): remove /careers redirect so the in-app page is reachabl…
waleedlatif1 Jul 1, 2026
0575875
fix(custom-tool): restore modal body scroll so Save stays reachable (…
waleedlatif1 Jul 1, 2026
b8e88b1
improvement(settings): react health pass across settings surface (#5324)
waleedlatif1 Jul 1, 2026
af87de0
fix(connectors): allow self-hosted private DB hosts via opt-in flag (…
waleedlatif1 Jul 1, 2026
78661d2
fix(providers): drop deprecated temperature capability from claude-so…
waleedlatif1 Jul 1, 2026
c957aa2
improvement(settings): react-doctor perf & correctness pass (#5327)
waleedlatif1 Jul 1, 2026
42e3a03
improvement(landing): react-doctor health pass across the landing sur…
waleedlatif1 Jul 1, 2026
8cebeb6
improvement(chat): code-split resource preview panel out of initial /…
waleedlatif1 Jul 1, 2026
6715354
improvement(tables): react-doctor safe performance pass (#5329)
waleedlatif1 Jul 1, 2026
76537c8
improvement(files): react-doctor performance pass on the files module…
waleedlatif1 Jul 1, 2026
9204b4a
improvement(knowledge): react-doctor perf pass across the knowledge b…
waleedlatif1 Jul 1, 2026
c923de6
feat(providers): add Claude Fable 5 model (#5334)
waleedlatif1 Jul 1, 2026
2c5ee4a
chore(rules): canonical useState prev-tracker for render-phase adjust…
waleedlatif1 Jul 1, 2026
eb367de
fix(demo): unblock Cal.com booking embed and align work-email validat…
waleedlatif1 Jul 1, 2026
4421512
improvement(tables): make max row size env-configurable and raise def…
TheodoreSpeaks Jul 1, 2026
62acc9c
fix(interfaces): always-light public surfaces (chat, resume, file sha…
TheodoreSpeaks Jul 1, 2026
4e8b88f
fix(landing): avoid unpolyfilled ES2023 array methods in client code …
waleedlatif1 Jul 1, 2026
507cee1
fix(integrations): repair corrupt icons, backfill missing block metas…
waleedlatif1 Jul 1, 2026
bca7f2c
feat(logs): add Troubleshoot in Chat button for errored runs (#5341)
waleedlatif1 Jul 2, 2026
4df352f
fix(integrations): resolve Server Components crash on the integration…
waleedlatif1 Jul 2, 2026
577b402
fix(chat): scope troubleshoot handoff to its workspace (#5344)
waleedlatif1 Jul 2, 2026
26fb687
fix(chat): fix secret-input chat widget reshaping after submit (#5346)
waleedlatif1 Jul 2, 2026
8d6cec1
feat(blog): add five blogs (#5268)
waleedlatif1 Jul 2, 2026
7457184
fix(knowledge): surface KB description validation errors and raise li…
TheodoreSpeaks Jul 2, 2026
69b81a6
feat(data-retention): granular PII redaction stages (input + block ou…
TheodoreSpeaks Jul 2, 2026
dcc7c0c
ci(dev): auto-deploy Trigger.dev tasks + fail dev db:push on TTY prom…
TheodoreSpeaks Jul 2, 2026
e01123f
feat(blog): add CTA band above footer (#5350)
waleedlatif1 Jul 2, 2026
b346088
fix(logs): right-size and reorganize log-detail action chips (#5352)
waleedlatif1 Jul 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .claude/rules/sim-components.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,22 @@ Component authoring rules — structure order (refs → external hooks → store
- `'use client'` only when using React hooks or browser-only APIs.
- Prefer semantic HTML (`aside`, `nav`, `article`).
- Optional-chain callbacks: `onAction?.(id)`.

## List-render performance

When rendering or sorting a list of rows against a lookup collection (members, folders, tags), keep the per-row work O(1):

- **Precompute a lookup `Map` once**, never `array.find(...)` per row. Build `const byId = useMemo(() => { const m = new Map<string, T>(); for (const x of items ?? []) m.set(x.id, x); return m }, [items])` and read `byId.get(id)` in the sort comparator, `.map(...)`, and cell builders. A `.find` inside a sort comparator is O(n²·log n) — the worst offender. Depend memos on the derived `Map`, not the raw array.
- **Sort with `[...array].sort(cmp)` in client code — NOT `array.toSorted(cmp)`.** SWC (Next's compiler) transforms syntax but does not polyfill prototype methods, and the repo sets no core-js/browserslist target, so `toSorted`/`toReversed`/`toSpliced`/`Array.prototype.with` ship as-is and throw `TypeError` on Safari <16 / iOS 15 (they landed in Safari 16). `tsconfig` `lib: ES2023` only affects type-checking, never the runtime. The `[...arr].sort()` spread copy is the intended cost — it keeps the sort non-mutating without an unpolyfilled builtin. These methods are only safe in server-only modules (no `'use client'`, executed on Node ≥20). react-doctor's `js-tosorted-immutable` is therefore a won't-fix in client components.
- **Partition in a single pass** — when splitting one collection into several (`fileIds`/`folderIds`), do one `for…of` pushing into each bucket and return `{ a, b }` from a single `useMemo`, not two memos that each `map→filter→map` the same source twice.

## react-doctor (`npx react-doctor`) — apply the wins, skip the false positives

react-doctor diagnostics are hypotheses, not verdicts — confirm against the code before acting, and preserve behavior. Known repo-specific false positives to NOT "fix":

- `no-barrel-import` — barrel imports are the repo convention (see sim-imports.md, "Barrel Exports"). Keep them.
- `js-tosorted-immutable` — in `'use client'` code, keep `[...arr].sort(cmp)`; `toSorted` is unpolyfilled and crashes Safari <16 / iOS 15 (see "List-render performance" above). Only apply it in server-only modules.
- `rerender-state-only-in-handlers` / "state set but never rendered" — a false positive when the `useState` is consumed by a `useEffect`/`useLayoutEffect` dependency (the effect must re-run on change). Only convert to a ref when nothing reads the value reactively.
- `no-render-in-render` — a helper *called inline* (`{renderRow()}`) is reconciled by position and does **not** remount, so extracting it to a component is usually pure churn and can regress behavior (prop-drilling many closures, focus/scroll loss on the inner `<input>`). Apply it only when the helper is genuinely a *component defined during render*, or when the move is mechanical (a stateless, ref-free helper whose closures become a small, explicit prop set).
- `async-await-in-loop` on an upload/progress loop where sequential execution is intentional (per-item progress, server backpressure) — leave it.
- Broad refactors (`prefer-useReducer` for many `useState`, `no-giant-component` splits) — out of scope for a perf pass; note, don't churn.
16 changes: 12 additions & 4 deletions .claude/rules/sim-hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,16 +51,24 @@ export function useFeature({ id, onSelect }: UseFeatureProps) {

## State shape

Never mirror a prop into state with `useState(prop)` + a syncing `useEffect` — a prop change clobbers in-progress local edits. Use the prop directly, reset via a remount `key`, or — when you must seed local state from a prop only on a transition (e.g. a modal opening) — reset during render with the `prevX` ref idiom:
Never mirror a prop into state with `useState(prop)` + a syncing `useEffect` — a prop change clobbers in-progress local edits. Use the prop directly, reset via a remount `key`, or — when you must seed local state from a prop only on a transition (e.g. a modal opening) — adjust it **during render** with a `prev`-value tracker held in `useState`:

```typescript
const prevOpenRef = useRef(open)
if (prevOpenRef.current !== open) {
prevOpenRef.current = open
const [prevOpen, setPrevOpen] = useState(open)
if (prevOpen !== open) {
setPrevOpen(open)
if (open) setName(initialName) // closed → open only
}
```

React re-renders immediately with the corrected state without committing the stale value. Rules: the `if (prev !== current)` guard is mandatory (an unconditional `setState` in render loops forever), the tracker is set **inside** the guard, and you may only set the currently-rendering component's state this way. Hold the tracker in `useState`, **not a `useRef`** — React forbids reading/writing `ref.current` during render (react.dev, useRef → "Do not write _or read_ `ref.current` during rendering"; the `react-hooks` `refs` lint flags it), and a `useState` tracker is concurrent-safe where a mutated ref is not (a discarded render rolls state back, not a ref).

**The tracker's initial value decides mount behavior — choose it deliberately.** The example seeds `useState(open)` because the modal mounts closed, so the first render's guard is `false` and nothing resets on mount (correct — `name` is already at its initial value). When the effect you're replacing did real work **on mount** — opening a panel because a prop already matches, seeding editable state from an already-present value, or a component that can mount in the active state — seed a **sentinel** the live value can't equal (e.g. `useState<T | null>(null)`), otherwise the guard is `false` on the first render and that mount action is silently dropped. Place the block before any early `return`.

> Some existing components use a `useRef` prev-tracker (`if (prevRef.current !== x) { prevRef.current = x; … }`). It works but reads/writes a ref during render — prefer the `useState` form above for new code.

Only convert a `useState` to a `useRef` when the value is **never read during render/JSX and is never a hook dependency** — a value in a `useEffect`/`useMemo`/`useCallback` dep array must re-run the hook on change, so it stays state (see also `rerender-state-only-in-handlers` in `sim-components.md`). Convert only set-only values read solely inside handlers or effect bodies (e.g. a prompt-history index, a pending-upload URL). If a ref feeds render, mutating it won't re-render and the UI goes stale.

Model mutually-exclusive flags as ONE `status` enum, not several contradictory booleans. `isLoading`/`isVerified`/`isInvalidOtp` describing one machine collapse to `status: 'idle' | 'verifying' | 'verified' | 'error'` (+ `errorMessage`); derive any boolean a consumer still needs (`status === 'error'`).

Derive busy/success from the mutation object — never duplicate `mutation.isPending`/`mutation.isSuccess` into local `useState`. Read them directly (`mutation.isSuccess`) and reset with `mutation.reset()`. A distinct phase the mutation doesn't cover — e.g. a pre-submit captcha/Turnstile gate that runs before `mutate()` — is not a duplicate; keep that flag.
14 changes: 14 additions & 0 deletions .claude/rules/sim-imports.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,20 @@ import { Dashboard, Sidebar } from '@/app/workspace/[workspaceId]/logs/component
import { Dashboard } from '@/app/workspace/[workspaceId]/logs/components/dashboard/dashboard'
```

## Code-splitting through barrels

When you `lazy(() => import(...))` a component to keep it out of a route's initial bundle, import the **deep module path** (`./components/foo/foo`), never the barrel — and **delete the now-dead barrel re-export** of that component. This app has no `"sideEffects": false` in `apps/sim/package.json`, so when any sibling still imports that barrel, webpack can conservatively keep the barrel's re-export edge to the heavy module. A leftover `export { Foo } from './foo'` line can therefore drag `Foo` (and its transitive deps) back into the initial chunk and silently defeat the split. Removing the dead re-export is the guaranteed fix; verify with a production bundle diff, not by eyeballing the `lazy()` call.

```typescript
// ✓ Good — deep lazy import + no barrel edge left behind
const MothershipView = lazy(() =>
import('./components/mothership-view/mothership-view').then((m) => ({ default: m.MothershipView }))
)
// (and remove `export { MothershipView } from './mothership-view'` from components/index.ts)
```

Wrap the lazy component in a **local `<Suspense>`** so its suspension resolves at the nearest boundary instead of bubbling to the page-level fallback (which would flash the whole route). `React.lazy(memo(forwardRef(...)))` forwards a DOM `ref` correctly in React 19 — but during the fallback window `ref.current` is `null`, so every consumer must guard it (`if (!el) return` / `el?.`).

## No Re-exports

Do not re-export from non-barrel files. Import directly from the source.
Expand Down
95 changes: 95 additions & 0 deletions .claude/rules/sim-react-performance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# React & Render Performance

Behavior-preserving performance idioms for components, hooks, and hot render paths. These are safe defaults — apply them freely. For the render-causing *effect/state* anti-patterns (derived state in effects, effect chains, state synced to a prop), use the dedicated skills: `/you-might-not-need-an-effect`, `/you-might-not-need-state`, `/you-might-not-need-a-memo`, `/you-might-not-need-a-callback`. Those refactors change render timing — verify them against the running UI, never mass-apply blind.

## Lazy-init refs that hold objects

`useRef(new Map())` / `useRef(new Set())` / `useRef({...})` allocates a fresh object on **every render** and throws it away — only the first is ever kept. Lazy-init instead so the allocation happens once.

```typescript
// ✗ Bad — allocates a new Map each render, discards all but the first
const cacheRef = useRef<Map<string, string>>(new Map())

// ✓ Good — allocated once, stable identity thereafter
const cacheRef = useRef<Map<string, string> | null>(null)
cacheRef.current ??= new Map()
```

Read `cacheRef.current` directly inside effects/handlers — refs are stable and never belong in a dependency array. A cheap primitive (`useRef(0)`, `useRef('')`, `useRef(null)`) needs no lazy init.

## Hoist static values and closure-free functions to module scope

A value or function declared inside a component is rebuilt every render. If it captures **nothing** from component scope (no props/state/refs), move it above the component at module scope. This skips the per-render allocation and keeps a stable identity so memoized children don't re-render.

```typescript
// ✗ Bad — rebuilt every render, new identity each time
function Toolbar({ mode }: ToolbarProps) {
const TITLES = { create: 'Add', edit: 'Configure' } as const
const handleWheel = (e: React.WheelEvent) => e.currentTarget.scrollBy(e.deltaX, e.deltaY)
// ...
}

// ✓ Good — allocated once at module load
const TITLES = { create: 'Add', edit: 'Configure' } as const
function handleWheel(e: React.WheelEvent) {
e.currentTarget.scrollBy(e.deltaX, e.deltaY)
}
function Toolbar({ mode }: ToolbarProps) { /* ... */ }
```

A closure-free function that IS wired through a ref sink or intentionally kept for stable identity may stay inline — hoisting a one-line `preventDefault` handler is churn, not a win. Hoist when it removes a real per-render allocation or unblocks child memoization.

## Pre-index with Map/Set for repeated lookups

`array.find()` / `array.includes()` / `array.indexOf()` scan the whole list each call. Inside a loop or a hot render path over a non-trivial list, that is O(n·m). Build a `Map` (for lookup-by-key) or `Set` (for membership) **once before** the loop, then look up in O(1).

```typescript
// ✗ Bad — find() re-scans outputs for every column
for (const child of columns) {
const output = group.outputs.find((o) => o.columnName === getColumnId(child))
}

// ✓ Good — index once, then O(1) lookups
const outputByName = new Map<string, Output>()
for (const o of group.outputs) {
if (!outputByName.has(o.columnName)) outputByName.set(o.columnName, o) // first wins, matches find()
}
for (const child of columns) {
const output = outputByName.get(getColumnId(child))
}
```

Preserve `.find()`'s **first-match** semantics when duplicate keys are possible: `new Map(arr.map(...))` keeps the *last* entry, so guard with `if (!map.has(key))` when replacing a `.find()`. Skip this for tiny, cold arrays (a handful of items in an event handler) where the Map build costs more than it saves.

## Never mutate a shared array in place

The real bug to avoid is `array.sort()` / `array.reverse()` on an array you don't own — sorting a React Query cache array in place corrupts shared state. Always sort a copy:

```typescript
// ✗ Bad — mutates the (possibly shared) source array in place
return items.sort(compare)

// ✓ Good — sorts a throwaway copy, source untouched
return [...items].sort(compare)
```

**Do NOT reach for `toSorted()` / `toReversed()` / `with()` / `toSpliced()` on client render paths.** They are ES2023 *runtime* methods — and a tsconfig `"lib": ["ES2023"]` only makes them **type-check**, it does not make them **run**. Next/SWC compiles syntax but does **not** polyfill prototype methods, and the default browserslist still includes browsers without them (`toSorted` landed in Safari 16 / iOS 16, so any device capped at iOS 15 throws `TypeError: x.toSorted is not a function` and crashes the page). The perf difference vs `[...arr].sort()` is negligible (both allocate one array), so the copy-then-sort form is the correct default everywhere client code runs. Only consider the immutable methods in Node-only code (server routes, scripts) on Node ≥20, where the runtime is known.

## Run independent awaits in parallel

Sequential `await`s that don't consume each other's result serialize latency for nothing — in an async Server Component or a route handler this directly delays the response. Kick them off together with `Promise.all` and destructure.

```typescript
// ✗ Bad — waits for params, then separately waits for searchParams
const { id } = await params
const { kbName } = await searchParams

// ✓ Good — one combined wait
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).

## 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.
47 changes: 45 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ jobs:
ecr_repo_secret: ECR_PII
steps:
- name: Checkout code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6

- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6
Expand Down Expand Up @@ -130,6 +130,49 @@ jobs:
provenance: false
sbom: false

# Dev: deploy Trigger.dev background tasks to the preview "dev-sim" branch.
# Gated after migrate-dev for the same reason as build-dev — the new task
# code runs against the dev DB, so the schema must be pushed first.
deploy-trigger-dev:
name: Deploy Trigger.dev (Dev)
needs: [migrate-dev]
if: github.event_name == 'push' && github.ref == 'refs/heads/dev'
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6

- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: 1.3.13

- name: Cache Bun dependencies
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: |
~/.bun/install/cache
node_modules
**/node_modules
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
restore-keys: |
${{ runner.os }}-bun-

- name: Install dependencies
run: bun install --frozen-lockfile

- name: Deploy to Trigger.dev
working-directory: ./apps/sim
env:
TRIGGER_ACCESS_TOKEN: ${{ secrets.DEV_TRIGGER_ACCESS_TOKEN }}
TRIGGER_PROJECT_ID: ${{ secrets.TRIGGER_PROJECT_ID }}
run: |
if [ -z "$TRIGGER_ACCESS_TOKEN" ] || [ -z "$TRIGGER_PROJECT_ID" ]; then
echo "ERROR: DEV_TRIGGER_ACCESS_TOKEN and TRIGGER_PROJECT_ID repo secrets must both be set" >&2
exit 1
fi
bunx trigger.dev@4.4.3 deploy --env preview --branch dev-sim

# Main/staging: build AMD64 images and push to ECR + GHCR
build-amd64:
name: Build AMD64
Expand Down Expand Up @@ -359,7 +402,7 @@ jobs:
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
fetch-depth: 2 # Need at least 2 commits to detect changes
fetch-depth: 2 # Need at least 2 commits to detect changes
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4
id: filter
with:
Expand Down
11 changes: 10 additions & 1 deletion .github/workflows/migrations.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,16 @@ jobs:

if [ "${ENVIRONMENT}" = "dev" ]; then
echo "Dev environment — pushing schema directly (db:push)"
bun run db:push --force
# drizzle-kit push needs a TTY to resolve ambiguous renames (--force only
# covers data-loss). In CI it throws "Interactive prompts require a TTY
# terminal" but still exits 0, so the job goes green without applying the
# change. tee keeps the output live in the log; we then fail on drizzle's
# own TTY error. A genuine non-zero exit already fails via `set -e`.
bun run db:push --force < /dev/null 2>&1 | tee /tmp/db-push.log
if grep -q "Interactive prompts require a TTY terminal" /tmp/db-push.log; then
echo "ERROR: db:push needs an interactive rename decision; land it as a versioned migration instead of relying on push." >&2
exit 1
fi
else
echo "Applying versioned migrations (db:migrate)"
bun run ./scripts/migrate.ts
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/test-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,9 @@ jobs:
- name: Bare-icon theme-safety audit
run: bun run check:bare-icons

- name: Icon SVG path validity audit
run: bun run check:icon-paths

- name: Verify realtime prune graph
run: bun run check:realtime-prune

Expand Down
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ export function Component({ requiredProp, optionalProp = false }: ComponentProps

Extract when: 50+ lines, used in 2+ files, or has own state/logic. Keep inline when: < 10 lines, single use, purely presentational.

Behavior-preserving render-performance idioms — lazy-init object refs, hoist closure-free values/functions to module scope, pre-index repeated lookups with `Map`/`Set`, and never mutating a shared array in place — are in `.claude/rules/sim-react-performance.md` (which also explains why `toSorted`/`toReversed` are unsafe on client render paths despite the ES2023 tsconfig lib — SWC does not polyfill prototype methods, so use `[...arr].sort()`). For the render-timing effect/state anti-patterns use the `/you-might-not-need-*` skills and verify against the running UI.

## API Contracts

Boundary HTTP request and response shapes for all routes under `apps/sim/app/api/**` live in `apps/sim/lib/api/contracts/**` (one file per resource family — `folders.ts`, `chats.ts`, `knowledge.ts`, etc.). Routes never define route-local boundary Zod schemas, and clients never define ad-hoc wire types — both sides consume the same contract.
Expand Down
Loading
Loading