Skip to content

Commit 93fc4d7

Browse files
committed
merge: preserve latest assistant and personal account changes
2 parents 3a3d38a + ee7ab5a commit 93fc4d7

300 files changed

Lines changed: 27712 additions & 915 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/sim/app/api/copilot/tools/execute/route.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,16 +41,19 @@ const turnRegistryCache = new Map<
4141
async function getTurnEgressRegistry(
4242
userId: string,
4343
workspaceId: string | undefined,
44-
messageId: string | undefined
44+
messageId: string | undefined,
45+
requestMode?: string
4546
): Promise<ResolvedSecretTraceRegistry> {
46-
const key = `${userId}\u0000${workspaceId ?? ''}\u0000${messageId ?? ''}`
47+
const key = `${userId}\u0000${workspaceId ?? ''}\u0000${messageId ?? ''}\u0000${requestMode ?? ''}`
4748
const now = Date.now()
4849
const hit = turnRegistryCache.get(key)
4950
if (hit && hit.expiresAt > now) {
5051
hit.expiresAt = now + TURN_REGISTRY_TTL_MS
5152
return hit.registry
5253
}
53-
const environmentContext = await prepareCopilotEnvironmentContext(userId, workspaceId)
54+
const environmentContext = await prepareCopilotEnvironmentContext(userId, workspaceId, {
55+
includeSecrets: requestMode !== 'assistant',
56+
})
5457
for (const [cachedKey, cached] of turnRegistryCache) {
5558
if (cached.expiresAt <= now) turnRegistryCache.delete(cachedKey)
5659
}
@@ -111,6 +114,8 @@ export const POST = withRouteHandler((request: NextRequest) =>
111114
messageId,
112115
parentToolCallId,
113116
userPermission,
117+
requestMode,
118+
assistantSearch,
114119
} = validation.data
115120
rootSpan.setAttributes({
116121
[TraceAttr.ToolName]: toolName,
@@ -121,7 +126,7 @@ export const POST = withRouteHandler((request: NextRequest) =>
121126
let toolRegistry: ResolvedSecretTraceRegistry
122127
let turnRegistry: ResolvedSecretTraceRegistry
123128
try {
124-
turnRegistry = await getTurnEgressRegistry(userId, workspaceId, messageId)
129+
turnRegistry = await getTurnEgressRegistry(userId, workspaceId, messageId, requestMode)
125130
toolRegistry = turnRegistry.forkForInputPaths([])
126131
} catch (err) {
127132
/**
@@ -173,6 +178,9 @@ export const POST = withRouteHandler((request: NextRequest) =>
173178
parentToolCallId,
174179
userPermission,
175180
copilotToolExecution: true,
181+
copilotInteractionMode: 'interactive',
182+
requestMode,
183+
assistantSearch,
176184
resolvedSecretTraceRegistry: toolRegistry,
177185
})
178186
const projection = inspectToolResultForCopilot(result, toolRegistry, toolName)

apps/sim/app/api/knowledge/search/route.test.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import { NextRequest } from 'next/server'
66
import { beforeEach, describe, expect, it, vi } from 'vitest'
77

88
const mocks = vi.hoisted(() => ({ search: vi.fn() }))
9-
vi.mock('@/lib/knowledge/application/search', () => ({
10-
searchKnowledge: { operation: { id: 'knowledge.search' }, execute: mocks.search },
9+
vi.mock('@/lib/knowledge/application/workspace-search', () => ({
10+
searchWorkspaceKnowledge: { operation: { id: 'knowledge.search' }, execute: mocks.search },
1111
}))
1212

1313
import { POST } from '@/app/api/knowledge/search/route'
@@ -29,7 +29,7 @@ describe('workspace search route', () => {
2929
headers: { 'content-type': 'application/json' },
3030
body: JSON.stringify({
3131
workspaceId: 'workspace-1',
32-
knowledgeBaseIds: ['kb-1'],
32+
filters: { source: 'slack', documentIds: ['doc-1'] },
3333
query: 'Orion',
3434
}),
3535
signal: controller.signal,
@@ -38,6 +38,8 @@ describe('workspace search route', () => {
3838
expect(response.status).toBe(200)
3939
const call = mocks.search.mock.calls[0][0]
4040
expect(call.principal).toEqual({ kind: 'session', userId: 'user-1', sessionId: 'session-1' })
41+
expect(call.input).not.toHaveProperty('knowledgeBaseIds')
42+
expect(call.input.filters).toEqual({ source: 'slack', documentIds: ['doc-1'] })
4143
expect(call.input.signal).toBe(request.signal)
4244
controller.abort()
4345
expect(call.input.signal.aborted).toBe(true)

apps/sim/app/api/knowledge/search/route.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
} from '@/lib/api/server/routes'
77
import { internalKnowledgeErrorPolicies } from '@/lib/knowledge/api/route-policies'
88
import { knowledgeOperations } from '@/lib/knowledge/application/operations'
9-
import { searchKnowledge } from '@/lib/knowledge/application/search'
9+
import { searchWorkspaceKnowledge } from '@/lib/knowledge/application/workspace-search'
1010
import { sourceAuthor } from '@/lib/knowledge/search/author'
1111

1212
export const POST = defineInternalJsonRoute({
@@ -19,13 +19,13 @@ export const POST = defineInternalJsonRoute({
1919
errorPolicy: internalKnowledgeErrorPolicies.search,
2020
mapInput: ({ body }, { request }) => ({
2121
workspaceId: body.workspaceId,
22-
knowledgeBaseIds: body.knowledgeBaseIds,
22+
filters: body.filters,
2323
query: body.query,
2424
topK: body.topK,
2525
surface: 'dashboard' as const,
2626
signal: request.signal,
2727
}),
28-
useCase: searchKnowledge,
28+
useCase: searchWorkspaceKnowledge,
2929
present: ({ results, knowledgeBases }, { input }) => {
3030
const knowledgeBaseNames = new Map(knowledgeBases.map((kb) => [kb.id, kb.name]))
3131
return {

apps/sim/app/workspace/[workspaceId]/home/components/knowledge-search-results/knowledge-search-results.tsx

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

33
import { useMemo } from 'react'
4-
import { Chip, OverflowText } from '@sim/emcn'
5-
import { FileText } from '@sim/emcn/icons'
6-
import { formatDate } from '@sim/utils/formatting'
4+
import { Chip } from '@sim/emcn'
75
import { useQueryStates } from 'nuqs'
8-
import type { WorkspaceKnowledgeSearchResult } from '@/lib/api/contracts/knowledge'
6+
import type {
7+
WorkspaceKnowledgeSearchResult,
8+
WorkspaceSearchFilters,
9+
} from '@/lib/api/contracts/knowledge'
10+
import { getBaseUrl } from '@/lib/core/utils/urls'
911
import { matchSnippet } from '@/lib/knowledge/search/snippet'
1012
import { connectorDisplayName } from '@/lib/sim-search/connectors'
11-
import { searchedKnowledgeBases } from '@/lib/sim-search/knowledge-bases'
12-
import {
13-
highlightTerms,
14-
SOURCE_ROW_CLASSES,
15-
SOURCE_ROW_MARK_CLASSES,
16-
SourceCard,
17-
} from '@/app/workspace/[workspaceId]/home/components/message-content/components/source-card'
13+
import { SourceCard } from '@/app/workspace/[workspaceId]/home/components/message-content/components/source-card'
1814
import {
1915
isHttpUrl,
2016
type SourceTagData,
@@ -78,14 +74,18 @@ export function indexingSourceNames(
7874

7975
/**
8076
* A result as the source card renders it: the row's second line names the
81-
* source app, or the knowledge base for an upload. A document without an
82-
* http(s) source URL cannot be opened, and a connector-supplied value of any
83-
* other scheme is never handed to the browser as a link.
77+
* source app, or the knowledge base for an upload. Without an HTTP(S) source
78+
* URL, the link opens the canonical document in Sim.
8479
*/
85-
function toSource(result: WorkspaceKnowledgeSearchResult, query: string): SourceTagData | null {
86-
if (!isHttpUrl(result.sourceUrl)) return null
80+
function toSource(
81+
result: WorkspaceKnowledgeSearchResult,
82+
query: string,
83+
workspaceId: string
84+
): SourceTagData {
8785
return {
88-
url: result.sourceUrl,
86+
url: isHttpUrl(result.sourceUrl)
87+
? result.sourceUrl
88+
: `${getBaseUrl()}/workspace/${encodeURIComponent(workspaceId)}/knowledge/${encodeURIComponent(result.knowledgeBaseId)}/${encodeURIComponent(result.documentId)}`,
8989
title: result.documentName ?? undefined,
9090
siteName: result.connectorType
9191
? connectorDisplayName(result.connectorType)
@@ -113,51 +113,16 @@ function handleResultsKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
113113
links[next].focus()
114114
}
115115

116-
interface UnlinkedResultRowProps {
117-
result: WorkspaceKnowledgeSearchResult
118-
query: string
119-
}
120-
121-
/**
122-
* A document with nowhere to open, such as an upload: the same row as a
123-
* linked result, with the file mark in place of a brand mark, so the list's
124-
* columns and the matched passage stay aligned whatever the document is.
125-
*/
126-
function UnlinkedResultRow({ result, query }: UnlinkedResultRowProps) {
127-
const meta = [
128-
result.knowledgeBaseName,
129-
result.author,
130-
result.sourceModifiedAt ? formatDate(new Date(result.sourceModifiedAt)) : null,
131-
].filter((part): part is string => Boolean(part))
132-
return (
133-
<div className={SOURCE_ROW_CLASSES}>
134-
<span className={SOURCE_ROW_MARK_CLASSES}>
135-
<FileText className='size-[16px] text-[var(--text-icon)]' />
136-
</span>
137-
<div className='flex min-w-0 flex-1 flex-col gap-0.5'>
138-
<OverflowText
139-
label={result.documentName ?? 'Untitled document'}
140-
className='text-[var(--text-primary)] text-sm'
141-
/>
142-
<OverflowText label={meta.join(' · ')} className='text-[var(--text-muted)] text-caption' />
143-
<p className='line-clamp-2 text-[var(--text-body)] text-small leading-snug'>
144-
{highlightTerms(matchSnippet(result.content, query), query)}
145-
</p>
146-
</div>
147-
</div>
148-
)
149-
}
150-
151116
interface KnowledgeSearchResultsProps {
152117
workspaceId: string
153118
query: string
154-
/** Asks the agent about one document; the prompt names it and links to it. */
155-
onSummarize: (prompt: string) => void
119+
/** Binds the Assistant turn to the selected canonical document. */
120+
onSummarize: (prompt: string, filters: WorkspaceSearchFilters) => void
156121
}
157122

158123
/**
159124
* The composer's Search mode: the documents the signed-in person may read that
160-
* match their query, across every knowledge base in the workspace, as rows
125+
* match their query in the canonical Enterprise Search index, as rows
161126
* that open the source. A header says how many and that the search ran as
162127
* them; while a connected source is still indexing it says so, and the list
163128
* grows as documents land. Filters by source and recency appear only once the
@@ -174,14 +139,26 @@ export function KnowledgeSearchResults({
174139
isPending: basesPending,
175140
error: basesError,
176141
} = useKnowledgeBasesQuery(workspaceId)
177-
const knowledgeBaseIds = searchedKnowledgeBases(knowledgeBases, workspaceId).map((kb) => kb.id)
142+
const index = knowledgeBases.find(
143+
(base) => base.workspaceId === workspaceId && base.isSearchIndex
144+
)
145+
const knowledgeBaseIds = index ? [index.id] : []
146+
const [filters, setFilters] = useQueryStates(searchFilterParsers, resourceUrlKeys)
147+
const searchFilters = useMemo<WorkspaceSearchFilters>(() => {
148+
const window = UPDATED_WINDOWS.find((entry) => entry.id === filters.updated)
149+
return {
150+
...(filters.source ? { source: filters.source } : {}),
151+
...(window?.days
152+
? { modifiedAfter: new Date(Date.now() - window.days * DAY_MS).toISOString() }
153+
: {}),
154+
}
155+
}, [filters.source, filters.updated])
178156
const {
179157
data: results,
180158
isPending,
181159
isFetching,
182-
isPlaceholderData,
183160
error,
184-
} = useWorkspaceKnowledgeSearch(workspaceId, knowledgeBaseIds, query)
161+
} = useWorkspaceKnowledgeSearch(workspaceId, query, searchFilters)
185162
/**
186163
* With per-member access off, member-scoped documents are hidden, so the
187164
* indexing list is not worth asking for.
@@ -196,28 +173,16 @@ export function KnowledgeSearchResults({
196173
: EMPTY_MEMBER_CONNECTORS
197174
const indexing = indexingSourceNames(memberConnectors, knowledgeBaseIds)
198175
const documents = useMemo(() => groupResultsByDocument(results ?? []), [results])
199-
const sourceTypes = useMemo(
200-
() => [...new Set(documents.map((result) => result.connectorType ?? UPLOAD_SOURCE))],
201-
[documents]
202-
)
203-
const [filters, setFilters] = useQueryStates(searchFilterParsers, resourceUrlKeys)
176+
const sourceTypes = [
177+
...new Set([
178+
...(filters.source ? [filters.source] : []),
179+
...documents.map((result) => result.connectorType ?? UPLOAD_SOURCE),
180+
]),
181+
]
204182
const filtersActive = filters.source !== null || filters.updated !== 'any'
205183
/** The controls appear once the list is long and mixed, and stay while a filter from the link is active. */
206184
const showFilters =
207185
filtersActive || (documents.length >= FILTERS_MIN_RESULTS && sourceTypes.length > 1)
208-
const visible = useMemo(() => {
209-
if (!filtersActive) return documents
210-
const window = UPDATED_WINDOWS.find((entry) => entry.id === filters.updated)
211-
const cutoff = window?.days ? Date.now() - window.days * DAY_MS : null
212-
return documents.filter((result) => {
213-
if (filters.source && (result.connectorType ?? UPLOAD_SOURCE) !== filters.source) return false
214-
if (cutoff !== null) {
215-
const modified = result.sourceModifiedAt ? Date.parse(result.sourceModifiedAt) : Number.NaN
216-
if (Number.isNaN(modified) || modified < cutoff) return false
217-
}
218-
return true
219-
})
220-
}, [documents, filtersActive, filters.source, filters.updated])
221186

222187
const failure = basesError ?? error
223188
if (failure) {
@@ -230,8 +195,7 @@ export function KnowledgeSearchResults({
230195
</p>
231196
)
232197
}
233-
/** Kept results belong to the previous query; a new query shows its own state. */
234-
if (isPending || isPlaceholderData || (isFetching && !results)) {
198+
if (isPending || (isFetching && !results)) {
235199
return <p className='px-2 py-2 text-[var(--text-muted)] text-caption'>Searching…</p>
236200
}
237201

@@ -283,27 +247,28 @@ export function KnowledgeSearchResults({
283247
))}
284248
</div>
285249
)}
286-
{visible.length === 0 ? (
250+
{documents.length === 0 ? (
287251
<p className='px-2 py-2 text-[var(--text-muted)] text-caption'>
288-
{documents.length === 0
289-
? `No documents you can read match ${query}”.`
290-
: 'No documents match these filters.'}
252+
{filtersActive
253+
? 'No documents match these filters.'
254+
: `No documents you can read match “${query}”.`}
291255
</p>
292256
) : (
293257
<div className='flex flex-col' onKeyDown={handleResultsKeyDown}>
294-
{visible.map((result) => {
295-
const source = toSource(result, query)
296-
return source ? (
258+
{documents.map((result) => {
259+
const source = toSource(result, query, workspaceId)
260+
return (
297261
<SourceCard
298262
key={result.documentId}
299263
source={source}
300264
query={query}
301265
onSummarize={(cited) =>
302-
onSummarize(`Summarize "${cited.title ?? cited.url}" (${cited.url})`)
266+
onSummarize(`Summarize "${cited.title ?? cited.url}"`, {
267+
...searchFilters,
268+
documentIds: [result.documentId],
269+
})
303270
}
304271
/>
305-
) : (
306-
<UnlinkedResultRow key={result.documentId} result={result} query={query} />
307272
)
308273
})}
309274
</div>

apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
} from '@/lib/copilot/tools/tool-display'
2323
import { useChatSurface } from '@/app/workspace/[workspaceId]/home/components/chat-surface-context'
2424
import type { CredentialSubmissionPayload } from '@/app/workspace/[workspaceId]/home/components/message-content/components/special-tags'
25+
import { resolveMessageCitations } from '@/app/workspace/[workspaceId]/home/components/message-content/resolve-citations'
2526
import { useCustomBlockOverlayVersion } from '@/blocks/custom/client-overlay'
2627
import type { ContentBlock, OptionItem, ToolCallData } from '../../types'
2728
import { SUBAGENT_LABELS } from '../../types'
@@ -818,6 +819,7 @@ interface MessageContentProps {
818819
blocks: ContentBlock[]
819820
fallbackContent: string
820821
messageId?: string
822+
requestMode?: 'agent' | 'assistant'
821823
isStreaming: boolean
822824
/**
823825
* True for the last message in the transcript. The last turn keeps a
@@ -848,6 +850,7 @@ function MessageContentInner({
848850
blocks,
849851
fallbackContent,
850852
messageId,
853+
requestMode,
851854
isStreaming = false,
852855
isLast = false,
853856
questionAnswers,
@@ -860,9 +863,13 @@ function MessageContentInner({
860863
}: MessageContentProps) {
861864
const { onWorkspaceResourceSelect } = useChatSurface()
862865
const blockOverlayVersion = useCustomBlockOverlayVersion()
866+
const cited = useMemo(
867+
() => resolveMessageCitations(blocks, fallbackContent, requestMode === 'assistant'),
868+
[blocks, fallbackContent, requestMode]
869+
)
863870
const parsed = useMemo(
864-
() => (blocks.length > 0 ? parseBlocks(blocks) : []),
865-
[blocks, blockOverlayVersion]
871+
() => (cited.blocks.length > 0 ? parseBlocks(cited.blocks) : []),
872+
[cited.blocks, blockOverlayVersion]
866873
)
867874

868875
const [trailingRevealing, setTrailingRevealing] = useState(false)
@@ -883,10 +890,10 @@ function MessageContentInner({
883890
() =>
884891
parsed.length > 0
885892
? parsed
886-
: fallbackContent?.trim()
887-
? [{ type: 'text', id: 'text-fallback', content: fallbackContent }]
893+
: cited.fallbackContent?.trim()
894+
? [{ type: 'text', id: 'text-fallback', content: cited.fallbackContent }]
888895
: [],
889-
[parsed, fallbackContent]
896+
[parsed, cited.fallbackContent]
890897
)
891898
/**
892899
* Collected from the segments that render, not the raw blocks: that is the

0 commit comments

Comments
 (0)