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
12 changes: 7 additions & 5 deletions apps/docs/content/docs/search/ask-questions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -127,19 +127,21 @@ Copy the reviewed draft into another app, or ask Assistant to create it using yo

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.

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

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

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.

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.

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

## Start from a document

Expand Down
18 changes: 9 additions & 9 deletions apps/docs/content/docs/search/connect-your-account.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,19 @@ After your admin adds a source in **Settings → Sources**, connect your own acc
## Connect and verify

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

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

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

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

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.

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.

## Tool permissions

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:
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:

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

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

## Reconnect or disconnect

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

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

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.

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.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { listAssistantActionReceiptsContract } from '@/lib/api/contracts/assistant-actions'
import {
defineInternalJsonRoute,
internalOrchestrationErrorPolicy,
internalRateLimits,
internalSessionAuth,
} from '@/lib/api/server/routes'
import { assistantActionOperations } from '@/lib/assistant-actions/application/operations'
import { listOrganizationAssistantActionReceipts } from '@/lib/assistant-actions/application/receipts'

export const GET = defineInternalJsonRoute({
contract: listAssistantActionReceiptsContract,
auth: internalSessionAuth,
operation: assistantActionOperations.receipts,
rateLimit: internalRateLimits.user({
bucketName: 'assistant-action-receipts',
config: { maxTokens: 120, refillRate: 120, refillIntervalMs: 60_000 },
}),
errorPolicy: internalOrchestrationErrorPolicy,
mapInput: ({ params, query }) => ({ organizationId: params.id, chatId: params.chatId, ...query }),
useCase: listOrganizationAssistantActionReceipts,
staticResponseHeaders: { 'Cache-Control': 'private, no-store' },
})
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
'use client'

