Skip to content

Commit 0eb9b4a

Browse files
committed
Merge remote-tracking branch 'origin/staging' into wip/platform-org-wiring
# Conflicts: # apps/docs/components/docs/navigation-drawer.tsx # apps/docs/components/docs/shell/shell.tsx
2 parents 1d5c4c8 + a0afe79 commit 0eb9b4a

46 files changed

Lines changed: 2184 additions & 577 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.

‎apps/desktop/src/main/load-health.test.ts‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,20 @@ import { attachLoadHealth, classifyLoadError } from '@/main/load-health'
33
import { BrowserWindow as MockBrowserWindow } from '@/test/electron-mock'
44

55
describe('classifyLoadError', () => {
6+
const SELF_HOSTED_URL = 'http://sim.corp.internal:3000/workspace'
7+
68
it('does NOT ignore ERR_FAILED (-2) or ERR_IO_PENDING (-1)', () => {
7-
expect(classifyLoadError(-2)).toBe('unreachable')
8-
expect(classifyLoadError(-1)).toBe('unreachable')
9+
expect(classifyLoadError(-2, SELF_HOSTED_URL)).toBe('unreachable')
10+
expect(classifyLoadError(-1, SELF_HOSTED_URL)).toBe('unreachable')
911
})
1012

1113
it('reports ERR_ADDRESS_UNREACHABLE as a local-network block only under Local Network privacy', () => {
12-
expect(classifyLoadError(-109, true)).toBe('local_network')
13-
expect(classifyLoadError(-109, false)).toBe('unreachable')
14+
expect(classifyLoadError(-109, SELF_HOSTED_URL, true)).toBe('local_network')
15+
expect(classifyLoadError(-109, SELF_HOSTED_URL, false)).toBe('unreachable')
16+
})
17+
18+
it('never blames Local Network privacy for Sim Cloud, which is public', () => {
19+
expect(classifyLoadError(-109, 'https://www.sim.ai/workspace', true)).toBe('unreachable')
1420
})
1521
})
1622

‎apps/desktop/src/main/load-health.ts‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { release } from 'node:os'
22
import { createLogger } from '@sim/logger'
33
import type { BrowserWindow } from 'electron'
4+
import { isSimCloudOrigin } from '@/main/config'
45
import { type EventRecorder, scrubUrl } from '@/main/observability'
56

