Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 0 additions & 1 deletion apps/docs/content/docs/search/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
"find-content",
"ask-questions",
"sim-links",
"people",
"troubleshooting",
"---Connect sources---",
"connect-your-account",
Expand Down
43 changes: 0 additions & 43 deletions apps/docs/content/docs/search/people.mdx

This file was deleted.

6 changes: 3 additions & 3 deletions apps/docs/content/docs/search/sim-links.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: Short sim/ links that take everyone in your organization to the rig

Sim Links are short names for pages your team opens often, like `sim/handbook` or `sim/okta`. Every member of your organization can use them, with or without a Search seat.

Open **Sim Links** in the organization sidebar to see your organization's links. Search by name, destination, or description, or use **Sort → Opens** to see the most used links.
Open **Sim Links** in the organization sidebar to see your organization's links. Search by name, destination, or description, or use **Sort → Opens** to see the 50 most opened links.

## Open a link

Expand All @@ -15,7 +15,7 @@ Names ignore case and punctuation, so `sim/Team-Handbook` and `sim/teamhandbook`

## Open links from the address bar

With the Sim Chrome extension, type `sim/handbook` in the address bar. To find a link, type `sim`, press **Space**, and start typing its name: matching links appear as you type, most opened first.
With the Sim Chrome extension, type `sim/handbook` in the address bar. To find a link, type `sim`, press **Space**, and start typing its name: matching links appear as you type, most opened first. Unlisted links aren't suggested.

Without the extension, add Sim as a Chrome site search shortcut to type `sim handbook` instead:

Expand Down Expand Up @@ -52,7 +52,7 @@ Values fill each `{*}` in order, and the last `{*}` takes the rest. A link with
By default, any member can edit or delete a link. The link's creator and organization admins can change two options under **Access**:

- **Hide from the Sim Links page**: only the creator and admins see the link in the list, marked **Unlisted**. Anyone with the name can still open it.
- **Only you and admins can edit**: only the creator and admins can edit or delete the link, marked **Restricted**.
- **Only the creator and admins can edit**: only the creator and admins can edit or delete the link, marked **Restricted**.

Changes to links are recorded in your organization's [audit logs](/platform/enterprise/audit-logs).

Expand Down
10 changes: 8 additions & 2 deletions apps/extension/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,14 @@ other workspace resources or browser-control tools.
- Other HTTP(S) origins are optional grants requested for the current site from a
user gesture. Each grant can be revoked. Keeping the panel open is not a grant
to read every subsequently selected tab.
- No history, bookmarks, cookie API, debugger, network interception, native
messaging, external messaging, or arbitrary remote code execution.
- No history, bookmarks, cookie API, debugger, native messaging, external messaging,
or arbitrary remote code execution. The only network interception is the static
`sim/` redirect rule above.
- The only background network request is the `sim` address-bar keyword: after
`sim` and Space, the text typed before the first `/` or `?` is sent to the
configured Sim origin with the signed-in session to suggest matching Sim links,
unless it contains a space (a search) or is longer than a link name can be. Page
content is never sent, and unlisted links are never suggested.
- No always-running page reader. Extraction is tied to an explicit attachment action.
- Page bytes are not written to extension local/sync storage. Temporary context-menu
handoff uses trusted-context session storage, expiration and navigation cleanup.
Expand Down
2 changes: 1 addition & 1 deletion apps/extension/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ SIM_EXTENSION_DEV=1 SIM_EXTENSION_ORIGIN=http://localhost:3000 bun run --cwd app

