Skip to content

Commit 3175daa

Browse files
authored
fix(search): harden assistant review and account discovery (#9478)
* fix(search): harden assistant review and account discovery * fix(search): clarify reconnect recovery and receipt states * fix(search): tighten review spacing and capture full UI states * test(search): scope filter assertion to the result link * fix(search): align account actions and simplify tool rows * chore(search): refresh permissions docs and verify receipt captures * fix(search): match tool descriptions to permission group info badges * docs(search): refresh tool permissions with description badges
1 parent 13a782b commit 3175daa

27 files changed

Lines changed: 1420 additions & 169 deletions

File tree

‎.github/workflows/checks.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,7 @@ jobs:
299299
path: |
300300
${{ runner.temp }}/e2e/**/*.json
301301
${{ runner.temp }}/e2e/browser-extension/**/*.png
302+
${{ runner.temp }}/e2e/*-ui-report.json.artifacts/*.png
302303
if-no-files-found: ignore
303304
retention-days: 7
304305

‎apps/docs/content/docs/search/connect-your-account.mdx‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ After your admin adds a source in **Settings → Sources**, connect your own acc
2222
variant="overview"
2323
/>
2424

25-
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.
25+
Your organization currently supports one connected account per provider. Open the app to see that account's permissions and connection actions.
2626

2727
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.
2828

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

4747
## Tool permissions
4848

49-
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:
49+
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:
5050

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

57-
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.
57+
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.
5858

5959
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.
6060

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

7171
## Reconnect or disconnect
7272

73-
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.
73+
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.
7474

7575
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.
7676

-22.1 KB
Loading
-4.94 KB
Loading
15 KB
Loading

‎apps/sim/app/o/[organizationId]/integrations/account-connection-actions.tsx‎

Lines changed: 50 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
'use client'
22

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

@@ -32,48 +33,57 @@ export function AccountConnectionActions({
3233
const disconnect = useDisconnectPersonalOrganizationAccount(organizationId)
3334
const [confirming, setConfirming] = useState(false)
3435
const pending = connectionPending || disconnect.isPending
36+
const actions: SettingsAction[] = []
37+
if (account) {
38+
actions.push({
39+
id: 'reconnect',
40+
text: 'Reconnect',
41+
disabled: pending || !canReconnect,
42+
onSelect: onReconnect,
43+
})
44+
}
45+
if (onConnect) {
46+
actions.push({
47+
id: 'connect',
48+
text: connectLabel,
49+
disabled: pending || !canConnect,
50+
onSelect: onConnect,
51+
})
52+
}
53+
if (account) {
54+
actions.push({
55+
id: 'delete',
56+
text: 'Disconnect',
57+
textTone: 'error',
58+
disabled: pending,
59+
onSelect: () => {
60+
disconnect.reset()
61+
setConfirming(true)
62+
},
63+
})
64+
}
3565
return (
3666
<>
67+
<SettingsActionChips actions={actions} />
3768
{account && (
38-
<Chip disabled={pending || !canReconnect} onClick={onReconnect}>
39-
Reconnect
40-
</Chip>
41-
)}
42-
{onConnect && (
43-
<Chip disabled={pending || !canConnect} onClick={onConnect}>
44-
{connectLabel}
45-
</Chip>
46-
)}
47-
{account && (
48-
<>
49-
<Chip
50-
disabled={pending}
51-
onClick={() => {
52-
disconnect.reset()
53-
setConfirming(true)
54-
}}
55-
>
56-
Disconnect
57-
</Chip>
58-
<ChipConfirmModal
59-
open={confirming}
60-
onOpenChange={(open) => {
61-
if (!disconnect.isPending) setConfirming(open)
62-
}}
63-
title={`Disconnect ${integrationName}`}
64-
text={`Disconnect ${account.displayName} from all ${integrationName} connections in this organization. Workflows using this account will also lose access. You can reconnect later.`}
65-
confirm={{
66-
label: 'Disconnect',
67-
pendingLabel: 'Disconnecting',
68-
pending: disconnect.isPending,
69-
disabled: disconnect.isPending,
70-
onClick: () =>
71-
disconnect.mutate(account.credentialId, { onSuccess: () => setConfirming(false) }),
72-
}}
73-
>
74-
<ChipModalError>{disconnect.error?.message}</ChipModalError>
75-
</ChipConfirmModal>
76-
</>
69+
<ChipConfirmModal
70+
open={confirming}
71+
onOpenChange={(open) => {
72+
if (!disconnect.isPending) setConfirming(open)
73+
}}
74+
title={`Disconnect ${integrationName}`}
75+
text={`Disconnect ${account.displayName} from all ${integrationName} connections in this organization. Workflows using this account will also lose access. You can reconnect later.`}
76+
confirm={{
77+
label: 'Disconnect',
78+
pendingLabel: 'Disconnecting',
79+
pending: disconnect.isPending,
80+
disabled: disconnect.isPending,
81+
onClick: () =>
82+
disconnect.mutate(account.credentialId, { onSuccess: () => setConfirming(false) }),
83+
}}
84+
>
85+
<ChipModalError>{disconnect.error?.message}</ChipModalError>
86+
</ChipConfirmModal>
7787
)}
7888
</>
7989
)

‎apps/sim/app/o/[organizationId]/integrations/accounts/[credentialId]/tool-permissions.tsx‎

Lines changed: 23 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client'
22

3-
import { Chip, ChipInput, ChipLink, ChipSelect, ChipSwitch, Tooltip, toast } from '@sim/emcn'
3+
import { ChipInput, ChipLink, ChipSelect, ChipSwitch, Info, toast } from '@sim/emcn'
44
import { ArrowLeft, Search } from '@sim/emcn/icons'
55
import { useQueryStates } from 'nuqs'
66
import type {
@@ -182,8 +182,6 @@ function ToolPermissionGroup({
182182
`${tool.name} ${tool.description}`.toLowerCase().includes(query)
183183
)
184184
if (tools.length === 0) return null
185-
const defaultPolicy = effect === 'read' ? data.readPolicy : data.writePolicy
186-
const defaultLabel = POLICY_OPTIONS.find((option) => option.value === defaultPolicy)?.label
187185
const groupValue = tools.every((tool) => tool.policy === tools[0].policy)
188186
? tools[0].policy
189187
: undefined
@@ -208,41 +206,29 @@ function ToolPermissionGroup({
208206
/>
209207
}
210208
>
211-
<div className='divide-y divide-[var(--border)]'>
212-
{visible.map((tool) => (
213-
<div
214-
key={tool.toolId}
215-
className='flex flex-col items-start gap-3 py-3 sm:flex-row sm:items-center sm:justify-between'
216-
>
217-
<div className='min-w-0 flex-1'>
218-
<Tooltip.Root>
219-
<Tooltip.Trigger asChild>
220-
<span className='text-[var(--text-body)] text-small'>{tool.name}</span>
221-
</Tooltip.Trigger>
222-
<Tooltip.Content>{tool.description}</Tooltip.Content>
223-
</Tooltip.Root>
224-
</div>
225-
<div className='flex max-w-full shrink-0 flex-wrap items-center gap-2'>
226-
{!tool.inherited && (
227-
<Chip
228-
aria-label={`Reset ${tool.name} to ${defaultLabel}`}
229-
disabled={pending}
230-
onClick={() => onChange({ kind: 'tool', toolId: tool.toolId, policy: null })}
231-
>
232-
Reset to {defaultLabel}
233-
</Chip>
234-
)}
235-
<ChipSwitch
236-
options={options}
237-
value={tool.policy}
238-
disabled={pending}
239-
aria-label={`${tool.name} permission`}
240-
onChange={(policy) => onChange({ kind: 'tool', toolId: tool.toolId, policy })}
241-
/>
242-
</div>
209+
{visible.map((tool) => (
210+
<div
211+
key={tool.toolId}
212+
className='flex flex-col items-start gap-3 py-3 sm:flex-row sm:items-center sm:justify-between'
213+
>
214+
<div className='flex min-w-0 flex-1 items-center gap-2'>
215+
<span className='text-[var(--text-body)] text-small'>{tool.name}</span>
216+
{tool.description && (
217+
<Info side='top' className='shrink-0'>
218+
{tool.description}
219+
</Info>
220+
)}
243221
</div>
244-
))}
245-
</div>
222+
<ChipSwitch
223+
className='max-w-full shrink-0'
224+
options={options}
225+
value={tool.policy}
226+
disabled={pending}
227+
aria-label={`${tool.name} permission`}
228+
onChange={(policy) => onChange({ kind: 'tool', toolId: tool.toolId, policy })}
229+
/>
230+
</div>
231+
))}
246232
{!visible.length && (
247233
<SettingsEmptyState variant='inline'>No matching tools</SettingsEmptyState>
248234
)}

‎apps/sim/app/o/[organizationId]/search/search.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ function OrganizationSearchContent({ userId, chatId }: OrganizationSearchProps)
248248
</nav>
249249
{(hasConversation || (activeMode === 'search' && q.trim())) && (
250250
<Chip
251-
rightIcon={Plus}
251+
leftIcon={Plus}
252252
aria-label={newLabel}
253253
onClick={startNew}
254254
disabled={isStartingNew}

‎apps/sim/components/assistant-actions/action-preview.tsx‎

Lines changed: 43 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,43 +8,52 @@ import {
88
useScrollEdges,
99
} from '@sim/emcn'
1010
import { ChevronDown, ChevronRight } from '@sim/emcn/icons'
11-
import { assistantActionPresentation } from '@/components/assistant-actions/presentation'
11+
import {
12+
assistantActionFieldPresentation,
13+
assistantActionPresentation,
14+
} from '@/components/assistant-actions/presentation'
1215
import type { AssistantActionsResponse } from '@/lib/api/contracts/assistant-actions'
1316

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

1619
interface ActionPreviewProps {
1720
action: Pick<AssistantActionsResponse['actions'][number], 'toolId' | 'name' | 'fields'>
21+
account?: string
1822
full?: boolean
1923
}
2024

2125
/** Readable content first; supplied secondary parameters remain inspectable. */
22-
export function ActionPreview({ action, full = false }: ActionPreviewProps) {
26+
export function ActionPreview({ action, account, full = false }: ActionPreviewProps) {
2327
const [expanded, setExpanded] = useState(false)
2428
const detailsId = useId()
2529
const { targets, primary, details } = assistantActionPresentation(action)
2630
return (
2731
<div className='flex min-w-0 flex-col gap-2'>
28-
{targets.length > 0 && (
29-
<dl className='flex flex-wrap gap-x-4 gap-y-1'>
32+
{(account || targets.length > 0) && (
33+
<div className='flex flex-col gap-1'>
34+
{account && (
35+
<dl className='grid grid-cols-[minmax(0,6rem)_minmax(0,1fr)] items-baseline gap-x-3 text-small'>
36+
<dt className='text-[var(--text-muted)] text-caption'>Account</dt>
37+
<dd className='min-w-0 break-all text-[var(--text-body)]'>{account}</dd>
38+
</dl>
39+
)}
3040
{targets.map((field) => (
31-
<div key={field.name} className='flex min-w-0 flex-wrap gap-x-1.5 text-small'>
32-
<dt className='text-[var(--text-muted)]'>{field.label}</dt>
33-
<dd className='whitespace-pre-wrap break-all text-[var(--text-body)]'>
34-
{field.value}
35-
</dd>
36-
</div>
41+
<PreviewField key={field.name} field={field} compact />
3742
))}
38-
</dl>
43+
</div>
44+
)}
45+
{primary.length > 0 && (
46+
<div className='flex min-w-0 flex-col gap-1 border-[var(--border)] border-t pt-2'>
47+
{primary.map((field, index) => (
48+
<PreviewField
49+
key={field.name}
50+
field={field}
51+
compact={field.format === 'text'}
52+
prominent={index === 0 && field.format === 'text'}
53+
/>
54+
))}
55+
</div>
3956
)}
40-
{primary.map((field, index) => (
41-
<PreviewField
42-
key={field.name}
43-
field={field}
44-
primary
45-
prominent={index === 0 && field.format === 'text'}
46-
/>
47-
))}
4857
{details.length > 0 && (
4958
<>
5059
{!full && (
@@ -62,7 +71,7 @@ export function ActionPreview({ action, full = false }: ActionPreviewProps) {
6271
{(expanded || full) && (
6372
<div id={detailsId} className='flex flex-col gap-2'>
6473
{details.map((field) => (
65-
<PreviewField key={field.name} field={field} />
74+
<PreviewField key={field.name} field={field} compact />
6675
))}
6776
</div>
6877
)}
@@ -74,30 +83,36 @@ export function ActionPreview({ action, full = false }: ActionPreviewProps) {
7483

7584
interface PreviewFieldProps {
7685
field: ReviewField
77-
primary?: boolean
86+
compact?: boolean
7887
prominent?: boolean
7988
}
8089

81-
function PreviewField({ field, primary = false, prominent = false }: PreviewFieldProps) {
90+
function PreviewField({ field, compact = false, prominent = false }: PreviewFieldProps) {
8291
const scroll = useRef<HTMLDivElement>(null)
8392
const edges = useScrollEdges(scroll)
84-
let value = field.value
93+
const display = assistantActionFieldPresentation(field)
94+
let value = display.value
8595
if (field.format === 'json') {
8696
try {
8797
value = JSON.stringify(JSON.parse(value), null, 2)
8898
} catch {}
8999
}
90100
return (
91-
<dl className='flex min-w-0 flex-col gap-1'>
92-
{(!primary || field.format === 'json') && (
93-
<dt className='text-[var(--text-muted)] text-caption'>{field.label}</dt>
101+
<dl
102+
className={cn(
103+
'min-w-0',
104+
compact && field.format === 'text'
105+
? 'grid grid-cols-[minmax(0,6rem)_minmax(0,1fr)] items-baseline gap-x-3'
106+
: 'flex flex-col gap-1'
94107
)}
95-
<dd>
108+
>
109+
<dt className='break-words text-[var(--text-muted)] text-caption'>{display.label}</dt>
110+
<dd className='min-w-0'>
96111
<div
97112
ref={scroll}
98113
tabIndex={edges.top || edges.bottom ? 0 : undefined}
99114
role='region'
100-
aria-label={`${field.label} preview`}
115+
aria-label={`${display.label} preview`}
101116
className={cn(
102117
'max-h-32 overflow-auto whitespace-pre-wrap break-words text-[var(--text-body)] text-small',
103118
scrollFadeClass,

0 commit comments

Comments
 (0)