import { useCallback, useEffect, useState } from 'react'
import { type ReactNode, useCallback, useEffect, useRef, useState } from 'react'
import { toast } from '@sim/emcn'
import { toRecord } from '@sim/utils/object'
import { useQueryClient } from '@tanstack/react-query'
import { useQueryStates } from 'nuqs'
import { createSerializer, useQueryStates } from 'nuqs'
import { AssistantActionReviewProvider } from '@/components/assistant-actions'
import { requestJson } from '@/lib/api/client/request'
import type { WorkspaceSearchFilters } from '@/lib/api/contracts/knowledge'
Expand All @@ -16,6 +16,7 @@ import {
getMothershipAttachmentUrl,
} from '@/lib/mothership/chat/attachment-preview'
import { createSearchResource } from '@/lib/mothership/resources/search'
import { organizationRoutes } from '@/lib/navigation/paths'
import { OrganizationLanding } from '@/app/o/[organizationId]/components/organization-landing'
import { Composer } from '@/app/o/[organizationId]/home/components/composer'
import { GetStarted } from '@/app/o/[organizationId]/home/components/get-started'
Expand All @@ -41,6 +42,7 @@ import {
import { useRestoredChatEntry } from '@/app/workspace/[workspaceId]/home/hooks/use-restored-chat-entry'
import { fetchWorkspaceResourceRef } from '@/app/workspace/[workspaceId]/home/resolve-resource-ref'
import {
resourceParam,
searchFilterParsers,
searchFiltersFromParams,
} from '@/app/workspace/[workspaceId]/home/search-params'
Expand All @@ -57,10 +59,26 @@ import { useHydrated } from '@/hooks/use-hydrated'
import { useMothershipDraftsStore } from '@/stores/mothership-drafts/store'
import type { ChatContext } from '@/stores/panel'

export interface OrganizationSummaryRequest {
id: string
message: string
assistantSearch: WorkspaceSearchFilters
}

const serializeSearchConversation = createSerializer({
...organizationSearchParsers,
...searchFilterParsers,
[resourceParam.key]: resourceParam.parser,
})

interface OrganizationConversationProps {
active?: boolean
summaryRequest?: OrganizationSummaryRequest
onSummaryConsumed?: (id: string) => void
userName?: string
chatId?: string
requestMode: ChatRequestMode
header?: ReactNode
}

/** Shared conversation surface; the destination fixes its intent before the first turn. */
Expand All @@ -74,7 +92,8 @@ export function OrganizationConversation(props: OrganizationConversationProps) {
(props.requestMode === 'assistant' ? !searchAccess.memberScoped : !canBuild)
)
return null
if (isRestoredChatEntry || !isClient || !session?.user?.id) return <HomeFallback />
if (isRestoredChatEntry || !isClient || !session?.user?.id)
return <HomeFallback header={props.header} />
return (
<OrganizationConversationContent
key={`${session.user.id}:${organization.id}:${props.chatId ?? 'new'}:${props.requestMode}`}
Expand All @@ -87,6 +106,10 @@ function OrganizationConversationContent({
userName,
chatId,
requestMode: destinationMode,
header,
active = true,
summaryRequest,
onSummaryConsumed,
}: OrganizationConversationProps) {
const { organization, canBuild } = useOrganizationContext()
const { data: session } = useSession()
Expand All @@ -95,17 +118,27 @@ function OrganizationConversationContent({
const [planning, setPlanning] = useState(false)
const requestMode =
destinationMode === 'agent' && !chatId && planEnabled && planning ? 'plan' : destinationMode
const [{ q, source, updated, from, to }] = useQueryStates(
const [searchParams] = useQueryStates(
{ ...organizationSearchParsers, ...searchFilterParsers },
organizationSearchUrlKeys
)
const { q, source, updated, from, to } = searchParams
const controller = useResourcePanelController()
const queryClient = useQueryClient()
const chat = useChat(
{ organizationId: organization.id },
chatId,
getMothershipUseChatOptions({
requestMode,
getChatUrl:
destinationMode === 'assistant'
? (id) =>
serializeSearchConversation(organizationRoutes(organization.id).searchChat(id), {
...searchParams,
mode: active ? 'assistant' : 'search',
resource: controller.activeResourceState[0],
})
: undefined,
onResourceEvent: controller.onResourceEvent,
activeResourceState: controller.activeResourceState,
})
Expand Down Expand Up @@ -240,9 +273,21 @@ function OrganizationConversationContent({
const firstName = userName?.split(' ')[0] ?? ''

useEffect(() => {
if (chat.resolvedChatId && !chat.isSending && !chat.isReconnecting)
if (active && chat.resolvedChatId && !chat.isSending && !chat.isReconnecting)
markRead(chat.resolvedChatId)
}, [chat.resolvedChatId, chat.isSending, chat.isReconnecting, markRead])
}, [active, chat.resolvedChatId, chat.isSending, chat.isReconnecting, markRead])

const consumedSummary = useRef<string | null>(null)
useEffect(() => {
if (!active || !summaryRequest || consumedSummary.current === summaryRequest.id) return
consumedSummary.current = summaryRequest.id
onSummaryConsumed?.(summaryRequest.id)
void sendMessage(summaryRequest.message, undefined, undefined, {
requestMode: 'assistant',
assistantSearchLevel,
assistantSearch: summaryRequest.assistantSearch,
})
}, [active, summaryRequest, onSummaryConsumed, sendMessage])

useEffect(() => {
if (chatId) return
Expand Down Expand Up @@ -370,6 +415,7 @@ function OrganizationConversationContent({
enabled={requestMode === 'assistant'}
>
<MothershipChat
reserveResourceHeader={!header}
onViewSources={(messageId, requestId) =>
addResource({
type: 'sources',
Expand Down Expand Up @@ -428,6 +474,7 @@ function OrganizationConversationContent({
)
return (
<ChatResourcePanel
header={header}
allowBuildControls={requestMode !== 'assistant' && canBuild}
organizationId={organization.id}
chat={chat}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
'use client'

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

interface AccountConnectionActionsProps {
organizationId: string
integrationName: string
account?: ViewerSearchSourceAccount
onConnect?: () => void
connectLabel: 'Connect' | 'Add account'
onReconnect: () => void
canReconnect: boolean
canConnect: boolean
connectionPending: boolean
}

/** Header actions act on the selected account; adding a connection uses its available slot. */
export function AccountConnectionActions({
organizationId,
integrationName,
account,
onConnect,
connectLabel,
onReconnect,
canReconnect,
canConnect,
connectionPending,
}: AccountConnectionActionsProps) {
const disconnect = useDisconnectPersonalOrganizationAccount(organizationId)
const [confirming, setConfirming] = useState(false)
const pending = connectionPending || disconnect.isPending
return (
<>
{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>
</>
)}
</>
)
}
Loading
Loading