67
const logger = createLogger('DesktopLoadHealth')
@@ -32,6 +33,7 @@ export type LoadErrorKind =
3233
*/
3334
export function classifyLoadError(
3435
errorCode: number,
36+
url: string,
3537
localNetworkPrivacy: boolean = hasLocalNetworkPrivacy()
3638
): LoadErrorKind {
3739
// Only ERR_ABORTED (-3) and success (0) are ignored. ERR_FAILED (-2) and
@@ -54,7 +56,8 @@ export function classifyLoadError(
5456
// macOS Local Network privacy fails the socket with EHOSTUNREACH, which
5557
// Chromium reports as ERR_ADDRESS_UNREACHABLE (-109), so a self-hosted server
5658
// on a private address looks like a routing failure until Sim is allowed.
57-
if (errorCode === -109 && localNetworkPrivacy) {
59+
// Sim Cloud is public, so that permission can never be what blocked it.
60+
if (errorCode === -109 && localNetworkPrivacy && !isSimCloudOrigin(url)) {
5861
return 'local_network'
5962
}
6063
return 'unreachable'
@@ -151,7 +154,7 @@ export function attachLoadHealth(win: BrowserWindow, deps: LoadHealthDeps): Load
151154
return
152155
}
153156
clearTimeout(watchdogTimer)
154-
const kind = classifyLoadError(errorCode)
157+
const kind = classifyLoadError(errorCode, validatedURL)
155158
if (kind === 'ignored') {
156159
return
157160
}

‎apps/docs/components/docs/navigation-drawer/navigation-drawer.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ export function DocsNavigationDrawer({ children }: DocsNavigationDrawerProps) {
8989
}}
9090
className={cn(
9191
docsScrollAreaClass,
92-
'-mx-1 mt-5 min-h-0 flex-1 overflow-y-auto overscroll-contain px-1 pb-6'
92+
'-mx-1 mt-5 min-h-0 flex-1 overflow-y-auto overflow-x-hidden overscroll-contain px-1 pb-6'
9393
)}
9494
>
9595
<SidebarNavigation

‎apps/docs/components/docs/shell/shell.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ export function DocsShell({ children, navigationBySection }: DocsShellProps) {
103103
key={section}
104104
className={cn(
105105
docsScrollAreaClass,
106-
'-mx-1 sticky top-24 hidden max-h-[calc(100dvh-120px)] min-w-0 flex-col gap-5 overflow-y-auto overscroll-contain px-1 pb-5 lg:flex'
106+
'-mx-1 sticky top-24 hidden max-h-[calc(100dvh-120px)] min-w-0 flex-col gap-5 overflow-y-auto overflow-x-hidden overscroll-contain px-1 pb-5 lg:flex'
107107
)}
108108
>
109109
<DocsSearchTrigger />

‎apps/docs/hooks/queries/search.ts‎

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { z } from 'zod'
55

66
const SEARCH_STALE_TIME = 60_000
77
const SEARCH_GC_TIME = 300_000
8+
const SEARCH_TIMEOUT_MS = 5000
89
const searchKeys = {
910
all: ['docs-search'] as const,
1011
query: (query: string) => [...searchKeys.all, query] as const,
@@ -28,9 +29,21 @@ export function useDocsSearch(query: string) {
2829
gcTime: SEARCH_GC_TIME,
2930
retry: false,
3031
queryFn: async ({ signal }) => {
31-
const response = await fetch(`/api/search?query=${encodeURIComponent(query)}`, { signal })
32-
if (!response.ok) throw new Error('Search is unavailable. Try again.')
33-
return resultsSchema.parse(await response.json())
32+
const controller = new AbortController()
33+
const abort = () => controller.abort(signal.reason)
34+
signal.addEventListener('abort', abort, { once: true })
35+
if (signal.aborted) abort()
36+
const timeout = setTimeout(() => controller.abort(), SEARCH_TIMEOUT_MS)
37+
try {
38+
const response = await fetch(`/api/search?query=${encodeURIComponent(query)}`, {
39+
signal: controller.signal,
40+
})
41+
if (!response.ok) throw new Error('Search is unavailable. Try again.')
42+
return resultsSchema.parse(await response.json())
43+
} finally {
44+
clearTimeout(timeout)
45+
signal.removeEventListener('abort', abort)
46+
}
3447
},
3548
})
3649
}

‎apps/docs/lib/search/build.ts‎

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,40 @@
1-
import type { DocsPage } from '@/lib/content/types'
1+
import { findNavigationGroup } from '@/lib/content/navigation'
2+
import type { ContentCatalog, DocsPage } from '@/lib/content/types'
23
import { pageSearchChunks } from '@/lib/search/chunks'
3-
import type { SearchIndex } from '@/lib/search/types'
4+
import type { SearchIndex, SearchPage } from '@/lib/search/types'
45
import { normalizeSearchText, searchCatalogHash } from '@/lib/search/utils'
56

67
const MAX_SEARCH_INDEX_BYTES = 64 * 1024 * 1024
8+
const MAX_SEARCH_CATALOG_BYTES = 4 * 1024 * 1024
9+
const SECTION_LABELS = { docs: 'Documentation', api: 'API reference', cli: 'CLI', mcp: 'MCP' }
10+
11+
/** Search citations need document identity and navigation, without loading document bodies. */
12+
export function buildSearchCatalog(catalog: ContentCatalog): SearchPage[] {
13+
const pages: SearchPage[] = []
14+
let bytes = 0
15+
for (const page of catalog.pages) {
16+
if (page.url === '/') continue
17+
const section = SECTION_LABELS[page.section]
18+
const group = findNavigationGroup(catalog.navigation[page.section], page.url)
19+
const entry: SearchPage = {
20+
id: page.id,
21+
url: page.url,
22+
title: page.title,
23+
contentHash: page.contentHash,
24+
headings: page.headings.map((heading) => heading.id),
25+
breadcrumbs: [
26+
section,
27+
...(group && group !== section && group !== page.title ? [group] : []),
28+
page.title,
29+
],
30+
}
31+
bytes += Buffer.byteLength(JSON.stringify(entry)) + 1
32+
if (bytes > MAX_SEARCH_CATALOG_BYTES - 2)
33+
throw new Error('Generated search catalog exceeds the 4 MiB size budget')
34+
pages.push(entry)
35+
}
36+
return pages
37+
}
738

839
/** Build-time parsing keeps the public search cold path independent of corpus Markdown size. */
940
export function buildSearchIndex(pages: DocsPage[]): SearchIndex {
Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
import 'server-only'
2-
import { getPages } from '@/lib/content/catalog'
3-
import type { SearchIndex } from '@/lib/search/types'
2+
import type { SearchIndex, SearchPage } from '@/lib/search/types'
43
import { searchCatalogHash } from '@/lib/search/utils'
54
import generatedIndex from '@/.content/search.json'
5+
import generatedCatalog from '@/.content/search-catalog.json'
66

77
const index = generatedIndex as SearchIndex
8-
if (index.version !== 1 || index.catalogHash !== searchCatalogHash(getPages()))
8+
const pages = generatedCatalog as SearchPage[]
9+
const pagesByUrl = new Map(pages.map((page) => [page.url, page]))
10+
if (index.version !== 1 || index.catalogHash !== searchCatalogHash(pages))
911
throw new Error('Search index does not match the documentation catalog. Run content:generate.')
1012

1113
/** An immutable server-only index, generated and published with the canonical catalog. */
1214
export function getSearchIndex() {
1315
return index.entries
1416
}
17+
18+
export function getSearchPage(path: string) {
19+
return pagesByUrl.get(path)
20+
}

0 commit comments

Comments
 (0)