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: 1 addition & 0 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,7 @@ jobs:
path: |
${{ runner.temp }}/e2e/**/*.json
${{ runner.temp }}/e2e/browser-extension/**/*.png
${{ runner.temp }}/e2e/*-ui-report.json.artifacts/*.png
if-no-files-found: ignore
retention-days: 7

Expand Down
8 changes: 4 additions & 4 deletions apps/docs/content/docs/search/connect-your-account.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ After your admin adds a source in **Settings → Sources**, connect your own acc
variant="overview"
/>

If your organization makes another connection available, choose **Add account** in the header. When several accounts are connected, select an account to see its permissions and connection actions.
Your organization currently supports one connected account per provider. Open the app to see that account's permissions and connection actions.

The connection is usable when authorization completes; no initial indexing run is required. If a provider is missing, ask an admin to add it in **Sources**. `Not configured` means its app or deployment setup needs an admin's attention.

Expand All @@ -46,15 +46,15 @@ Creating Jira sprints requires the Jira sprint permission. If your account was c

## Tool permissions

Open the app in **Integrations** and select your account. Tool permissions appear in its details when the app has supported Assistant tools available to you. Set a preference for each operation or use the **Read-only tools** and **Write tools** group controls:
Open the app in **Integrations** and select your account. Tool permissions appear in its details when the app has supported Assistant tools available to you. Hover over or focus the info button beside a tool to read its description. Set a preference for each operation or use the **Read-only tools** and **Write tools** group controls:

| Preference | Behavior |
| --- | --- |
| **Allow** | Runs without a review prompt, within your account and organization permissions. |
| **Ask** | Waits for you to review the exact proposal. |
| **Block** | Cannot run through Assistant. |

Read-only operations default to Allow; write operations default to Ask. An organization can require review even when you select Allow. A group-level Allow covers the currently listed operation versions; newly added or changed operations still ask. **Reset** restores the group's current default for that operation.
Read-only operations default to Allow; write operations default to Ask. An organization can require review even when you select Allow. A group-level Allow covers the currently listed operation versions; newly added or changed operations still ask.

These preferences belong to your account. They cannot grant access to another person's credentials or bypass an administrator's source restrictions. If a related app uses a restricted Search source, direct operations remain unavailable; Assistant can search and read within the configured scope.

Expand All @@ -70,7 +70,7 @@ Search sources and apps used for actions share the same Integrations list. An ad

## Reconnect or disconnect

Open the app and select the account, then choose **Reconnect** in the header when a grant expires, is revoked, or needs more scopes. A temporary provider outage does not necessarily require reconnecting.
Open the app and select the account, then choose **Reconnect** in the header when a grant expires, is revoked, or needs more scopes. Reconnect with the same provider account; switching identities on an existing connection is not supported, including after disconnecting. A temporary provider outage does not necessarily require reconnecting.

Choose **Disconnect** in the same header to stop future use of the grant. This can affect workflows sharing the account. Review the confirmation before removing it. Disconnecting does not remove your Sim membership.

Expand Down
Binary file modified apps/docs/public/static/search/assistant-action-review.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/docs/public/static/search/assistant-tool-permissions.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/docs/public/static/search/organization-search.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
'use client'

import { useState } from 'react'
import { Chip, ChipConfirmModal, ChipModalError } from '@sim/emcn'
import { ChipConfirmModal, ChipModalError } from '@sim/emcn'
import { type SettingsAction, SettingsActionChips } from '@/components/settings/settings-header'
import type { ViewerSearchSourceAccount } from '@/lib/api/contracts/knowledge/connectors'
import { useDisconnectPersonalOrganizationAccount } from '@/hooks/queries/organization-accounts'

Expand Down Expand Up @@ -32,48 +33,57 @@ export function AccountConnectionActions({
const disconnect = useDisconnectPersonalOrganizationAccount(organizationId)
const [confirming, setConfirming] = useState(false)
const pending = connectionPending || disconnect.isPending
const actions: SettingsAction[] = []
if (account) {
actions.push({
id: 'reconnect',
text: 'Reconnect',
disabled: pending || !canReconnect,
onSelect: onReconnect,
})
}
if (onConnect) {
actions.push({
id: 'connect',
text: connectLabel,
disabled: pending || !canConnect,
onSelect: onConnect,
})
}
if (account) {
actions.push({
id: 'delete',
text: 'Disconnect',
textTone: 'error',
disabled: pending,
onSelect: () => {
disconnect.reset()
setConfirming(true)
},
})
}
return (
<>
<SettingsActionChips actions={actions} />
{account && (
<Chip disabled={pending || !canReconnect} onClick={onReconnect}>
Reconnect
</Chip>
)}
{onConnect && (
<Chip disabled={pending || !canConnect} onClick={onConnect}>
{connectLabel}
</Chip>
)}
{account && (
<>
<Chip
disabled={pending}
onClick={() => {
disconnect.reset()
setConfirming(true)
}}
>
Disconnect
</Chip>
<ChipConfirmModal
open={confirming}
onOpenChange={(open) => {
if (!disconnect.isPending) setConfirming(open)
}}
title={`Disconnect ${integrationName}`}
text={`Disconnect ${account.displayName} from all ${integrationName} connections in this organization. Workflows using this account will also lose access. You can reconnect later.`}
confirm={{
label: 'Disconnect',
pendingLabel: 'Disconnecting',
pending: disconnect.isPending,
disabled: disconnect.isPending,
onClick: () =>
disconnect.mutate(account.credentialId, { onSuccess: () => setConfirming(false) }),
}}
>
<ChipModalError>{disconnect.error?.message}</ChipModalError>
</ChipConfirmModal>
</>
<ChipConfirmModal
open={confirming}
onOpenChange={(open) => {
if (!disconnect.isPending) setConfirming(open)
}}
title={`Disconnect ${integrationName}`}
text={`Disconnect ${account.displayName} from all ${integrationName} connections in this organization. Workflows using this account will also lose access. You can reconnect later.`}
confirm={{
label: 'Disconnect',
pendingLabel: 'Disconnecting',
pending: disconnect.isPending,
disabled: disconnect.isPending,
onClick: () =>
disconnect.mutate(account.credentialId, { onSuccess: () => setConfirming(false) }),
}}
>
<ChipModalError>{disconnect.error?.message}</ChipModalError>
</ChipConfirmModal>
)}
</>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client'

import { Chip, ChipInput, ChipLink, ChipSelect, ChipSwitch, Tooltip, toast } from '@sim/emcn'
import { ChipInput, ChipLink, ChipSelect, ChipSwitch, Info, toast } from '@sim/emcn'
import { ArrowLeft, Search } from '@sim/emcn/icons'
import { useQueryStates } from 'nuqs'
import type {
Expand Down Expand Up @@ -182,8 +182,6 @@ function ToolPermissionGroup({
`${tool.name} ${tool.description}`.toLowerCase().includes(query)
)
if (tools.length === 0) return null
const defaultPolicy = effect === 'read' ? data.readPolicy : data.writePolicy
const defaultLabel = POLICY_OPTIONS.find((option) => option.value === defaultPolicy)?.label
const groupValue = tools.every((tool) => tool.policy === tools[0].policy)
? tools[0].policy
: undefined
Expand All @@ -208,41 +206,29 @@ function ToolPermissionGroup({
/>
}
>
<div className='divide-y divide-[var(--border)]'>
{visible.map((tool) => (
<div
key={tool.toolId}
className='flex flex-col items-start gap-3 py-3 sm:flex-row sm:items-center sm:justify-between'
>
<div className='min-w-0 flex-1'>
<Tooltip.Root>
<Tooltip.Trigger asChild>
<span className='text-[var(--text-body)] text-small'>{tool.name}</span>
</Tooltip.Trigger>
<Tooltip.Content>{tool.description}</Tooltip.Content>
</Tooltip.Root>
</div>
<div className='flex max-w-full shrink-0 flex-wrap items-center gap-2'>
{!tool.inherited && (
<Chip
aria-label={`Reset ${tool.name} to ${defaultLabel}`}
disabled={pending}
onClick={() => onChange({ kind: 'tool', toolId: tool.toolId, policy: null })}
>
Reset to {defaultLabel}
</Chip>
)}
<ChipSwitch
options={options}
value={tool.policy}
disabled={pending}
aria-label={`${tool.name} permission`}
onChange={(policy) => onChange({ kind: 'tool', toolId: tool.toolId, policy })}
/>
</div>
{visible.map((tool) => (
<div
key={tool.toolId}
className='flex flex-col items-start gap-3 py-3 sm:flex-row sm:items-center sm:justify-between'
>
<div className='flex min-w-0 flex-1 items-center gap-2'>
<span className='text-[var(--text-body)] text-small'>{tool.name}</span>
{tool.description && (
<Info side='top' className='shrink-0'>
{tool.description}
</Info>
)}
</div>
))}
</div>
<ChipSwitch
className='max-w-full shrink-0'
options={options}
value={tool.policy}
disabled={pending}
aria-label={`${tool.name} permission`}
onChange={(policy) => onChange({ kind: 'tool', toolId: tool.toolId, policy })}
/>
</div>
))}
{!visible.length && (
<SettingsEmptyState variant='inline'>No matching tools</SettingsEmptyState>
)}
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/app/o/[organizationId]/search/search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ function OrganizationSearchContent({ userId, chatId }: OrganizationSearchProps)
</nav>
{(hasConversation || (activeMode === 'search' && q.trim())) && (
<Chip
rightIcon={Plus}
leftIcon={Plus}
aria-label={newLabel}
onClick={startNew}
disabled={isStartingNew}
Expand Down
71 changes: 43 additions & 28 deletions apps/sim/components/assistant-actions/action-preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,43 +8,52 @@ import {
useScrollEdges,
} from '@sim/emcn'
import { ChevronDown, ChevronRight } from '@sim/emcn/icons'
import { assistantActionPresentation } from '@/components/assistant-actions/presentation'
import {
assistantActionFieldPresentation,
assistantActionPresentation,
} from '@/components/assistant-actions/presentation'
import type { AssistantActionsResponse } from '@/lib/api/contracts/assistant-actions'

type ReviewField = AssistantActionsResponse['actions'][number]['fields'][number]

interface ActionPreviewProps {
action: Pick<AssistantActionsResponse['actions'][number], 'toolId' | 'name' | 'fields'>
account?: string
full?: boolean
}

/** Readable content first; supplied secondary parameters remain inspectable. */
export function ActionPreview({ action, full = false }: ActionPreviewProps) {
export function ActionPreview({ action, account, full = false }: ActionPreviewProps) {
const [expanded, setExpanded] = useState(false)
const detailsId = useId()
const { targets, primary, details } = assistantActionPresentation(action)
return (
<div className='flex min-w-0 flex-col gap-2'>
{targets.length > 0 && (
<dl className='flex flex-wrap gap-x-4 gap-y-1'>
{(account || targets.length > 0) && (
<div className='flex flex-col gap-1'>
{account && (
<dl className='grid grid-cols-[minmax(0,6rem)_minmax(0,1fr)] items-baseline gap-x-3 text-small'>
<dt className='text-[var(--text-muted)] text-caption'>Account</dt>
<dd className='min-w-0 break-all text-[var(--text-body)]'>{account}</dd>
</dl>
)}
{targets.map((field) => (
<div key={field.name} className='flex min-w-0 flex-wrap gap-x-1.5 text-small'>
<dt className='text-[var(--text-muted)]'>{field.label}</dt>
<dd className='whitespace-pre-wrap break-all text-[var(--text-body)]'>
{field.value}
</dd>
</div>
<PreviewField key={field.name} field={field} compact />
))}
</dl>
</div>
)}
{primary.length > 0 && (
<div className='flex min-w-0 flex-col gap-1 border-[var(--border)] border-t pt-2'>
{primary.map((field, index) => (
<PreviewField
key={field.name}
field={field}
compact={field.format === 'text'}
prominent={index === 0 && field.format === 'text'}
/>
))}
</div>
)}
{primary.map((field, index) => (
<PreviewField
key={field.name}
field={field}
primary
prominent={index === 0 && field.format === 'text'}
/>
))}
{details.length > 0 && (
<>
{!full && (
Expand All @@ -62,7 +71,7 @@ export function ActionPreview({ action, full = false }: ActionPreviewProps) {
{(expanded || full) && (
<div id={detailsId} className='flex flex-col gap-2'>
{details.map((field) => (
<PreviewField key={field.name} field={field} />
<PreviewField key={field.name} field={field} compact />
))}
</div>
)}
Expand All @@ -74,30 +83,36 @@ export function ActionPreview({ action, full = false }: ActionPreviewProps) {

interface PreviewFieldProps {
field: ReviewField
primary?: boolean
compact?: boolean
prominent?: boolean
}

function PreviewField({ field, primary = false, prominent = false }: PreviewFieldProps) {
function PreviewField({ field, compact = false, prominent = false }: PreviewFieldProps) {
const scroll = useRef<HTMLDivElement>(null)
const edges = useScrollEdges(scroll)
let value = field.value
const display = assistantActionFieldPresentation(field)
let value = display.value
if (field.format === 'json') {
try {
value = JSON.stringify(JSON.parse(value), null, 2)
} catch {}
}
return (
<dl className='flex min-w-0 flex-col gap-1'>
{(!primary || field.format === 'json') && (
<dt className='text-[var(--text-muted)] text-caption'>{field.label}</dt>
<dl
className={cn(
'min-w-0',
compact && field.format === 'text'
? 'grid grid-cols-[minmax(0,6rem)_minmax(0,1fr)] items-baseline gap-x-3'
: 'flex flex-col gap-1'
)}
<dd>
>
<dt className='break-words text-[var(--text-muted)] text-caption'>{display.label}</dt>
<dd className='min-w-0'>
<div
ref={scroll}
tabIndex={edges.top || edges.bottom ? 0 : undefined}
role='region'
aria-label={`${field.label} preview`}
aria-label={`${display.label} preview`}
className={cn(
'max-h-32 overflow-auto whitespace-pre-wrap break-words text-[var(--text-body)] text-small',
scrollFadeClass,
Expand Down
Loading
Loading