Skip to content

Commit 2aa0bd1

Browse files
committed
Merge staging into the Platform port
Staging's #9708 brought a snapshot of the design system and Home, Search, Integrations, Links and the rail into the organization surface. Both sides now live in one tree, so the two modes get one seam instead of two copies. Design system and shared views take staging's fixes (focus-visible, link hover, --container-chat, .chipStatus, avatars, tooltip, connect cards) and keep what only the zone needs. Home has one data module and one dashboard, Links one page, Integrations staging's real wiring; the zone's fixture Integrations stay beside it for its fixture specs. What needs legacy chrome (the rail, Search's source cards, the composer host) moved verbatim to app/o/[id]/_redesign, so platform/ stays inside its data seam. The layout now fills the Platform organization context from the surface read both hosts share. The design audit learns the embedded host and that keyboard focus may paint. knip gets the live suite's scripted worker. The macOS job skips next build's type pass, which ran out its 25 minutes on that runner.
2 parents 5c4a33e + f17f1b6 commit 2aa0bd1

325 files changed

Lines changed: 16436 additions & 6577 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.

‎.agents/skills/add-enrichment/SKILL.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ Because enrichments run on Sim's hosted keys by default, **every provider tool y
2222

2323
## Architecture (what you're plugging into)
2424

25-
- **`enrichments/types.ts`** — `EnrichmentConfig { id, name, description, icon, inputs, outputs, providers }` and `EnrichmentProvider { id, label, toolId, buildParams, projectFailure, mapOutput, isVerified? }` — `toolProvider` fills `projectFailure` with the standard HTTP projection; override it only for a provider whose failure shape is nonstandard (see `enrichments/provider-failures/`). Providers are **plain data** (no `@/tools` import) so the catalog stays client-safe.
26-
- **`enrichments/providers.ts`** — `toolProvider(...)` (typed passthrough) plus shared input helpers: `str(v)`, `normalizeDomain(v)`, `firstNonEmpty(arr)`, `splitName(fullName)`, and `projectEnrichmentProviderFailure`.
25+
- **`enrichments/types.ts`** — `EnrichmentConfig { id, name, description, icon, inputs, outputs, providers }` and `EnrichmentProvider { id, label, toolId, buildParams, projectFailure, mapOutput, isVerified? }` — `toolProvider` fills `projectFailure` with the standard HTTP projection; override it only for a provider whose failure shape is nonstandard. Providers are **plain data** (no `@/tools` import) so the catalog stays client-safe.
26+
- **`enrichments/providers.ts`** — `toolProvider(...)` (typed passthrough) plus shared input helpers: `str(v)`, `normalizeDomain(v)`, `splitName(fullName)`, and `projectEnrichmentProviderFailure`.
2727
- **`enrichments/run.ts`** — the server-only cascade runner. Calls `executeTool(provider.toolId, { ...params, _context: { workspaceId, userId } })`, accumulates hosted-key cost, returns the first non-empty mapped result. **You do not edit this** — it works for any registry entry.
2828
- **`enrichments/registry.ts`** — `ENRICHMENT_REGISTRY` / `ALL_ENRICHMENTS` / `getEnrichment`. Register new entries here.
2929

@@ -111,7 +111,7 @@ export { myEnrichment } from './my-enrichment'
111111
```
112112

113113
Rules:
114-
- Keep the file **client-safe**: import only `@sim/emcn/icons`, `@sim/utils/*`, `@/enrichments/providers`, `@/enrichments/provider-failures/*`, and the types. **Never import `@/tools`** here — the runner does the tool call.
114+
- Keep the file **client-safe**: import only `@sim/emcn/icons`, `@sim/utils/*`, `@/enrichments/providers`, and the types. **Never import `@/tools`** here — the runner does the tool call.
115115
- `buildParams` returns `null` when inputs are insufficient (provider skipped). `mapOutput` returns `null`/empty for a miss (falls through). Use `filterUndefined` when assembling optional tool params; coerce numbers explicitly (don't pass `''` to number outputs).
116116
- Output `id`s are the keys `mapOutput` returns; output `name`s are the default column names (the user can rename them in the config).
117117

‎.github/ci-suites.json‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@
2424
"group": "organization-search",
2525
"task": "ci:e2e-organization-search"
2626
},
27+
{
28+
"group": "org-redesign",
29+
"task": "ci:e2e-org-redesign"
30+
},
2731
{
2832
"group": "browser-extension-navigation",
2933
"task": "ci:e2e-browser-extension"

‎.github/scripts/ci-plan.gql‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ query ($base: String!, $head: String!) {
2020
"ci:e2e-desktop-inbox"
2121
"ci:e2e-assistant-actions"
2222
"ci:e2e-organization-search"
23+
"ci:e2e-org-redesign"
2324
"ci:e2e-docs-platform"
2425
"ci:e2e-browser-extension"
2526
"ci:e2e-project-files"

‎.github/scripts/http-e2e.sh‎

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# CI groups, which reuse the production build from `e2e-build`.
44
# The job extracts it into apps/sim/.next before running these suites.
55
#
6-
# Usage: http-e2e.sh <scim|cli|stop-after|desktop-inbox|assistant-actions|organization-search|browser-extension|browser-extension-native|browser-extension-navigation|browser-extension-composer|browser-extension-access|project-files|mobile|seo|platform-live-zone|platform-live-flag-off> (run from apps/sim)
6+
# Usage: http-e2e.sh <scim|cli|stop-after|desktop-inbox|assistant-actions|organization-search|org-redesign|browser-extension|browser-extension-native|browser-extension-navigation|browser-extension-composer|browser-extension-access|project-files|mobile|seo|platform-live-zone|platform-live-flag-off> (run from apps/sim)
77
#
88
# The job provides DATABASE_URL, BETTER_AUTH_SECRET and ENCRYPTION_KEY; each group sets the rest of
99
# its app's environment here. Reports and server logs land in $RUNNER_TEMP/e2e.
@@ -20,7 +20,7 @@
2020
# telemetry flush runs detached and still writes .next/dev).
2121
set -euo pipefail
2222

23-
group=${1:?usage: http-e2e.sh <scim|cli|stop-after|desktop-inbox|assistant-actions|organization-search|browser-extension|browser-extension-native|browser-extension-navigation|browser-extension-composer|browser-extension-access|project-files|mobile|seo|platform-live-zone|platform-live-flag-off>}
23+
group=${1:?usage: http-e2e.sh <scim|cli|stop-after|desktop-inbox|assistant-actions|organization-search|org-redesign|browser-extension|browser-extension-native|browser-extension-navigation|browser-extension-composer|browser-extension-access|project-files|mobile|seo|platform-live-zone|platform-live-flag-off>}
2424
report_dir="$RUNNER_TEMP/e2e"
2525
ready_timeout_seconds=600
2626
mkdir -p "$report_dir"
@@ -349,6 +349,59 @@ case "$group" in
349349
esac
350350
;;
351351

352+
# One hosted Enterprise organization, a scripted worker in place of the
353+
# model, and the app twice: with People off, then on. Hosted is decided by the
354+
# app's hostname, which the job maps to loopback. Both phases report, whichever fails.
355+
org-redesign)
356+
app_server=start
357+
export REDIS_URL=redis://127.0.0.1:6379
358+
export E2E_APP_PUBLIC_URL=http://org-redesign.sim.ai:3029
359+
export NEXT_PUBLIC_APP_URL="$E2E_APP_PUBLIC_URL"
360+
export INTERNAL_API_BASE_URL=http://127.0.0.1:3029 TRUSTED_ORIGINS=http://127.0.0.1:3029
361+
export NEXT_PUBLIC_SOCKET_URL=http://127.0.0.1:3030 SOCKET_SERVER_URL=http://127.0.0.1:3030
362+
export BILLING_ENABLED=true NEXT_PUBLIC_BILLING_ENABLED=true
363+
export ENTERPRISE_ENABLED=true NEXT_PUBLIC_ENTERPRISE_ENABLED=true
364+
export ORGANIZATIONS_ENABLED=true NEXT_PUBLIC_ORGANIZATIONS_ENABLED=true
365+
export SSO_ENABLED=true NEXT_PUBLIC_SSO_ENABLED=true
366+
export KNOWLEDGE_MEMBER_ACCESS=true CREDENTIAL_GROUPS=true ACCESS_CONTROL_ENABLED=true
367+
export NEXT_PUBLIC_ACCESS_CONTROL_ENABLED=true SIM_LINKS=true
368+
export COPILOT_TOOL_PERMISSIONS_ENABLED=true
369+
export INTERNAL_API_SECRET=org-redesign-ci-local-secret-at-least-32-characters
370+
export COPILOT_API_KEY=org-redesign-ci-local-copilot-key MOTHERSHIP_SIM_TRANSPORT=direct
371+
export SIM_AGENT_API_URL=http://127.0.0.1:3031
372+
export GOOGLE_CLIENT_ID=org-redesign-google-client GOOGLE_CLIENT_SECRET=org-redesign-google-secret
373+
export JIRA_CLIENT_ID=org-redesign-jira-client JIRA_CLIENT_SECRET=org-redesign-jira-secret
374+
export CONFLUENCE_CLIENT_ID=org-redesign-confluence-client CONFLUENCE_CLIENT_SECRET=org-redesign-confluence-secret
375+
export STRIPE_SECRET_KEY=sk_test_org_redesign STRIPE_WEBHOOK_SECRET=whsec_org_redesign
376+
export DB_TX_TRIPWIRE=throw
377+
export BROWSER_EXTENSION_IDS=abcdefghijklmnopabcdefghijklmnop
378+
bun --no-env-file scripts/org-redesign-seed.ts
379+
start_browser_install org-redesign --only-shell chromium
380+
bun --no-env-file scripts/org-redesign-e2e/scripted-agent.ts 3031 \
381+
> "$report_dir/org-redesign-agent.log" 2>&1 &
382+
agent_pid=$!
383+
wait_ready "$agent_pid" http://127.0.0.1:3031/health 'scripted agent'
384+
start_app org-redesign 3029 'organization redesign' record-http-status
385+
start_realtime org-redesign 3030
386+
wait_for_browsers
387+
run_status=0
388+
ORG_REDESIGN_E2E_ORIGIN="$E2E_APP_PUBLIC_URL" \
389+
ORG_REDESIGN_E2E_DATABASE_URL="$DATABASE_URL" \
390+
ORG_REDESIGN_E2E_AUTH_SECRET="$BETTER_AUTH_SECRET" \
391+
ORG_REDESIGN_E2E_REPORT_PATH="$report_dir/org-redesign-ui-report.json" \
392+
bun --no-env-file scripts/test-org-redesign-e2e.ts || run_status=$?
393+
stop_app
394+
export PEOPLE=true
395+
start_app org-redesign-people 3029 'organization redesign, People on'
396+
ORG_REDESIGN_E2E_PHASE=people \
397+
ORG_REDESIGN_E2E_ORIGIN="$E2E_APP_PUBLIC_URL" \
398+
ORG_REDESIGN_E2E_DATABASE_URL="$DATABASE_URL" \
399+
ORG_REDESIGN_E2E_AUTH_SECRET="$BETTER_AUTH_SECRET" \
400+
ORG_REDESIGN_E2E_REPORT_PATH="$report_dir/org-redesign-people-ui-report.json" \
401+
bun --no-env-file scripts/test-org-redesign-e2e.ts || run_status=$?
402+
exit "$run_status"
403+
;;
404+
352405
browser-extension|browser-extension-native|browser-extension-navigation|browser-extension-composer|browser-extension-access)
353406
extension_group=all
354407
case "$group" in

‎.github/workflows/checks.yml‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ jobs:
207207
needs: [changes, e2e-build]
208208
if: needs.changes.outputs.http == 'true'
209209
runs-on: ubuntu-24.04
210-
timeout-minutes: 20
210+
timeout-minutes: ${{ matrix.group == 'org-redesign' && 45 || 20 }}
211211
strategy:
212212
fail-fast: false
213213
matrix: ${{ fromJSON(needs.changes.outputs.http_matrix) }}
@@ -227,7 +227,7 @@ jobs:
227227
--health-retries 10
228228
# Desktop inbox, browser extension, and Project Files use Redis.
229229
redis:
230-
image: ${{ (matrix.group == 'desktop-inbox' || matrix.group == 'assistant-actions' || matrix.group == 'organization-search' || startsWith(matrix.group, 'browser-extension-') || matrix.group == 'project-files') && 'redis:8.2-alpine' || '' }}
230+
image: ${{ (matrix.group == 'desktop-inbox' || matrix.group == 'assistant-actions' || matrix.group == 'organization-search' || matrix.group == 'org-redesign' || startsWith(matrix.group, 'browser-extension-') || matrix.group == 'project-files') && 'redis:8.2-alpine' || '' }}
231231
ports:
232232
- 6379:6379
233233
options: >-
@@ -293,6 +293,10 @@ jobs:
293293
if: matrix.group == 'scim'
294294
run: printf '127.0.0.1 scim-e2e.sim.ai\n' | sudo tee -a /etc/hosts
295295

296+
- name: Map the hosted organization fixture to loopback
297+
if: matrix.group == 'org-redesign'
298+
run: printf '127.0.0.1 org-redesign.sim.ai\n' | sudo tee -a /etc/hosts
299+
296300
# No step timeout: the job's bound covers a hang without cutting a slow but healthy suite
297301
# short of writing its report.
298302
- name: Run end-to-end suites

‎.github/workflows/platform-design.yml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,10 @@ jobs:
7373
- run: bun install --frozen-lockfile
7474
- uses: ./.github/actions/desktop-native-cache
7575
- run: bun run --cwd apps/desktop build
76+
# The browser job type-checks; on this 3-core macOS runner next build's own pass outruns the job.
7677
- run: bun run --cwd apps/sim build:platform
78+
env:
79+
NEXT_BUILD_SKIP_TYPE_CHECK: '1'
7780
- run: bun run --cwd apps/sim test:platform:desktop
7881
env:
7982
PLATFORM_DESKTOP_REPORT_PATH: ${{ runner.temp }}/platform-desktop/report.json

‎apps/docs/content/docs/integrations/firecrawl.mdx‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,4 +454,38 @@ Retrieve the remaining and allocated Firecrawl credits for the team.
454454

455455
</ReferenceDetails>
456456

457+
### Firecrawl Enrich
458+
459+
Find work emails, phone numbers, LinkedIn profiles, company domains, and company information through Firecrawl Alexandria. Uses FullEnrich for contact lookups and People Data Labs for company, name-based profile, and single-part-name contact lookups. Provider terms must be enabled in Firecrawl. People Data Labs has a 50,000-record annual team limit.
460+
461+
<ReferenceDetails>
462+
463+
#### Input
464+
465+
| Parameter | Type | Required | Description |
466+
| --------- | ---- | -------- | ----------- |
467+
| `apiKey` | string | Yes | Firecrawl API key |
468+
| `enrichmentType` | string | Yes | Lookup type: work_email, phone, linkedin, company, or company_domain |
469+
| `fullName` | string | No | Full name of the person |
470+
| `companyDomain` | string | No | Company website domain |
471+
| `companyName` | string | No | Company name when the domain is unknown |
472+
| `linkedinUrl` | string | No | LinkedIn profile URL for work email or mobile lookup |
473+
| `email` | string | No | Email address for reverse LinkedIn profile lookup |
474+
475+
#### Output
476+
477+
| Parameter | Type | Description |
478+
| --------- | ---- | ----------- |
479+
| `email` | string | Work email, when found |
480+
| `emailVerified` | boolean | True only when FullEnrich reports DELIVERABLE; catch-all and high-probability addresses remain unverified |
481+
| `phone` | string | Phone number, preferring mobile when available |
482+
| `linkedinUrl` | string | LinkedIn profile URL, when found |
483+
| `domain` | string | Company website domain, when found |
484+
| `employeeCount` | string | Company headcount or size range |
485+
| `description` | string | Company description |
486+
| `creditsUsed` | number | Firecrawl credits charged for this lookup |
487+
| `statusCode` | number | Provider or HTTP response status code |
488+
489+
</ReferenceDetails>
490+
457491

‎apps/sim/app/api/organizations/[id]/sim-links/route.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
internalRateLimits,
66
internalSessionAuth,
77
} from '@/lib/api/server/routes'
8-
import { presentSimLink } from '@/lib/api/server/sim-link-presenters'
8+
import { presentSimLink, presentSimLinkPage } from '@/lib/api/server/sim-link-presenters'
99
import { createSimLink, listSimLinks } from '@/lib/sim-links/application/use-cases'
1010

1111
const policy = {
@@ -21,7 +21,7 @@ export const GET = defineInternalJsonRoute({
2121
operation: listSimLinks.operation,
2222
useCase: listSimLinks,
2323
mapInput: ({ params, query }) => ({ organizationId: params.id, ...query }),
24-
present: ({ links, nextCursor }) => ({ links: links.map(presentSimLink), nextCursor }),
24+
present: presentSimLinkPage,
2525
})
2626

2727
export const POST = defineInternalJsonRoute({
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
'use client'
2+
3+
import { useState, useTransition } from 'react'
4+
import { useRouter } from 'next/navigation'
5+
import { useSession } from '@/lib/auth/client'
6+
import { organizationRoutes } from '@/lib/navigation/paths'
7+
import { LiveHome } from '@/app/o/[organizationId]/_redesign/home/live-home'
8+
import { SearchSurfaceHeader } from '@/app/o/[organizationId]/_redesign/search/search-surface-header'
9+
import { OrganizationConversation } from '@/app/o/[organizationId]/components/organization-conversation'
10+
import { useOrganizationContext } from '@/app/o/[organizationId]/providers/organization-provider'
11+
import { SEARCH_MODES } from '@/app/o/[organizationId]/search/use-organization-search'
12+
import { HomeDashboardSkeleton } from '@/platform/app/(shell)/home/_components/home-dashboard/home-dashboard-skeleton'
13+
import { useMothershipDraftsStore } from '@/stores/mothership-drafts/store'
14+
15+
/** Search's Assistant keeps its own unsent question; the agent composer keeps the default `home` key. */
16+
const HOME_ASSISTANT_DRAFT_ID = 'home-assistant'
17+
18+
interface HomeConversationProps {
19+
requestMode: 'agent' | 'assistant'
20+
}
21+
22+
/** The real conversation, landing on the dashboard until the first turn takes over. */
23+
export function HomeConversation({ requestMode }: HomeConversationProps) {
24+
const { organization } = useOrganizationContext()
25+
const router = useRouter()
26+
const { data: session } = useSession()
27+
const routes = organizationRoutes(organization.id)
28+
const [conversationInstance, setConversationInstance] = useState(0)
29+
const [isStartingNew, startNewTransition] = useTransition()
30+
// An Assistant chat started here takes its Search address without leaving this page, so it carries
31+
// Search's controls itself, and they navigate rather than switch a view this page doesn't hold.
32+
const assistantChatHeader = (
33+
<SearchSurfaceHeader
34+
search={{
35+
activeMode: 'assistant',
36+
modes: [...SEARCH_MODES],
37+
changeMode: (mode) => {
38+
if (mode !== 'search') return
39+
const current = new URL(window.location.href)
40+
const onChat = current.pathname.startsWith(`${routes.search}/chat/`)
41+
const params = new URLSearchParams(onChat ? current.search : '')
42+
params.set('mode', 'search')
43+
router.push(`${onChat ? current.pathname : routes.search}?${params}`)
44+
},
45+
newLabel: 'New chat',
46+
canStartNew: true,
47+
isStartingNew,
48+
startNew: () => {
49+
useMothershipDraftsStore
50+
.getState()
51+
.clearDraft(
52+
`${session?.user?.id}:organization:${organization.id}:${HOME_ASSISTANT_DRAFT_ID}`
53+
)
54+
startNewTransition(() => {
55+
setConversationInstance((current) => current + 1)
56+
router.push(routes.home, { scroll: false })
57+
})
58+
},
59+
}}
60+
/>
61+
)
62+
return (
63+
<OrganizationConversation
64+
key={conversationInstance}
65+
requestMode={requestMode}
66+
draftId={requestMode === 'assistant' ? HOME_ASSISTANT_DRAFT_ID : undefined}
67+
landing={{
68+
chatHeader: requestMode === 'assistant' ? assistantChatHeader : undefined,
69+
fallback: <HomeDashboardSkeleton requestMode={requestMode} />,
70+
render: (slot) => <LiveHome {...slot} />,
71+
}}
72+
/>
73+
)
74+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
'use client'
2+
3+
import { createContext, memo, type ReactNode, useContext } from 'react'
4+
import { useSession } from '@/lib/auth/client'
5+
import type { OrganizationLandingSlot } from '@/app/o/[organizationId]/components/organization-conversation'
6+
import { useOrganizationContext } from '@/app/o/[organizationId]/providers/organization-provider'
7+
import type { ChatRequestMode } from '@/app/workspace/[workspaceId]/home/types'
8+
import { HomeDashboard } from '@/platform/app/(shell)/home/_components/home-dashboard/home-dashboard'
9+
import { useLiveHome } from '@/platform/lib/data/home'
10+
import {
11+
type OrganizationHomeWidget,
12+
useOrganizationHomeStore,
13+
} from '@/stores/organization-home/store'
14+
15+
const NO_WIDGETS: readonly OrganizationHomeWidget[] = []
16+
const COMPOSER_PROMPTS: Record<ChatRequestMode, string> = {
17+
agent: 'What should we get done?',
18+
plan: 'What should we understand and plan?',
19+
assistant: 'Ask about your company’s knowledge.',
20+
}
21+
22+
const ComposerSlot = createContext<ReactNode>(null)
23+
24+
function ComposerOutlet() {
25+
return useContext(ComposerSlot)
26+
}
27+
28+
/** The conversation re-renders on every keystroke; through context, only its composer follows. */
29+
const LiveDashboard = memo(function LiveDashboard({
30+
requestMode,
31+
}: Pick<OrganizationLandingSlot, 'requestMode'>) {
32+
const { organization } = useOrganizationContext()
33+
const { data: session } = useSession()
34+
const viewer = `${session?.user?.id}:${organization.id}`
35+
const hiddenWidgets = useOrganizationHomeStore(
36+
(state) => state.hiddenWidgets[viewer] ?? NO_WIDGETS
37+
)
38+
const setWidgetHidden = useOrganizationHomeStore((state) => state.setWidgetHidden)
39+
const { view, actions } = useLiveHome(requestMode === 'assistant', hiddenWidgets)
40+
return (
41+
<HomeDashboard
42+
view={view}
43+
actions={actions}
44+
mode='Build'
45+
prompt={COMPOSER_PROMPTS[requestMode]}
46+
composer={<ComposerOutlet />}
47+
hiddenWidgets={hiddenWidgets}
48+
onWidgetHiddenChange={(widget, hidden) => setWidgetHidden(viewer, widget, hidden)}
49+
/>
50+
)
51+
})
52+
53+
export function LiveHome({ requestMode, composer }: OrganizationLandingSlot) {
54+
return (
55+
<ComposerSlot.Provider value={composer}>
56+
<LiveDashboard requestMode={requestMode} />
57+
</ComposerSlot.Provider>
58+
)
59+
}

0 commit comments

Comments
 (0)