Skip to content

Commit baba1d4

Browse files
Merge remote-tracking branch 'origin/staging' into feat/openrouter-kb-fallback
# Conflicts: # apps/sim/tools/generated/tool-ids.ts # apps/sim/tools/generated/tool-metadata.ts # apps/sim/tools/generated/tool-outputs.ts
2 parents 05535d9 + bc8826a commit baba1d4

542 files changed

Lines changed: 65900 additions & 4428 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/rules/sim-list-ordering.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
---
2+
paths:
3+
- "apps/sim/app/**/*.tsx"
4+
- "apps/sim/ee/**/*.tsx"
5+
- "apps/sim/components/**/*.tsx"
6+
---
7+
8+
# List & Menu Ordering
9+
10+
**A list orders itself the way the user already reads the same things somewhere else.** Dropdowns, context menus, tab strips, command palettes, and settings navs are all *second* presentations of a set the user has already seen — in the sidebar, in a toolbar, in a column-header row. When the second presentation reorders that set, the user re-reads it from scratch every time.
11+
12+
This is not a style preference. Order is the cheapest affordance a list has, and the only one that costs nothing to get right.
13+
14+
## The rule
15+
16+
Before writing a list of items, find where the user sees those same items *first*. That surface owns the order; your list mirrors it.
17+
18+
| The list | Mirrors |
19+
| --- | --- |
20+
| Resource menus (`+` attach, `@` mention, resource-tab `+`) | the workspace **sidebar**, top-down |
21+
| A row / root **context menu** | that surface's **toolbar**, left-to-right → top-to-bottom |
22+
| Settings tab strip, recently-deleted tabs | the **settings nav**, top-down |
23+
| A "New …" menu | the order those things appear once created |
24+
25+
Left-to-right becomes top-to-bottom. A toolbar reading `Filter · Sort · Export · Delete` becomes a menu reading Filter, Sort, Export, Delete — never alphabetized, never grouped by implementation, never "destructive last" unless the toolbar already puts it last.
26+
27+
Platform-only entries (desktop **Browser** and **Terminal**) trail the shared set rather than interleaving, so the common prefix is identical on every platform.
28+
29+
## Encode the order once
30+
31+
An order duplicated across surfaces is an order that will drift. Export **one** constant and sort by it — do not hand-maintain a matching literal per menu.
32+
33+
```ts
34+
/** Top-down order for every menu listing resource families, mirroring the sidebar. */
35+
export const RESOURCE_MENU_ORDER: readonly MothershipResourceType[] = [
36+
'integration', 'task', 'table', 'file', 'filefolder',
37+
'knowledgebase', 'log', 'workflow', 'folder', 'browser', 'terminal', 'generic',
38+
]
39+
40+
export function byResourceMenuOrder<T extends { type: MothershipResourceType }>(a: T, b: T) {
41+
return RESOURCE_MENU_ORDER.indexOf(a.type) - RESOURCE_MENU_ORDER.indexOf(b.type)
42+
}
43+
```
44+
45+
Canonical instance: `app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry/resource-registry.tsx`, consumed by `useAvailableResources` and `ResourceMenuSections`.
46+
47+
## Render kinds in one pass, not one phase per kind
48+
49+
The most common way a canonical order gets silently defeated: emitting all items of one *kind* and then all of another. Every submenu-backed family lands above every flat family regardless of what the order constant says.
50+
51+
```tsx
52+
// ✗ Bad — two phases; the trees always pin to the top
53+
<ResourceTreeSections sections={treeSections} />
54+
{groups.filter((g) => !FOLDERED.has(g.type)).map(renderFlat)}
55+
56+
// ✓ Good — one ordered pass; each entry picks its own rendering
57+
{entries.sort(byResourceMenuOrder).map((entry) =>
58+
sectionByType.has(entry.type) ? renderTree(entry) : renderFlat(entry)
59+
)}
60+
```
61+
62+
The same trap appears as "render the pinned ones, then the rest", "render enabled, then disabled", and "render the groups, then the loose items".
63+
64+
## When order may diverge
65+
66+
Only for reasons the user can perceive:
67+
68+
- **Search/filter results** rank by match quality — the whole point is that ranking beats position.
69+
- **User-controlled ordering** (drag-to-reorder, manual `sortOrder`) wins over any canonical order.
70+
- **Recency lists** ("Recent chats") order by time, which *is* the order the user reads them elsewhere.
71+
72+
"Grouped by which hook provides it", "alphabetical because it was easy", and "that's the order the array was built in" are not reasons.
73+
74+
## Reviewing
75+
76+
When a diff adds or edits a list of items, ask: where does the user see this set already, and does this match? If the answer is a different file with a different order, the diff needs a shared constant, not a second literal.