- Required capabilities: `activeTab`, `scripting`, `sidePanel`, `storage`, `contextMenus`, `declarativeNetRequestWithHostAccess`, and access to the configured Sim origin and to `http(s)://sim/*`.
- Typing `sim/<name>` in the address bar opens the organization's [Sim link](https://docs.sim.ai/search/sim-links). A single static redirect rule rewrites top-level navigations to `http(s)://sim/*` to `<Sim origin>/sim/*` with the path and query unchanged. The `sim` host permission exists only for that rule: every navigation to that host is redirected before it loads, and no other request is matched or observed. An organization that already serves an intranet host named `sim` should not install this build, since its links would be redirected to Sim.
- The `sim` address-bar keyword (`sim`, then Space) lists matching Sim links while you type and opens what you enter through the same resolver. It is the extension's only network request: the background worker sends the typed text, and nothing else, to `<Sim origin>/api/sim-links/suggestions` with the signed-in session, and the extension's content security policy allows connections to that origin alone. Unlisted links are never suggested.
- The `sim` address-bar keyword (`sim`, then Space) lists matching Sim links while you type and opens what you enter through the same resolver. It is the extension's only network request: the background worker sends the text typed before the first `/` or `?`, and nothing else, unless that text contains a space (a search) or is longer than a link name can be, to `<Sim origin>/api/sim-links/suggestions` with the signed-in session, and the extension's content security policy allows connections to that origin alone. Unlisted links are never suggested.
- Optional HTTP/HTTPS site access is requested only by a menu action naming that site. Chrome's `activeTab` access is temporary and does not follow arbitrary new sites.
- Page capture runs on demand in Chrome's isolated world. It excludes editable fields, inputs, hidden elements, scripts, and embedded documents. It reads only the top frame. PDF viewers and canvas documents may have no accessible text.
- Extraction stops at 50,000 text characters, 150,000 serialized UTF-8 bytes, 15,000 visited nodes, or 150ms. The composer exposes truncation.
Expand Down
2 changes: 1 addition & 1 deletion apps/extension/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@sim/extension",
"version": "0.1.1",
"version": "0.1.2",
"private": true,
"type": "module",
"license": "Apache-2.0",
Expand Down
4 changes: 2 additions & 2 deletions apps/extension/scripts/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,9 @@ await writeFile(
{
manifest_version: 3,
name: development ? 'Sim (Development)' : 'Sim',
version: '0.1.1',
version: '0.1.2',
description:
'Ask Sim about the page you are reading, with your existing Sim conversations and tools.',
"Ask Sim about the page you are reading, and open your organization's sim/ links from the address bar.",
minimum_chrome_version: '120',
permissions: [
'activeTab',
Expand Down
66 changes: 50 additions & 16 deletions apps/extension/src/omnibox.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
SIM_LINK_NAME_MAX_LENGTH,
SIM_LINK_SUGGESTIONS_PATH,
simLinkSuggestionsSchema,
} from '@sim/browser-extension-protocol'
Expand All @@ -9,8 +10,8 @@ import { SIM_ORIGIN } from '@/config'

const logger = createLogger('BrowserExtensionOmnibox')

/** Long enough to skip the requests a fast typist's intermediate keystrokes would make. */
const SUGGEST_DELAY_MS = 120
/** Longer than the gap between a typist's keystrokes, so a name is looked up once it settles. */
const SUGGEST_DELAY_MS = 220

/** A Sim link as suggested: its name, and what it shows beside the name. */
interface SuggestedLink {
Expand All @@ -20,42 +21,68 @@ interface SuggestedLink {

/**
* What was typed after the `sim` keyword, split where the name ends: `sim/jira/ABC-1?tab=all`
* (the `sim/` is optional) has the name `jira` and the rest `/ABC-1?tab=all`. Only the space
* before the name and the prefix are dropped; values and the query stay exactly as typed.
* (the `sim/` is optional) has the name `jira` and the rest `/ABC-1?tab=all`. Surrounding spaces,
* the prefix, leading slashes, and any `#fragment` are dropped, as the address bar would; values
* and the query otherwise stay exactly as typed.
*/
function readTyped(text: string): { name: string; rest: string } {
const typed = text.trimStart().replace(/^sim\//i, '')
const typed = text
.trim()
Comment thread
waleedlatif1 marked this conversation as resolved.
Resolved
.replace(/^sim\//i, '')
.replace(/^\/+/, '')
.replace(/#.*$/, '')
const nameEnd = typed.search(/[/?]/)
return nameEnd === -1
? { name: typed, rest: '' }
: { name: typed.slice(0, nameEnd), rest: typed.slice(nameEnd) }
}

/** Suggestion text is XML, where these five characters are markup. */
const LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g

/** Code points XML forbids even as references, besides lone surrogates. */
const NON_XML_CHARACTER = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\uFFFE\uFFFF]/g

/**
* Suggestion text is XML, where these five characters are markup and some code points aren't
* allowed at all, so one bad description can't make Chrome drop every suggestion.
*/
function escapeXml(text: string): string {
return text.replace(/[&<>"']/g, (character) => `&#${character.charCodeAt(0)};`)
return text
.replace(NON_XML_CHARACTER, '')
.replace(LONE_SURROGATE, '')
.replace(/[&<>"']/g, (character) => `&#${character.charCodeAt(0)};`)
}

/** `encodeURIComponent`, except a lone surrogate (which it refuses) is dropped. */
function encodeSegment(segment: string): string {
return encodeURIComponent(segment.replace(LONE_SURROGATE, ''))
}

/**
* The resolver address for what was typed, as the address bar would send `sim/<typed>`: each
* `/` starts a value, kept in position even when empty, and a trailing `/` adds none.
* `/` starts a value, kept in position even when empty, and a trailing `/` adds none. The query
* is passed on as typed.
*/
function simLinkUrl(text: string): string {
const { name, rest } = readTyped(text)
const queryStart = rest.indexOf('?')
const path = `${name}${queryStart === -1 ? rest : rest.slice(0, queryStart)}`.replace(/\/$/, '')
const query = queryStart === -1 ? '' : new URLSearchParams(rest.slice(queryStart + 1)).toString()
const segments = path ? path.split('/').map(encodeURIComponent) : []
return `${SIM_ORIGIN}/${['sim', ...segments].join('/')}${query ? `?${query}` : ''}`
const segments = path ? path.split('/').map(encodeSegment) : []
const url = new URL(`${SIM_ORIGIN}/${['sim', ...segments].join('/')}`)
if (queryStart !== -1) url.search = rest.slice(queryStart + 1)
return url.toString()
}

async function fetchSuggestedLinks(name: string, signal: AbortSignal): Promise<SuggestedLink[]> {
/** The links Sim suggests for `name`, or `null` when it couldn't answer (signed out, rate limited). */
async function fetchSuggestedLinks(
name: string,
signal: AbortSignal
): Promise<SuggestedLink[] | null> {
const response = await fetch(
`${SIM_ORIGIN}${SIM_LINK_SUGGESTIONS_PATH}?${new URLSearchParams({ q: name })}`,
{ credentials: 'include', signal }
)
// Signed out, or Sim links are unavailable: the typed link still opens through the resolver.
if (!response.ok) return []
if (!response.ok) return null
const { links } = simLinkSuggestionsSchema.parse(await response.json())
return links.map((link) => ({ name: link.name, detail: link.description ?? link.url }))
}
Expand Down Expand Up @@ -91,6 +118,11 @@ export function registerSimLinkOmnibox() {
chrome.omnibox.setDefaultSuggestion({
description: `Open <match>sim/${escapeXml(`${name}${rest}`)}</match>`,
})
// A name can't hold a space, so `sim card prices` is a search, not a link: ask Sim nothing.
if (/\s/.test(name) || name.length > SIM_LINK_NAME_MAX_LENGTH) {
suggest([])
return
}
const key = name.toLowerCase()
const known = linksByName.get(key)
if (known) {
Expand All @@ -102,9 +134,11 @@ export function registerSimLinkOmnibox() {
void (async () => {
await sleep(SUGGEST_DELAY_MS)
if (controller.signal.aborted) return
// Signed out or rate limited: the typed link still opens through the resolver, and the name
// is asked about again on the next keystroke rather than remembered as having no links.
const links = await fetchSuggestedLinks(key, controller.signal)
linksByName.set(key, links)
if (!controller.signal.aborted) suggest(toSuggestions(links, rest))
if (links) linksByName.set(key, links)
if (!controller.signal.aborted) suggest(toSuggestions(links ?? [], rest))
})().catch(() => {
if (!controller.signal.aborted) logger.warn('Could not load Sim link suggestions')
})
Expand Down
5 changes: 3 additions & 2 deletions apps/sim/app/api/favicon/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ export const GET = defineInternalBinaryRoute({
operation: getFavicon.operation,
rateLimit: internalRateLimits.user({
bucketName: 'favicon',
/** A page can render dozens of icons at once; the browser caches each one after its first load. */
config: { maxTokens: 300, refillRate: 300, refillIntervalMs: 60_000 },
// A table can show hundreds of sites at once; requests that reach the icon providers are
// budgeted separately (favicon use case), so this only bounds cheap cached reads.
config: { maxTokens: 1200, refillRate: 1200, refillIntervalMs: 60_000 },
}),
errorPolicy: internalOrchestrationErrorPolicy,
mapInput: ({ query }) => ({ host: query.host }),
Expand Down
17 changes: 5 additions & 12 deletions apps/sim/app/o/[organizationId]/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { dehydrate, HydrationBoundary } from '@tanstack/react-query'
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
Expand Down Expand Up @@ -66,19 +65,13 @@ export default async function OrganizationLayout({
if (!context) {
return <OrganizationAccessDenied />
}
const peopleEnabledLookup = canBrowsePeople(organizationId, context.viewer.role).catch(
(error) => {
logger.warn('Could not resolve People navigation access', {
organizationId,
error: getErrorMessage(error),
})
return false
}
)
const peopleEnabledLookup = canBrowsePeople(organizationId, context.viewer.role)
const simLinksEnabledLookup = isSimLinksEnabled(organizationId)
if (
!context.mothershipAvailable &&
!context.searchAccess.memberScoped &&
!(await peopleEnabledLookup)
!(await peopleEnabledLookup) &&
!(await simLinksEnabledLookup)
Comment thread
waleedlatif1 marked this conversation as resolved.
Resolved
)
redirect(WORKSPACE_SETTINGS_PATH)

Expand Down Expand Up @@ -110,7 +103,7 @@ export default async function OrganizationLayout({
isWorkflowTestsEnabled(organizationId),
isChangelogEnabled(organizationId),
isProjectsEnabled(),
isSimLinksEnabled(organizationId),
simLinksEnabledLookup,
peopleEnabledLookup,
])
const initialSidebarCollapsed = cookieStore.get('sidebar_collapsed')?.value === '1'
Expand Down
4 changes: 2 additions & 2 deletions apps/sim/app/o/[organizationId]/links/search-params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ const simLinkViewParsers = { create: parseAsString } as const

export const serializeSimLinkView = createSerializer(simLinkViewParsers)

/** `search` filters by name, destination, or description; only its URL write is debounced. */
/** `search` filters by name, destination, or description. */
export const simLinksParsers = {
...simLinkViewParsers,
search: parseAsString.withDefault(''),
} as const

/** Shared `sort` + `dir` params for the Sim Links list. Default: alphabetical. */
/** Shared `sort` + `dir` params for the Sim Links list. */
export const simLinksSortParams = createSortParams(SIM_LINK_SORTS, {
column: 'name',
direction: 'asc',
Expand Down
7 changes: 4 additions & 3 deletions apps/sim/app/o/[organizationId]/links/sim-link-modal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use client'

import { useId, useState } from 'react'
import { SIM_LINK_NAME_MAX_LENGTH } from '@sim/browser-extension-protocol'
import {
Checkbox,
ChipModal,
Expand All @@ -10,12 +11,12 @@ import {
ChipModalFooter,
ChipModalHeader,
} from '@sim/emcn'
import { SIM_DOCS_URL } from '@sim/utils/site'
import { isApiClientError } from '@/lib/api/client/errors'
import type { SimLink, SimLinkBody } from '@/lib/api/contracts/sim-links'
import {
isVariableSimLinkUrl,
SIM_LINK_DESCRIPTION_MAX_LENGTH,
SIM_LINK_NAME_MAX_LENGTH,
SIM_LINK_VARIABLE,
simLinkFieldsSchema,
toSimLinkName,
Expand All @@ -24,7 +25,7 @@ import { useCreateSimLink, useUpdateSimLink } from '@/hooks/queries/sim-links'

type Field = 'name' | 'url' | 'defaultUrl' | 'description'

const SIM_LINKS_DOCS_URL = 'https://docs.sim.ai/search/sim-links'
const SIM_LINKS_DOCS_URL = `${SIM_DOCS_URL}/search/sim-links`

function openDocs() {
window.open(SIM_LINKS_DOCS_URL, '_blank', 'noopener,noreferrer')
Expand Down Expand Up @@ -173,7 +174,7 @@ export function SimLinkModal({ organizationId, onClose, link, initialName }: Sim
onCheckedChange={(checked) => change('restrictEdit', checked === true)}
/>
<span className='text-[var(--text-primary)] text-small'>
Only you and admins can edit
Only the creator and admins can edit
</span>
</label>
</div>
Expand Down
Loading
Loading