Skip to content

Commit 04e9a87

Browse files
committed
Merge remote-tracking branch 'origin/staging' into feat/snowflake-integration
# Conflicts: # apps/sim/tools/generated/tool-ids.ts # apps/sim/tools/generated/tool-metadata.ts # apps/sim/tools/generated/tool-outputs.ts
2 parents c4d3718 + 5620017 commit 04e9a87

265 files changed

Lines changed: 48074 additions & 2510 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.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+
}

apps/docs/components/footer/footer.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Link from 'next/link'
2+
import { FooterOverlapProbe } from '@/components/footer/footer-overlap'
23
import { SimWordmark } from '@/components/ui/sim-logo'
34
import { SIM_SITE_URL } from '@/lib/urls'
45

@@ -133,13 +134,17 @@ function FooterColumn({ title, items }: { title: string; items: FooterItem[] })
133134
/**
134135
* Site footer.
135136
*
136-
* `relative z-[22]` stacks it above the docs sidebar (z-20) and that sidebar's
137-
* divider (z-21), both of which are pinned to the viewport, so the footer slides
138-
* over them at the end of the page instead of being drawn through.
137+
* The docs sidebar and its divider are pinned to the viewport, so they would run
138+
* underneath a full-bleed footer at the end of the page. `FooterOverlapProbe`
139+
* publishes how far the footer reaches into the viewport and both stop there
140+
* instead. `relative z-[22]` stacks the footer above the sidebar (z-20) and its
141+
* divider (z-21) so that, before the probe's first measurement, the footer covers
142+
* them rather than being drawn through.
139143
*/
140144
export function Footer() {
141145
return (
142146
<footer className='relative z-[22] mt-[120px] w-full border-[var(--border)] border-t bg-[var(--bg)] max-sm:mt-16 max-lg:mt-[88px]'>
147+
<FooterOverlapProbe />
143148
<div className='mx-auto w-full max-w-[1460px] px-20 pt-16 pb-16 max-sm:px-5 max-lg:px-8 max-lg:pt-12 max-lg:pb-12'>
144149
<nav
145150
aria-label='Footer navigation'

apps/docs/components/icons.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2494,6 +2494,25 @@ export function DocumentIcon(props: SVGProps<SVGSVGElement>) {
24942494
)
24952495
}
24962496

2497+
export function MintlifyIcon(props: SVGProps<SVGSVGElement>) {
2498+
return (
2499+
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 19 19' fill='none'>
2500+
<path
2501+
d='M18.367 7.28888V1.59755C18.367 0.986819 17.8715 0.5 17.2699 0.5H11.5812C10.6877 0.5 9.80295 0.677018 8.98017 1.01336C8.15738 1.35856 7.40539 1.85424 6.77724 2.49152L6.733 2.53578C5.90137 3.37664 5.30862 4.42108 5.00781 5.57174C5.54749 5.43012 6.10483 5.35931 6.6622 5.35046C8.14852 5.33276 9.60831 5.81073 10.7938 6.7047C11.8643 7.50131 12.6783 8.59885 13.1206 9.86458C13.5807 11.148 13.6337 12.5465 13.2887 13.8653C14.43 13.5644 15.4828 12.9714 16.3233 12.1393L16.3675 12.0951C16.9957 11.4667 17.4999 10.7143 17.845 9.89114C18.19 9.06797 18.3581 8.18285 18.3581 7.28888H18.367Z'
2502+
fill='#18E299'
2503+
/>
2504+
<path
2505+
d='M4.83793 7.193C4.84674 5.44706 5.54303 3.77167 6.76814 2.51953L2.03511 7.25472C2.01749 7.27236 1.99985 7.28117 1.98222 7.29881C0.827615 8.44513 0.131342 9.97945 0.0167623 11.6019C-0.0890033 13.1186 0.307609 14.6176 1.15373 15.8698C1.23444 15.9892 1.45343 16.0285 1.57682 15.9139L4.47656 13.0216C5.38438 12.1134 5.66643 10.7642 5.23455 9.55618C4.96132 8.80666 4.82912 8.00424 4.83793 7.193Z'
2506+
fill='#0C8C5E'
2507+
/>
2508+
<path
2509+
d='M16.341 12.0938C15.4332 12.9844 14.2962 13.6016 13.0623 13.875C11.8195 14.1483 10.5327 14.0689 9.33405 13.6457C9.33405 13.6457 9.32522 13.6457 9.31641 13.6457C8.10892 13.2136 6.76042 13.4958 5.8526 14.3952L2.95282 17.2875C2.82943 17.4109 2.84706 17.6137 2.99689 17.7107C4.24845 18.5484 5.74683 18.954 7.26281 18.8482C8.88455 18.7336 10.4093 18.037 11.5639 16.8818L11.608 16.8378L16.341 12.1026V12.0938Z'
2510+
fill='#0C8C5E'
2511+
/>
2512+
</svg>
2513+
)
2514+
}
2515+
24972516
export function MistralIcon(props: SVGProps<SVGSVGElement>) {
24982517
const id = useId()
24992518
const clipId = `mistral_clip_${id}`

apps/docs/components/ui/icon-mapping.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ import {
149149
MicrosoftSharepointIcon,
150150
MicrosoftTeamsIcon,
151151
MillionVerifierIcon,
152+
MintlifyIcon,
152153
MistralIcon,
153154
MondayIcon,
154155
MongoDBIcon,
@@ -429,6 +430,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
429430
microsoft_teams: MicrosoftTeamsIcon,
430431
'microsoft-teams': MicrosoftTeamsIcon,
431432
millionverifier: MillionVerifierIcon,
433+
mintlify: MintlifyIcon,
432434
mistral_parse: MistralIcon,
433435
mistral_parse_v2: MistralIcon,
434436
mistral_parse_v3: MistralIcon,

0 commit comments

Comments
 (0)