.claude/rules/sim-url-state.md

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ Put state in the URL **only** when it is *all* of: shareable, deep-linkable, boo
3434
## Anti-patterns (forbidden)
3535

3636
- Direct `useSearchParams().get(...)` or `new URLSearchParams(window.location.search)` to **read** state.
37-
- Hand-built query strings + `router.replace`/`router.push` to **mutate** state.
37+
- Hand-built query strings + `router.replace`/`router.push` to **mutate** state. **If the target path equals the current path, it is a query mutation, not a navigation** — even when written as a full path template. Re-serializing the path by hand is lossy by construction: it drops every param the template forgets. Use the nuqs setter (`setParams({ key: null }, { history: 'replace', scroll: false })`) — `null` always removes the key, and only the params you name are touched. Both options are already nuqs defaults (see "Conventions"); write them explicitly because a group whose shared options set `history: 'push'` (e.g. `filesUrlKeys`) would otherwise push a back-stack entry for a strip.
3838
- `window.history.replaceState`/`pushState` to mutate a param.
3939
- Duplicating URL state into a store and syncing it with effects / `popstate` listeners.
4040
- High-frequency or large state in the URL (cursor, pan/zoom, un-debounced keystrokes, big JSON blobs).
@@ -44,7 +44,7 @@ These reads/mutations are **not** anti-patterns and stay as-is:
4444

4545
- **Outbound URL builders**`new URLSearchParams({...})` to construct a `href`, a download endpoint, an external WebSocket/API URL, or a `window.open(_, '_blank')` destination.
4646
- **Route navigations**`router.push('/path/[id]?folderId=x')` that changes the route *path*, not just the current query. A nuqs setter only mutates the query on the current path; cross-path navigation stays on `router`.
47-
- **Read-once auth / redirect signals**`token`, `callbackUrl`, `redirect`, `error`, `invite_flow`, `upgraded`, `redirect_workflow`, etc. These are navigation signals consumed once (often read-then-strip), not synced view-state. Leave them on `useSearchParams`.
47+
- **Read-once auth / redirect signals**`token`, `callbackUrl`, `redirect`, `error`, `invite_flow`, `new` (invite signup flow), `upgraded`, `redirect_workflow`, etc. These are navigation signals consumed once (often read-then-strip), not synced view-state. Leave them on `useSearchParams`. Key names are per-surface: files' `new` is a genuine nuqs param (`files/search-params.ts`), while invite's `new` is a one-shot signup signal.
4848

4949
## Per-feature `search-params.ts` — single source of truth
5050

