Skip to content

Commit dff74b6

Browse files
authored
fix(search): unify navigation and preserve action review history (#9467)
* fix(search): unify navigation and preserve action review history * fix(search): preserve current view and uncertain action outcomes
1 parent 9e2afe0 commit dff74b6

61 files changed

Lines changed: 3123 additions & 687 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/docs/content/docs/search/ask-questions.mdx‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -127,19 +127,21 @@ Copy the reviewed draft into another app, or ask Assistant to create it using yo
127127

128128
When integration actions are enabled, Assistant can answer questions and take supported actions in the same conversation. For example, ask it to draft an email in Gmail or create a Jira issue using your own connected account.
129129

130-
Reads run without a prompt by default. Writes wait for review by default. Each proposal appears inline in the conversation with its app, account, destination, and proposed content. Edit the supported fields, then select **Allow** or **Cancel** for each action. After several proposals, **Allow N shown** or **Cancel N shown** applies your choice to that displayed batch. You can approve individual actions first, then cancel the remaining batch.
130+
Reads run without a prompt by default. Writes wait for review by default. Each proposal appears inline in the conversation with its app, account, destination, and proposed content. Select **Edit** to change supported fields, then choose **Allow** or **Cancel** for each action. After several proposals, **Allow N shown** or **Cancel N shown** applies your choice to that displayed batch. You can approve individual actions first, then cancel the remaining batch.
131131

132132
<Image
133133
src="/static/search/assistant-action-review.png"
134-
alt="Inline Assistant action review showing the connected Jira account, exact site and project, editable issue summary and description, and Allow or Cancel controls"
135-
width={705}
136-
height={489}
134+
alt="Compact inline Jira proposals showing the account, destination, proposed content, Edit, Allow, and Cancel"
135+
width={1440}
136+
height={1000}
137137
variant="detail"
138138
/>
139139

140+
Decisions remain in the conversation as expandable receipts after reload. **Approved** means review is complete; the status changes to **Running**, **Completed**, or **Failed** as execution proceeds. **Cancelled** records a skipped proposal. **Outcome unknown** means you should check the original app before repeating the action.
141+
140142
Each decision applies to that proposal. Changing its account or destination requires a new proposal. An action can still fail if your account is disconnected or an administrator changes access before execution. If the provider outcome is unknown, check the original app before asking Assistant to repeat a write.
141143

142-
Configure persistent **Allow**, **Ask**, or **Block** preferences under **Integrations → ⋯ → Tool permissions** for your account. See [tool permissions](/search/connect-your-account#tool-permissions).
144+
Configure persistent **Allow**, **Ask**, or **Block** preferences under **Integrations → app → account**. See [tool permissions](/search/connect-your-account#tool-permissions).
143145

144146
## Start from a document
145147

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

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,19 @@ After your admin adds a source in **Settings → Sources**, connect your own acc
1010
## Connect and verify
1111

1212
1. Join the Sim organization through its invitation or SSO. Use the verified email required by the provider connection.
13-
2. Open **Integrations**, find the provider, and choose **Connect** from its **⋯** menu.
13+
2. Open **Integrations**, select the app, and choose **Connect** in its header.
1414
3. Complete authorization in the new tab. If the popup is blocked, allow it and select **Connect** again.
1515
4. Return to **Search** or **Search → Assistant** and search for content your account can open. Read a result and follow its source link.
1616

1717
<Image
18-
src="/static/search/connect-account-menu.png"
19-
alt="The Confluence menu in Integrations with Connect available before an account has been authorized"
18+
src="/static/search/connect-account-details.png"
19+
alt="Coda integration details with Connect in the resource header and no account connected"
2020
width={1440}
2121
height={1000}
2222
variant="overview"
2323
/>
2424

25-
If your organization makes another connection available, choose **Add account** from the same menu.
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.
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

@@ -38,15 +38,15 @@ You do not choose repositories, channels, labels, or an indexing method during p
3838
| Confluence service account | Connect Confluence. Both your account and the service connection must read the selected content. |
3939
| Coda | Connect personal Coda MCP. Service mode also verifies the parent document with its REST token. |
4040
| GitLab | No personal connection. Your verified Sim email must match current GitLab identity and permissions, or the admin's CSV mapping and project grant. |
41-
| [Generic Secrets, Member mode](/search/generic-secrets) | Select **Manage secrets** in Integrations and save your own values for organization Home conversations. |
41+
| [Generic Secrets, Member mode](/search/generic-secrets) | Open **Generic Secrets** in Integrations and save your own values for organization Home conversations. |
4242

4343
Gmail, Calendar, and Drive have separate grants. Connecting one does not authorize the others. Except for GitLab's permission model, a service account restricts resources rather than replacing your personal connection.
4444

4545
Creating Jira sprints requires the Jira sprint permission. If your account was connected before sprint actions were added, reconnect it to grant that permission. Existing Jira operations continue to use their existing permissions.
4646

4747
## Tool permissions
4848

49-
When Assistant integration actions are enabled, open the app’s **⋯** menu in **Integrations** and select **Tool permissions** for your connected account. This option appears 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+
When Assistant integration actions are enabled, 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:
5050

5151
| Preference | Behavior |
5252
| --- | --- |
@@ -66,13 +66,13 @@ These preferences belong to your account. They cannot grant access to another pe
6666
variant="overview"
6767
/>
6868

69-
Apps used only for actions appear under **Assistant apps**. An admin makes the app available in organization source settings, then each member connects their own account in Integrations.
69+
Search sources and apps used for actions share the same Integrations list. An admin makes the app available in organization source settings, then each member connects their own account. Configured apps remain visible when disabled; their connection actions are unavailable, and existing accounts can still be inspected or disconnected.
7070

7171
## Reconnect or disconnect
7272

73-
Choose **Reconnect** from the app’s **⋯** menu 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. A temporary provider outage does not necessarily require reconnecting.
7474

75-
Choose **Disconnect** from the same menu 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.
75+
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

7777
For an account-connection request, sign in as the verified Sim user associated with the requested email, then authorize the provider. The request does not invite you into the organization; membership and Search access are still required.
7878

73.7 KB
Loading
3.52 KB
Loading
38.7 KB
Loading
-80.8 KB
Binary file not shown.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { listAssistantActionReceiptsContract } from '@/lib/api/contracts/assistant-actions'
2+
import {
3+
defineInternalJsonRoute,
4+
internalOrchestrationErrorPolicy,
5+
internalRateLimits,
6+
internalSessionAuth,
7+
} from '@/lib/api/server/routes'
8+
import { assistantActionOperations } from '@/lib/assistant-actions/application/operations'
9+
import { listOrganizationAssistantActionReceipts } from '@/lib/assistant-actions/application/receipts'
10+
11+
export const GET = defineInternalJsonRoute({
12+
contract: listAssistantActionReceiptsContract,
13+
auth: internalSessionAuth,
14+
operation: assistantActionOperations.receipts,
15+
rateLimit: internalRateLimits.user({
16+
bucketName: 'assistant-action-receipts',
17+
config: { maxTokens: 120, refillRate: 120, refillIntervalMs: 60_000 },
18+
}),
19+
errorPolicy: internalOrchestrationErrorPolicy,
20+
mapInput: ({ params, query }) => ({ organizationId: params.id, chatId: params.chatId, ...query }),
21+
useCase: listOrganizationAssistantActionReceipts,
22+
staticResponseHeaders: { 'Cache-Control': 'private, no-store' },
23+
})

‎apps/sim/app/o/[organizationId]/components/organization-conversation.tsx‎

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

3-
import { useCallback, useEffect, useState } from 'react'
3+
import { type ReactNode, useCallback, useEffect, useRef, useState } from 'react'
44
import { toast } from '@sim/emcn'
55
import { toRecord } from '@sim/utils/object'
66
import { useQueryClient } from '@tanstack/react-query'
7-
import { useQueryStates } from 'nuqs'
7+
import { createSerializer, useQueryStates } from 'nuqs'
88
import { AssistantActionReviewProvider } from '@/components/assistant-actions'
99
import { requestJson } from '@/lib/api/client/request'
1010
import type { WorkspaceSearchFilters } from '@/lib/api/contracts/knowledge'
@@ -16,6 +16,7 @@ import {
1616
getMothershipAttachmentUrl,
1717
} from '@/lib/mothership/chat/attachment-preview'
1818
import { createSearchResource } from '@/lib/mothership/resources/search'
19+
import { organizationRoutes } from '@/lib/navigation/paths'
1920
import { OrganizationLanding } from '@/app/o/[organizationId]/components/organization-landing'
2021
import { Composer } from '@/app/o/[organizationId]/home/components/composer'
2122
import { GetStarted } from '@/app/o/[organizationId]/home/components/get-started'
@@ -41,6 +42,7 @@ import {
4142
import { useRestoredChatEntry } from '@/app/workspace/[workspaceId]/home/hooks/use-restored-chat-entry'
4243
import { fetchWorkspaceResourceRef } from '@/app/workspace/[workspaceId]/home/resolve-resource-ref'
4344
import {
45+
resourceParam,
4446
searchFilterParsers,
4547
searchFiltersFromParams,
4648
} from '@/app/workspace/[workspaceId]/home/search-params'
@@ -57,10 +59,26 @@ import { useHydrated } from '@/hooks/use-hydrated'
5759
import { useMothershipDraftsStore } from '@/stores/mothership-drafts/store'
5860
import type { ChatContext } from '@/stores/panel'
5961

62+
export interface OrganizationSummaryRequest {
63+
id: string
64+
message: string
65+
assistantSearch: WorkspaceSearchFilters
66+
}
67+
68+
const serializeSearchConversation = createSerializer({
69+
...organizationSearchParsers,
70+
...searchFilterParsers,
71+
[resourceParam.key]: resourceParam.parser,
72+
})
73+
6074
interface OrganizationConversationProps {
75+
active?: boolean
76+
summaryRequest?: OrganizationSummaryRequest
77+
onSummaryConsumed?: (id: string) => void
6178
userName?: string
6279
chatId?: string
6380
requestMode: ChatRequestMode
81+
header?: ReactNode
6482
}
6583

6684
/** Shared conversation surface; the destination fixes its intent before the first turn. */
@@ -74,7 +92,8 @@ export function OrganizationConversation(props: OrganizationConversationProps) {
7492
(props.requestMode === 'assistant' ? !searchAccess.memberScoped : !canBuild)
7593
)
7694
return null
77-
if (isRestoredChatEntry || !isClient || !session?.user?.id) return <HomeFallback />
95+
if (isRestoredChatEntry || !isClient || !session?.user?.id)
96+
return <HomeFallback header={props.header} />
7897
return (
7998
<OrganizationConversationContent
8099
key={`${session.user.id}:${organization.id}:${props.chatId ?? 'new'}:${props.requestMode}`}
@@ -87,6 +106,10 @@ function OrganizationConversationContent({
87106
userName,
88107
chatId,
89108
requestMode: destinationMode,
109+
header,
110+
active = true,
111+
summaryRequest,
112+
onSummaryConsumed,
90113
}: OrganizationConversationProps) {
91114
const { organization, canBuild } = useOrganizationContext()
92115
const { data: session } = useSession()
@@ -95,17 +118,27 @@ function OrganizationConversationContent({
95118
const [planning, setPlanning] = useState(false)
96119
const requestMode =
97120
destinationMode === 'agent' && !chatId && planEnabled && planning ? 'plan' : destinationMode
98-
const [{ q, source, updated, from, to }] = useQueryStates(
121+
const [searchParams] = useQueryStates(
99122
{ ...organizationSearchParsers, ...searchFilterParsers },
100123
organizationSearchUrlKeys
101124
)
125+
const { q, source, updated, from, to } = searchParams
102126
const controller = useResourcePanelController()
103127
const queryClient = useQueryClient()
104128
const chat = useChat(
105129
{ organizationId: organization.id },
106130
chatId,
107131
getMothershipUseChatOptions({
108132
requestMode,
133+
getChatUrl:
134+
destinationMode === 'assistant'
135+
? (id) =>
136+
serializeSearchConversation(organizationRoutes(organization.id).searchChat(id), {
137+
...searchParams,
138+
mode: active ? 'assistant' : 'search',
139+
resource: controller.activeResourceState[0],
140+
})
141+
: undefined,
109142
onResourceEvent: controller.onResourceEvent,
110143
activeResourceState: controller.activeResourceState,
111144
})
@@ -240,9 +273,21 @@ function OrganizationConversationContent({
240273
const firstName = userName?.split(' ')[0] ?? ''
241274

242275
useEffect(() => {
243-
if (chat.resolvedChatId && !chat.isSending && !chat.isReconnecting)
276+
if (active && chat.resolvedChatId && !chat.isSending && !chat.isReconnecting)
244277
markRead(chat.resolvedChatId)
245-
}, [chat.resolvedChatId, chat.isSending, chat.isReconnecting, markRead])
278+
}, [active, chat.resolvedChatId, chat.isSending, chat.isReconnecting, markRead])
279+
280+
const consumedSummary = useRef<string | null>(null)
281+
useEffect(() => {
282+
if (!active || !summaryRequest || consumedSummary.current === summaryRequest.id) return
283+
consumedSummary.current = summaryRequest.id
284+
onSummaryConsumed?.(summaryRequest.id)
285+
void sendMessage(summaryRequest.message, undefined, undefined, {
286+
requestMode: 'assistant',
287+
assistantSearchLevel,
288+
assistantSearch: summaryRequest.assistantSearch,
289+
})
290+
}, [active, summaryRequest, onSummaryConsumed, sendMessage])
246291

247292
useEffect(() => {
248293
if (chatId) return
@@ -370,6 +415,7 @@ function OrganizationConversationContent({
370415
enabled={requestMode === 'assistant'}
371416
>
372417
<MothershipChat
418+
reserveResourceHeader={!header}
373419
onViewSources={(messageId, requestId) =>
374420
addResource({
375421
type: 'sources',
@@ -428,6 +474,7 @@ function OrganizationConversationContent({
428474
)
429475
return (
430476
<ChatResourcePanel
477+
header={header}
431478
allowBuildControls={requestMode !== 'assistant' && canBuild}
432479
organizationId={organization.id}
433480
chat={chat}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
'use client'
2+
3+
import { useState } from 'react'
4+
import { Chip, ChipConfirmModal, ChipModalError } from '@sim/emcn'
5+
import type { ViewerSearchSourceAccount } from '@/lib/api/contracts/knowledge/connectors'
6+
import { useDisconnectPersonalOrganizationAccount } from '@/hooks/queries/organization-accounts'
7+
8+
interface AccountConnectionActionsProps {
9+
organizationId: string
10+
integrationName: string
11+
account?: ViewerSearchSourceAccount
12+
onConnect?: () => void
13+
connectLabel: 'Connect' | 'Add account'
14+
onReconnect: () => void
15+
canReconnect: boolean
16+
canConnect: boolean
17+
connectionPending: boolean
18+
}
19+
20+
/** Header actions act on the selected account; adding a connection uses its available slot. */
21+
export function AccountConnectionActions({
22+
organizationId,
23+
integrationName,
24+
account,
25+
onConnect,
26+
connectLabel,
27+
onReconnect,
28+
canReconnect,
29+
canConnect,
30+
connectionPending,
31+
}: AccountConnectionActionsProps) {
32+
const disconnect = useDisconnectPersonalOrganizationAccount(organizationId)
33+
const [confirming, setConfirming] = useState(false)
34+
const pending = connectionPending || disconnect.isPending
35+
return (
36+
<>
37+
{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+
</>
77+
)}
78+
</>
79+
)
80+
}

0 commit comments

Comments
 (0)