mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-28 03:00:29 -04:00
Clickable resources
This commit is contained in:
@@ -17,6 +17,7 @@ import {
|
||||
parseSpecialTags,
|
||||
SpecialTags,
|
||||
} from '@/app/workspace/[workspaceId]/home/components/message-content/components/special-tags'
|
||||
import type { MothershipResource } from '@/app/workspace/[workspaceId]/home/types'
|
||||
import { useStreamingText } from '@/hooks/use-streaming-text'
|
||||
|
||||
const LANG_ALIASES: Record<string, string> = {
|
||||
@@ -119,6 +120,28 @@ const MARKDOWN_COMPONENTS = {
|
||||
)
|
||||
},
|
||||
a({ children, href }: { children?: React.ReactNode; href?: string }) {
|
||||
if (href?.startsWith('#wsres-')) {
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
className='text-[var(--text-primary)] underline decoration-dashed underline-offset-4'
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
const match = href.match(/^#wsres-(\w+)-(.+)$/)
|
||||
if (match) {
|
||||
const linkText = e.currentTarget.textContent || match[2]
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('wsres-click', {
|
||||
detail: { type: match[1], id: match[2], title: linkText },
|
||||
})
|
||||
)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
@@ -172,6 +195,7 @@ interface ChatContentProps {
|
||||
content: string
|
||||
isStreaming?: boolean
|
||||
onOptionSelect?: (id: string) => void
|
||||
onWorkspaceResourceSelect?: (resource: MothershipResource) => void
|
||||
smoothStreaming?: boolean
|
||||
}
|
||||
|
||||
@@ -179,6 +203,7 @@ export function ChatContent({
|
||||
content,
|
||||
isStreaming = false,
|
||||
onOptionSelect,
|
||||
onWorkspaceResourceSelect,
|
||||
smoothStreaming = true,
|
||||
}: ChatContentProps) {
|
||||
const hydratedStreamingRef = useRef(isStreaming && content.trim().length > 0)
|
||||
@@ -193,6 +218,23 @@ export function ChatContent({
|
||||
previousIsStreamingRef.current = isStreaming
|
||||
}, [content, isStreaming])
|
||||
|
||||
const onWorkspaceResourceSelectRef = useRef(onWorkspaceResourceSelect)
|
||||
onWorkspaceResourceSelectRef.current = onWorkspaceResourceSelect
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e: Event) => {
|
||||
const { type, id, title } = (e as CustomEvent).detail
|
||||
const RESOURCE_TYPE_MAP: Record<string, string> = {}
|
||||
onWorkspaceResourceSelectRef.current?.({
|
||||
type: RESOURCE_TYPE_MAP[type] || type,
|
||||
id,
|
||||
title: title || id,
|
||||
})
|
||||
}
|
||||
window.addEventListener('wsres-click', handler)
|
||||
return () => window.removeEventListener('wsres-click', handler)
|
||||
}, [])
|
||||
|
||||
const rendered = useStreamingText(content, isStreaming && smoothStreaming)
|
||||
|
||||
const parsed = useMemo(() => parseSpecialTags(rendered, isStreaming), [rendered, isStreaming])
|
||||
@@ -202,22 +244,37 @@ export function ChatContent({
|
||||
return (
|
||||
<div className='space-y-3'>
|
||||
{parsed.segments.map((segment, i) => {
|
||||
if (segment.type === 'text' || segment.type === 'thinking') {
|
||||
return (
|
||||
<div
|
||||
key={`${segment.type}-${i}`}
|
||||
className={cn(PROSE_CLASSES, '[&>:first-child]:mt-0 [&>:last-child]:mb-0')}
|
||||
>
|
||||
<Streamdown mode='static' components={MARKDOWN_COMPONENTS}>
|
||||
{segment.content}
|
||||
</Streamdown>
|
||||
</div>
|
||||
)
|
||||
if (
|
||||
segment.type === 'text' ||
|
||||
segment.type === 'thinking' ||
|
||||
segment.type === 'workspace_resource'
|
||||
) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<SpecialTags key={`special-${i}`} segment={segment} onOptionSelect={onOptionSelect} />
|
||||
)
|
||||
})}
|
||||
{(() => {
|
||||
const reassembled = parsed.segments
|
||||
.map((s) => {
|
||||
if (s.type === 'workspace_resource') {
|
||||
const label = s.data.title || s.data.id
|
||||
return `[${label}](#wsres-${s.data.type}-${s.data.id})`
|
||||
}
|
||||
if (s.type === 'text' || s.type === 'thinking') return s.content
|
||||
return ''
|
||||
})
|
||||
.join('')
|
||||
if (!reassembled.trim()) return null
|
||||
return (
|
||||
<div className={cn(PROSE_CLASSES, '[&>:first-child]:mt-0 [&>:last-child]:mb-0')}>
|
||||
<Streamdown mode='static' components={MARKDOWN_COMPONENTS}>
|
||||
{reassembled}
|
||||
</Streamdown>
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
{parsed.hasPendingTag && isStreaming && <PendingTagIndicator />}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -9,6 +9,8 @@ export type {
|
||||
RuntimeSpecialTagName,
|
||||
UsageUpgradeAction,
|
||||
UsageUpgradeTagData,
|
||||
WorkspaceResourceTagData,
|
||||
WorkspaceResourceTagType,
|
||||
} from './special-tags'
|
||||
export {
|
||||
CREDENTIAL_TAG_TYPES,
|
||||
@@ -20,4 +22,6 @@ export {
|
||||
parseTextTagBody,
|
||||
SpecialTags,
|
||||
USAGE_UPGRADE_ACTIONS,
|
||||
WORKSPACE_RESOURCE_TAG_TYPES,
|
||||
WorkspaceResourceDisplay,
|
||||
} from './special-tags'
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
'use client'
|
||||
|
||||
import { createElement, useState } from 'react'
|
||||
import { createElement, useMemo, useState } from 'react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { ArrowRight, ChevronDown, Expandable, ExpandableContent } from '@/components/emcn'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { OAUTH_PROVIDERS } from '@/lib/oauth/oauth'
|
||||
import { ContextMentionIcon } from '@/app/workspace/[workspaceId]/home/components/context-mention-icon'
|
||||
import type {
|
||||
ChatMessageContext,
|
||||
MothershipResource,
|
||||
} from '@/app/workspace/[workspaceId]/home/types'
|
||||
import { useKnowledgeBasesQuery } from '@/hooks/queries/kb/knowledge'
|
||||
import { useTablesList } from '@/hooks/queries/tables'
|
||||
import { useWorkflows } from '@/hooks/queries/workflows'
|
||||
import { useWorkspaceFiles } from '@/hooks/queries/workspace-files'
|
||||
|
||||
export interface OptionsItemData {
|
||||
title: string
|
||||
@@ -55,6 +64,16 @@ export interface FileTagData {
|
||||
content: string
|
||||
}
|
||||
|
||||
export const WORKSPACE_RESOURCE_TAG_TYPES = ['workflow', 'table', 'file'] as const
|
||||
|
||||
export type WorkspaceResourceTagType = (typeof WORKSPACE_RESOURCE_TAG_TYPES)[number]
|
||||
|
||||
export interface WorkspaceResourceTagData {
|
||||
type: WorkspaceResourceTagType
|
||||
id: string
|
||||
title?: string
|
||||
}
|
||||
|
||||
export type ContentSegment =
|
||||
| { type: 'text'; content: string }
|
||||
| { type: 'thinking'; content: string }
|
||||
@@ -62,6 +81,7 @@ export type ContentSegment =
|
||||
| { type: 'usage_upgrade'; data: UsageUpgradeTagData }
|
||||
| { type: 'credential'; data: CredentialTagData }
|
||||
| { type: 'mothership-error'; data: MothershipErrorTagData }
|
||||
| { type: 'workspace_resource'; data: WorkspaceResourceTagData }
|
||||
|
||||
export type RuntimeSpecialTagName =
|
||||
| 'thinking'
|
||||
@@ -69,6 +89,7 @@ export type RuntimeSpecialTagName =
|
||||
| 'credential'
|
||||
| 'mothership-error'
|
||||
| 'file'
|
||||
| 'workspace_resource'
|
||||
|
||||
export interface ParsedSpecialContent {
|
||||
segments: ContentSegment[]
|
||||
@@ -81,6 +102,7 @@ const RUNTIME_SPECIAL_TAG_NAMES = [
|
||||
'credential',
|
||||
'mothership-error',
|
||||
'file',
|
||||
'workspace_resource',
|
||||
] as const
|
||||
|
||||
const SPECIAL_TAG_NAMES = [
|
||||
@@ -89,6 +111,7 @@ const SPECIAL_TAG_NAMES = [
|
||||
'usage_upgrade',
|
||||
'credential',
|
||||
'mothership-error',
|
||||
'workspace_resource',
|
||||
] as const
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
@@ -134,6 +157,16 @@ function isMothershipErrorTagData(value: unknown): value is MothershipErrorTagDa
|
||||
)
|
||||
}
|
||||
|
||||
function isWorkspaceResourceTagData(value: unknown): value is WorkspaceResourceTagData {
|
||||
if (!isRecord(value)) return false
|
||||
return (
|
||||
typeof value.type === 'string' &&
|
||||
(WORKSPACE_RESOURCE_TAG_TYPES as readonly string[]).includes(value.type) &&
|
||||
typeof value.id === 'string' &&
|
||||
value.id.trim().length > 0
|
||||
)
|
||||
}
|
||||
|
||||
export function parseJsonTagBody<T>(
|
||||
body: string,
|
||||
isExpectedShape: (value: unknown) => value is T
|
||||
@@ -181,6 +214,7 @@ function parseSpecialTagData(
|
||||
| { type: 'usage_upgrade'; data: UsageUpgradeTagData }
|
||||
| { type: 'credential'; data: CredentialTagData }
|
||||
| { type: 'mothership-error'; data: MothershipErrorTagData }
|
||||
| { type: 'workspace_resource'; data: WorkspaceResourceTagData }
|
||||
| null {
|
||||
if (tagName === 'thinking') {
|
||||
const content = parseTextTagBody(body)
|
||||
@@ -207,11 +241,16 @@ function parseSpecialTagData(
|
||||
return data ? { type: 'mothership-error', data } : null
|
||||
}
|
||||
|
||||
if (tagName === 'workspace_resource') {
|
||||
const data = parseJsonTagBody(body, isWorkspaceResourceTagData)
|
||||
return data ? { type: 'workspace_resource', data } : null
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses inline special tags (`<options>`, `<usage_upgrade>`) from streamed
|
||||
* Parses inline special tags (`<options>`, `<usage_upgrade>`, `<workspace_resource>`) from streamed
|
||||
* text content. Complete tags are extracted into typed segments; incomplete
|
||||
* tags (still streaming) are suppressed from display and flagged via
|
||||
* `hasPendingTag` so the caller can show a loading indicator.
|
||||
@@ -307,12 +346,18 @@ const THINKING_BLOCKS = [
|
||||
interface SpecialTagsProps {
|
||||
segment: Exclude<ContentSegment, { type: 'text' }>
|
||||
onOptionSelect?: (id: string) => void
|
||||
onWorkspaceResourceSelect?: (resource: MothershipResource) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Unified renderer for inline special tags: `<options>`, `<usage_upgrade>`, and `<credential>`.
|
||||
* Unified renderer for inline special tags: `<options>`, `<usage_upgrade>`, `<credential>`,
|
||||
* and `<workspace_resource>`.
|
||||
*/
|
||||
export function SpecialTags({ segment, onOptionSelect }: SpecialTagsProps) {
|
||||
export function SpecialTags({
|
||||
segment,
|
||||
onOptionSelect,
|
||||
onWorkspaceResourceSelect,
|
||||
}: SpecialTagsProps) {
|
||||
switch (segment.type) {
|
||||
case 'thinking':
|
||||
return null
|
||||
@@ -324,6 +369,8 @@ export function SpecialTags({ segment, onOptionSelect }: SpecialTagsProps) {
|
||||
return <CredentialDisplay data={segment.data} />
|
||||
case 'mothership-error':
|
||||
return <MothershipErrorDisplay data={segment.data} />
|
||||
case 'workspace_resource':
|
||||
return <WorkspaceResourceDisplay data={segment.data} onSelect={onWorkspaceResourceSelect} />
|
||||
default:
|
||||
return null
|
||||
}
|
||||
@@ -413,6 +460,102 @@ function OptionsDisplay({ data, onSelect }: OptionsDisplayProps) {
|
||||
)
|
||||
}
|
||||
|
||||
function fallbackWorkspaceResourceTitle(type: WorkspaceResourceTagType): string {
|
||||
switch (type) {
|
||||
case 'workflow':
|
||||
return 'Workflow'
|
||||
case 'table':
|
||||
return 'Table'
|
||||
case 'file':
|
||||
return 'File'
|
||||
}
|
||||
}
|
||||
|
||||
function toMothershipResourceType(type: WorkspaceResourceTagType): MothershipResource['type'] {
|
||||
return type
|
||||
}
|
||||
|
||||
function toChatMessageContext(data: WorkspaceResourceTagData, label: string): ChatMessageContext {
|
||||
switch (data.type) {
|
||||
case 'workflow':
|
||||
return { kind: 'workflow', label, workflowId: data.id }
|
||||
case 'table':
|
||||
return { kind: 'table', label, tableId: data.id }
|
||||
case 'file':
|
||||
return { kind: 'file', label, fileId: data.id }
|
||||
}
|
||||
}
|
||||
|
||||
export function WorkspaceResourceDisplay({
|
||||
data,
|
||||
onSelect,
|
||||
}: {
|
||||
data: WorkspaceResourceTagData
|
||||
onSelect?: (resource: MothershipResource) => void
|
||||
}) {
|
||||
const { workspaceId } = useParams<{ workspaceId: string }>()
|
||||
const { data: workflows = [] } = useWorkflows(workspaceId)
|
||||
const { data: tables = [] } = useTablesList(workspaceId)
|
||||
const { data: files = [] } = useWorkspaceFiles(workspaceId)
|
||||
const { data: knowledgeBases = [] } = useKnowledgeBasesQuery(workspaceId)
|
||||
|
||||
const resource = useMemo<MothershipResource>(() => {
|
||||
const title =
|
||||
data.type === 'workflow'
|
||||
? (workflows.find((workflow) => workflow.id === data.id)?.name ??
|
||||
fallbackWorkspaceResourceTitle(data.type))
|
||||
: data.type === 'table'
|
||||
? (tables.find((table) => table.id === data.id)?.name ??
|
||||
fallbackWorkspaceResourceTitle(data.type))
|
||||
: data.type === 'file'
|
||||
? (files.find((file) => file.id === data.id)?.name ??
|
||||
fallbackWorkspaceResourceTitle(data.type))
|
||||
: (knowledgeBases.find((knowledgeBase) => knowledgeBase.id === data.id)?.name ??
|
||||
fallbackWorkspaceResourceTitle(data.type))
|
||||
|
||||
return {
|
||||
type: toMothershipResourceType(data.type),
|
||||
id: data.id,
|
||||
title,
|
||||
}
|
||||
}, [data.id, data.type, files, knowledgeBases, tables, workflows])
|
||||
|
||||
const context = useMemo(() => toChatMessageContext(data, resource.title), [data, resource.title])
|
||||
|
||||
const workflowColor = useMemo(() => {
|
||||
if (data.type !== 'workflow') return null
|
||||
return workflows.find((workflow) => workflow.id === data.id)?.color ?? null
|
||||
}, [data.id, data.type, workflows])
|
||||
|
||||
const mentionContent = (
|
||||
<>
|
||||
<ContextMentionIcon
|
||||
context={context}
|
||||
workflowColor={workflowColor}
|
||||
className='relative top-0.5 h-[12px] w-[12px] flex-shrink-0 text-[var(--text-icon)]'
|
||||
/>
|
||||
{resource.title}
|
||||
</>
|
||||
)
|
||||
|
||||
const classes =
|
||||
'inline-flex items-baseline gap-1 rounded-[5px] bg-[var(--surface-5)] px-[5px] align-baseline font-[inherit] text-[inherit] leading-[inherit]'
|
||||
|
||||
if (!onSelect) {
|
||||
return <span className={classes}>{mentionContent}</span>
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => onSelect(resource)}
|
||||
className={cn(classes, 'cursor-pointer transition-colors hover-hover:bg-[var(--surface-6)]')}
|
||||
>
|
||||
{mentionContent}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
function getCredentialIcon(provider: string): React.ComponentType<{ className?: string }> | null {
|
||||
const lower = provider.toLowerCase()
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from '@/lib/copilot/generated/tool-catalog-v1'
|
||||
import { resolveToolDisplay } from '@/lib/copilot/tools/client/store-utils'
|
||||
import { ClientToolCallState } from '@/lib/copilot/tools/client/tool-display-registry'
|
||||
import type { ContentBlock, OptionItem, ToolCallData } from '../../types'
|
||||
import type { ContentBlock, MothershipResource, OptionItem, ToolCallData } from '../../types'
|
||||
import { SUBAGENT_LABELS, TOOL_UI_METADATA } from '../../types'
|
||||
import type { AgentGroupItem } from './components'
|
||||
import { AgentGroup, ChatContent, CircleStop, Options, PendingTagIndicator } from './components'
|
||||
@@ -331,6 +331,7 @@ interface MessageContentProps {
|
||||
fallbackContent: string
|
||||
isStreaming: boolean
|
||||
onOptionSelect?: (id: string) => void
|
||||
onWorkspaceResourceSelect?: (resource: MothershipResource) => void
|
||||
}
|
||||
|
||||
export function MessageContent({
|
||||
@@ -338,6 +339,7 @@ export function MessageContent({
|
||||
fallbackContent,
|
||||
isStreaming = false,
|
||||
onOptionSelect,
|
||||
onWorkspaceResourceSelect,
|
||||
}: MessageContentProps) {
|
||||
const parsed = blocks.length > 0 ? parseBlocks(blocks) : []
|
||||
|
||||
@@ -391,6 +393,7 @@ export function MessageContent({
|
||||
content={segment.content}
|
||||
isStreaming={isStreaming}
|
||||
onOptionSelect={onOptionSelect}
|
||||
onWorkspaceResourceSelect={onWorkspaceResourceSelect}
|
||||
smoothStreaming={!hasStructuredSegments}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -15,6 +15,7 @@ import { UserMessageContent } from '@/app/workspace/[workspaceId]/home/component
|
||||
import type {
|
||||
ChatMessage,
|
||||
FileAttachmentForApi,
|
||||
MothershipResource,
|
||||
QueuedMessage,
|
||||
} from '@/app/workspace/[workspaceId]/home/types'
|
||||
import { useAutoScroll } from '@/hooks/use-auto-scroll'
|
||||
@@ -38,6 +39,7 @@ interface MothershipChatProps {
|
||||
chatId?: string
|
||||
onContextAdd?: (context: ChatContext) => void
|
||||
onContextRemove?: (context: ChatContext) => void
|
||||
onWorkspaceResourceSelect?: (resource: MothershipResource) => void
|
||||
editValue?: string
|
||||
onEditValueConsumed?: () => void
|
||||
layout?: 'mothership-view' | 'copilot-view'
|
||||
@@ -85,6 +87,7 @@ export function MothershipChat({
|
||||
chatId,
|
||||
onContextAdd,
|
||||
onContextRemove,
|
||||
onWorkspaceResourceSelect,
|
||||
editValue,
|
||||
onEditValueConsumed,
|
||||
layout = 'mothership-view',
|
||||
@@ -175,6 +178,7 @@ export function MothershipChat({
|
||||
fallbackContent={msg.content}
|
||||
isStreaming={isThisStreaming}
|
||||
onOptionSelect={isLastMessage ? onSubmit : undefined}
|
||||
onWorkspaceResourceSelect={onWorkspaceResourceSelect}
|
||||
/>
|
||||
{!isThisStreaming && (msg.content || msg.contentBlocks?.length) && (
|
||||
<div className='mt-2.5'>
|
||||
|
||||
@@ -17,7 +17,7 @@ import { useChatHistory, useMarkTaskRead } from '@/hooks/queries/tasks'
|
||||
import type { ChatContext } from '@/stores/panel'
|
||||
import { MothershipChat, MothershipView, TemplatePrompts, UserInput } from './components'
|
||||
import { getMothershipUseChatOptions, useChat, useMothershipResize } from './hooks'
|
||||
import type { FileAttachmentForApi, MothershipResourceType } from './types'
|
||||
import type { FileAttachmentForApi, MothershipResource, MothershipResourceType } from './types'
|
||||
|
||||
const logger = createLogger('Home')
|
||||
|
||||
@@ -299,6 +299,17 @@ export function Home({ chatId }: HomeProps = {}) {
|
||||
[resolveResourceFromContext, removeResource]
|
||||
)
|
||||
|
||||
const handleWorkspaceResourceSelect = useCallback(
|
||||
(resource: MothershipResource) => {
|
||||
const wasAdded = addResource(resource)
|
||||
if (!wasAdded) {
|
||||
setActiveResourceId(resource.id)
|
||||
}
|
||||
handleResourceEvent()
|
||||
},
|
||||
[addResource, handleResourceEvent, setActiveResourceId]
|
||||
)
|
||||
|
||||
const hasMessages = messages.length > 0
|
||||
|
||||
useEffect(() => {
|
||||
@@ -368,6 +379,7 @@ export function Home({ chatId }: HomeProps = {}) {
|
||||
chatId={resolvedChatId}
|
||||
onContextAdd={handleContextAdd}
|
||||
onContextRemove={handleContextRemove}
|
||||
onWorkspaceResourceSelect={handleWorkspaceResourceSelect}
|
||||
editValue={editingInputValue}
|
||||
onEditValueConsumed={clearEditingValue}
|
||||
animateInput={isInputEntering}
|
||||
|
||||
@@ -54,8 +54,9 @@ export interface WorkspaceMdData {
|
||||
connectorTypes?: string[]
|
||||
}>
|
||||
tables: Array<{ id: string; name: string; description?: string | null; rowCount: number }>
|
||||
files: Array<{ name: string; type: string; size: number }>
|
||||
credentials: Array<{ providerId: string }>
|
||||
files: Array<{ id: string; name: string; type: string; size: number }>
|
||||
oauthIntegrations: Array<{ providerId: string }>
|
||||
envVariables: string[]
|
||||
tasks?: Array<{ id: string; title: string; updatedAt: Date }>
|
||||
customTools?: Array<{ id: string; name: string }>
|
||||
mcpServers?: Array<{ id: string; name: string; url?: string | null; enabled: boolean }>
|
||||
@@ -158,21 +159,28 @@ export function buildWorkspaceMd(data: WorkspaceMdData): string {
|
||||
}
|
||||
|
||||
if (data.files.length > 0) {
|
||||
const lines = data.files.map((f) => `- **${f.name}** (${f.type}, ${formatSize(f.size)})`)
|
||||
const lines = data.files.map(
|
||||
(f) => `- **${f.name}** (${f.id}) — ${f.type}, ${formatSize(f.size)}`
|
||||
)
|
||||
sections.push(`## Files (${data.files.length})\n${lines.join('\n')}`)
|
||||
} else {
|
||||
sections.push('## Files (0)\n(none)')
|
||||
}
|
||||
|
||||
if (data.credentials.length > 0) {
|
||||
const providers = [...new Set(data.credentials.map((c) => c.providerId))]
|
||||
if (data.oauthIntegrations.length > 0) {
|
||||
const providers = [...new Set(data.oauthIntegrations.map((c) => c.providerId))]
|
||||
const lines = providers.map((p) => {
|
||||
const services = PROVIDER_SERVICES[p]
|
||||
return services ? `- ${p} (${services.join(', ')})` : `- ${p}`
|
||||
})
|
||||
sections.push(`## Connected Services\n${lines.join('\n')}`)
|
||||
sections.push(`## Connected Integrations\n${lines.join('\n')}`)
|
||||
} else {
|
||||
sections.push('## Connected Services\n(none)')
|
||||
sections.push('## Connected Integrations\n(none)')
|
||||
}
|
||||
|
||||
if (data.envVariables.length > 0) {
|
||||
const lines = data.envVariables.map((v) => `- ${v}`)
|
||||
sections.push(`## Environment Variables (${data.envVariables.length})\n${lines.join('\n')}`)
|
||||
}
|
||||
|
||||
if (data.customTools && data.customTools.length > 0) {
|
||||
@@ -360,8 +368,9 @@ export async function generateWorkspaceContext(
|
||||
connectorTypes: connectorTypesByKb.get(kb.id),
|
||||
})),
|
||||
tables: tables.map((t, i) => ({ ...t, rowCount: rowCounts[i] ?? 0 })),
|
||||
files: files.map((f) => ({ name: f.name, type: f.type, size: f.size })),
|
||||
credentials: credentials.map((c) => ({ providerId: c.providerId })),
|
||||
files: files.map((f) => ({ id: f.id, name: f.name, type: f.type, size: f.size })),
|
||||
oauthIntegrations: credentials.map((c) => ({ providerId: c.providerId })),
|
||||
envVariables: [],
|
||||
customTools: customTools.map((t) => ({ id: t.id, name: t.title })),
|
||||
mcpServers: mcpServerRows,
|
||||
skills: skillRows.map((s) => ({ id: s.id, name: s.name, description: s.description })),
|
||||
@@ -382,7 +391,7 @@ export async function generateWorkspaceContext(
|
||||
workspaceId,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
})
|
||||
return '## Workspace\n(unavailable)\n\n## Workflows\n(unavailable)\n\n## Knowledge Bases\n(unavailable)\n\n## Tables\n(unavailable)\n\n## Files\n(unavailable)\n\n## Credentials\n(unavailable)'
|
||||
return '## Workspace\n(unavailable)\n\n## Workflows\n(unavailable)\n\n## Knowledge Bases\n(unavailable)\n\n## Tables\n(unavailable)\n\n## Files\n(unavailable)\n\n## Connected Integrations\n(unavailable)'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -380,7 +380,8 @@ export class WorkspaceVFS {
|
||||
knowledgeBases: kbSummary,
|
||||
tables: tblSummary,
|
||||
files: fileSummary,
|
||||
credentials: envSummary,
|
||||
oauthIntegrations: envSummary.oauthIntegrations,
|
||||
envVariables: envSummary.envVariables,
|
||||
tasks: taskSummary,
|
||||
customTools: toolsSummary,
|
||||
mcpServers: mcpServersSummary,
|
||||
@@ -776,7 +777,7 @@ export class WorkspaceVFS {
|
||||
)
|
||||
}
|
||||
|
||||
return files.map((f) => ({ name: f.name, type: f.type, size: f.size }))
|
||||
return files.map((f) => ({ id: f.id, name: f.name, type: f.type, size: f.size }))
|
||||
} catch (err) {
|
||||
logger.warn('Failed to materialize files', {
|
||||
workspaceId,
|
||||
@@ -1219,7 +1220,10 @@ export class WorkspaceVFS {
|
||||
private async materializeEnvironment(
|
||||
workspaceId: string,
|
||||
userId: string
|
||||
): Promise<WorkspaceMdData['credentials']> {
|
||||
): Promise<{
|
||||
oauthIntegrations: WorkspaceMdData['oauthIntegrations']
|
||||
envVariables: WorkspaceMdData['envVariables']
|
||||
}> {
|
||||
try {
|
||||
const [envCredentials, oauthCredentials, apiKeyRows, envData] = await Promise.all([
|
||||
getAccessibleEnvCredentials(workspaceId, userId),
|
||||
@@ -1255,16 +1259,18 @@ export class WorkspaceVFS {
|
||||
serializeEnvironmentVariables(personalVarNames, workspaceVarNames)
|
||||
)
|
||||
|
||||
const envKeys = envCredentials.map((c) => c.envKey)
|
||||
const oauthProviders = oauthCredentials.map((c) => c.providerId)
|
||||
const allProviders = [...new Set([...oauthProviders, ...envKeys])]
|
||||
return allProviders.map((key) => ({ providerId: key }))
|
||||
const oauthProviders = [...new Set(oauthCredentials.map((c) => c.providerId))]
|
||||
const envKeys = [...new Set(envCredentials.map((c) => c.envKey))]
|
||||
return {
|
||||
oauthIntegrations: oauthProviders.map((key) => ({ providerId: key })),
|
||||
envVariables: envKeys,
|
||||
}
|
||||
} catch (err) {
|
||||
logger.warn('Failed to materialize environment data', {
|
||||
workspaceId,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
})
|
||||
return []
|
||||
return { oauthIntegrations: [], envVariables: [] }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user