@@ -128,7 +128,22 @@ If a client param must be re-read server-side after a change, set `shallow: fals
128128

129129
## Suspense boundary
130130

131-
`useQueryState`/`useQueryStates` read `useSearchParams` internally, so any client component using them must sit under a `<Suspense>` boundary (Next.js requirement). Wrap the page entry with a real-chrome fallback so a suspend never flashes a blank frame — see `apps/sim/app/workspace/[workspaceId]/files/page.tsx`.
131+
`useQueryState`/`useQueryStates` read `useSearchParams` internally, so any client component using them must sit under a `<Suspense>` boundary (Next.js requirement). Wrap the page entry with a real-chrome fallback so a suspend never flashes a blank frame.
132+
133+
**Never `fallback={null}` on a page entry.** The route's co-located `loading.tsx` default export *is* the correct fallback — one skeleton serves both the route-level navigation transition (which Next renders automatically) and the in-page suspend (which this boundary renders). If the segment has no `loading.tsx`, add one; the route transition needs it anyway. Import it absolutely (`sim-imports.md`):
134+
135+
```typescript
136+
import { KnowledgeBase } from '@/app/workspace/[workspaceId]/knowledge/[id]/base'
137+
import KnowledgeBaseLoading from '@/app/workspace/[workspaceId]/knowledge/[id]/loading'
138+
139+
<Suspense fallback={<KnowledgeBaseLoading />}>
140+
<KnowledgeBase id={id} knowledgeBaseName={kbName || 'Knowledge Base'} />
141+
</Suspense>
142+
```
143+
144+
Reference: `apps/sim/app/workspace/[workspaceId]/knowledge/[id]/page.tsx`.
145+
146+
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".
132147

133148
## Debounced text inputs
134149

.devcontainer/docker-compose.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ services:
1919
- BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET:-your_auth_secret_here}
2020
- ENCRYPTION_KEY=${ENCRYPTION_KEY:-your_encryption_key_here}
2121
- COPILOT_API_KEY=${COPILOT_API_KEY}
22+
- MSHIP_SYSPROMPT_OVERRIDE=${MSHIP_SYSPROMPT_OVERRIDE:-}
2223
- NEXT_PUBLIC_CHAT_DISABLED=${NEXT_PUBLIC_CHAT_DISABLED:-}
2324
- SIM_AGENT_API_URL=${SIM_AGENT_API_URL}
2425
- OLLAMA_URL=${OLLAMA_URL:-http://localhost:11434}

.github/workflows/codeql.yml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,12 @@ on:
4141
# Safety net behind the push trigger, and the thing that keeps the
4242
# default-branch alert view fresh when main is quiet. Only fires once this
4343
# file is on the default branch — schedule events ignore other branches.
44-
- cron: '17 8 * * *'
44+
- cron: '17 8 * * 1'
4545
workflow_dispatch:
4646

47-
# Scheduled main scans must run to completion — only PR pushes supersede.
4847
concurrency:
4948
group: codeql-${{ github.ref }}
50-
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
49+
cancel-in-progress: true
5150

5251
permissions:
5352
contents: read

CLAUDE.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,12 @@ Shareable *client* view-state (active tab/panel, filters, search query, paginati
378378

379379
Co-locate a `search-params.ts` per feature exporting the parser map (single source of truth, shared by client `useQueryStates`/`useQueryState` and server `createSearchParamsCache`). Never `import { z }` in client code for params — use nuqs parsers. Full decision framework, conventions, the debounced-input pattern, and the workflow-editor carve-out are in `.claude/rules/sim-url-state.md`.
380380

381+
## List & Menu Ordering
382+
383+
A list orders itself the way the user already reads the same things somewhere else. Resource menus (`+` attach, `@` mention, resource-tab `+`) mirror the **sidebar** top-down; a row or root **context menu** mirrors that surface's **toolbar**, left-to-right becoming top-to-bottom; tab strips mirror their nav. Platform-only entries (desktop Browser, Terminal) trail the shared set.
384+
385+
Encode the order in ONE exported constant and sort by it — never a hand-maintained literal per menu (`RESOURCE_MENU_ORDER` / `byResourceMenuOrder` in `home/components/mothership-view/components/resource-registry`). Render mixed item kinds in a single ordered pass; emitting all submenu-backed families and then all flat ones silently pins every submenu to the top no matter what the constant says. Divergence is allowed only for search ranking, user-controlled ordering, and recency. Full rule in `.claude/rules/sim-list-ordering.md`.
386+
381387
## Styling
382388

383389
Use Tailwind only, no inline styles. Use `cn()` from `@sim/emcn` for conditional classes.

apps/desktop/src/main/browser-agent/url-guard.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import { beforeEach, describe, expect, it, vi } from 'vitest'
22

3+
// url-guard pulls in @/main/navigation, which imports electron.
4+
vi.mock('electron', () => import('@/test/electron-mock'))
5+
36
const { mockLookup } = vi.hoisted(() => ({ mockLookup: vi.fn() }))
47

58
// The real resolveHostAddresses runs; only the resolver under it is mocked, so

apps/desktop/src/main/csp.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
import { beforeEach, describe, expect, it, vi } from 'vitest'
2+
3+
// csp pulls in @/main/navigation, which imports electron.
4+
vi.mock('electron', () => import('@/test/electron-mock'))
5+
26
import { attachCspFallback, DEFAULT_DESKTOP_CSP } from '@/main/csp'
37

48
type HeadersReceivedHandler = (

apps/desktop/src/main/telemetry-policy.test.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { describe, expect, it } from 'vitest'
1+
import { describe, expect, it, vi } from 'vitest'
2+
3+
// telemetry-policy pulls in @/main/navigation, which imports electron.
4+
vi.mock('electron', () => import('@/test/electron-mock'))
5+
26
import { shouldBlockRequest } from '@/main/telemetry-policy'
37

48
describe('shouldBlockRequest', () => {

apps/docs/app/global.css

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -373,23 +373,33 @@ aside#nd-sidebar [data-radix-scroll-area-viewport] {
373373
Safe because the grid columns are explicit (`0px 300px 1fr 268px 0px`), so
374374
removing the placeholder from flow leaves its track intact. `left`/`width`
375375
are restated because a fixed box no longer derives them from its grid cell,
376-
and `top`/`height` already come from fumadocs' own utility classes. */
376+
and `height` already comes from fumadocs' own utility classes.
377+
378+
Anchoring to `bottom` rather than `top` is what keeps the footer off it: the
379+
offset is how far the footer currently reaches into the viewport (published
380+
by `FooterOverlapProbe`), so the sidebar keeps its full height and slides up
381+
out of view as the footer arrives, the way it did before it was pinned. With
382+
no footer on screen the offset is 0 and this resolves back to top: 92px. */
377383
[data-sidebar-placeholder] {
378384
position: fixed !important;
379385
left: var(--sidebar-offset);
380386
width: var(--fd-sidebar-width);
387+
top: auto !important;
388+
bottom: var(--docs-footer-overlap, 0px) !important;
381389
}
382390

383391
/* Sidebar divider line — pinned for the same reason, and so it stays glued to
384392
the sidebar's right edge. Being fixed takes it out of #nd-docs-layout's grid
385393
entirely, so it needs no grid placement and cannot skew a content cell; its
386-
position comes from `left`/`top` alone. */
394+
position comes from `left`/`top`/`bottom` alone. Unlike the sidebar it is
395+
shortened rather than slid, so it runs from the navbar down to the footer's
396+
top border and the two meet instead of the line stopping short. */
387397
#nd-docs-layout::before {
388398
content: "";
389399
display: block;
390400
position: fixed;
391401
top: 92px; /* below navbar */
392-
height: calc(100dvh - 92px);
402+
bottom: var(--docs-footer-overlap, 0px);
393403
left: calc(var(--sidebar-offset) + var(--fd-sidebar-width));
394404
width: 1px;
395405
background-color: var(--surface-active);
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
'use client'
2+
3+
import { useEffect, useRef } from 'react'
4+
5+
const OVERLAP_PROPERTY = '--docs-footer-overlap'
6+
7+
/**
8+
* Publishes how many pixels of the viewport bottom the footer currently covers.
9+
*
10+
* The docs sidebar and its divider are pinned to the viewport, so on their own
11+
* they would run underneath the footer at the end of the page. Both read this as
12+
* their `bottom` and stop at the footer's top edge instead — the sidebar slides
13+
* away with the page and the divider meets the footer's border.
14+
*
15+
* It is deliberately measured against the viewport rather than the document, so
16+
* the value only moves while the footer is actually on screen — a content-height
17+
* change higher up the page cannot disturb the sidebar at all. That was the
18+
* regression #6301 fixed and this must not undo.
19+
*/
20+
export function FooterOverlapProbe() {
21+
const sentinelRef = useRef<HTMLDivElement>(null)
22+
23+
useEffect(() => {
24+
const sentinel = sentinelRef.current
25+
if (!sentinel) return
26+
27+
const root = document.documentElement
28+
let frame = 0
29+
let published = -1
30+
31+
const measure = () => {
32+
frame = 0
33+
const overlap = Math.max(
34+
0,
35+
Math.round(window.innerHeight - sentinel.getBoundingClientRect().top)
36+
)
37+
if (overlap === published) return
38+
published = overlap
39+
root.style.setProperty(OVERLAP_PROPERTY, `${overlap}px`)
40+
}
41+
42+
const schedule = () => {
43+
if (frame) return
44+
frame = requestAnimationFrame(measure)
45+
}
46+
47+
measure()
48+
window.addEventListener('scroll', schedule, { passive: true })
49+
window.addEventListener('resize', schedule)
50+
51+
const observer = new ResizeObserver(schedule)
52+
observer.observe(document.body)
53+
54+
return () => {
55+
if (frame) cancelAnimationFrame(frame)
56+
window.removeEventListener('scroll', schedule)
57+
window.removeEventListener('resize', schedule)
58+
observer.disconnect()
59+
root.style.removeProperty(OVERLAP_PROPERTY)
60+
}
61+
}, [])
62+
63+
return (
64+
<div ref={sentinelRef} aria-hidden className='pointer-events-none absolute inset-x-0 top-0' />
65+
)
66+
}

0 commit comments

Comments
 (0)