mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
fix(copilot): expand tool metadata, fix thinking text rendering, clean up display logic (#3779)
* fix(copilot): expand tool metadata, fix thinking text rendering, clean up display logic * fix(copilot): guard null reasoning data, use ensureTextBlock for thinking end * fix(copilot): restore displayTitle precedence so cancelled tools show 'Stopped by user'
This commit is contained in:
@@ -6,7 +6,7 @@ import { env } from '@/lib/core/config/env'
|
||||
|
||||
const logger = createLogger('CopilotAutoAllowedToolsAPI')
|
||||
|
||||
/** Headers for server-to-server calls to the Go copilot backend. */
|
||||
/** Headers for server-to-server calls to the copilot backend. */
|
||||
function copilotHeaders(): Record<string, string> {
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -36,7 +36,7 @@ export async function GET() {
|
||||
)
|
||||
|
||||
if (!res.ok) {
|
||||
logger.warn('Go backend returned error for list auto-allowed', { status: res.status })
|
||||
logger.warn('Copilot returned error for list auto-allowed', { status: res.status })
|
||||
return NextResponse.json({ autoAllowedTools: [] })
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ export async function POST(request: NextRequest) {
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
logger.warn('Go backend returned error for add auto-allowed', { status: res.status })
|
||||
logger.warn('Copilot returned error for add auto-allowed', { status: res.status })
|
||||
return NextResponse.json({ error: 'Failed to add tool' }, { status: 500 })
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ export async function DELETE(request: NextRequest) {
|
||||
)
|
||||
|
||||
if (!res.ok) {
|
||||
logger.warn('Go backend returned error for remove auto-allowed', { status: res.status })
|
||||
logger.warn('Copilot returned error for remove auto-allowed', { status: res.status })
|
||||
return NextResponse.json({ error: 'Failed to remove tool' }, { status: 500 })
|
||||
}
|
||||
|
||||
|
||||
@@ -176,7 +176,7 @@ export async function POST(req: NextRequest) {
|
||||
const workflowId = resolved.workflowId
|
||||
const workflowResolvedName = resolved.workflowName
|
||||
|
||||
// Resolve workspace from workflow so it can be sent as implicit context to the Go backend.
|
||||
// Resolve workspace from workflow so it can be sent as implicit context to the copilot.
|
||||
let resolvedWorkspaceId: string | undefined
|
||||
try {
|
||||
const { getWorkflowById } = await import('@/lib/workflows/utils')
|
||||
|
||||
+19
-46
@@ -1,11 +1,21 @@
|
||||
'use client'
|
||||
|
||||
import { resolveToolDisplay } from '@/lib/copilot/store-utils'
|
||||
import { ClientToolCallState } from '@/lib/copilot/tools/client/tool-display-registry'
|
||||
import type { ContentBlock, OptionItem, SubagentName, ToolCallData } from '../../types'
|
||||
import { SUBAGENT_LABELS, TOOL_UI_METADATA } from '../../types'
|
||||
import type { AgentGroupItem } from './components'
|
||||
import { AgentGroup, ChatContent, CircleStop, Options, PendingTagIndicator } from './components'
|
||||
import type { AgentGroupItem } from '@/app/workspace/[workspaceId]/home/components/message-content/components'
|
||||
import {
|
||||
AgentGroup,
|
||||
ChatContent,
|
||||
CircleStop,
|
||||
Options,
|
||||
PendingTagIndicator,
|
||||
} from '@/app/workspace/[workspaceId]/home/components/message-content/components'
|
||||
import type {
|
||||
ContentBlock,
|
||||
MothershipToolName,
|
||||
OptionItem,
|
||||
SubagentName,
|
||||
ToolCallData,
|
||||
} from '@/app/workspace/[workspaceId]/home/types'
|
||||
import { SUBAGENT_LABELS, TOOL_UI_METADATA } from '@/app/workspace/[workspaceId]/home/types'
|
||||
|
||||
interface TextSegment {
|
||||
type: 'text'
|
||||
@@ -45,16 +55,8 @@ const SUBAGENT_DISPATCH_TOOLS: Record<string, string> = {
|
||||
file_write: 'workspace_file',
|
||||
}
|
||||
|
||||
function formatToolName(name: string): string {
|
||||
return name
|
||||
.replace(/_v\d+$/, '')
|
||||
.split('_')
|
||||
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
||||
.join(' ')
|
||||
}
|
||||
|
||||
function resolveAgentLabel(key: string): string {
|
||||
return SUBAGENT_LABELS[key as SubagentName] ?? formatToolName(key)
|
||||
return SUBAGENT_LABELS[key as SubagentName] ?? key
|
||||
}
|
||||
|
||||
function isToolDone(status: ToolCallData['status']): boolean {
|
||||
@@ -65,41 +67,12 @@ function isDelegatingTool(tc: NonNullable<ContentBlock['toolCall']>): boolean {
|
||||
return tc.status === 'executing'
|
||||
}
|
||||
|
||||
function mapToolStatusToClientState(
|
||||
status: ContentBlock['toolCall'] extends { status: infer T } ? T : string
|
||||
) {
|
||||
switch (status) {
|
||||
case 'success':
|
||||
return ClientToolCallState.success
|
||||
case 'error':
|
||||
return ClientToolCallState.error
|
||||
case 'cancelled':
|
||||
return ClientToolCallState.cancelled
|
||||
default:
|
||||
return ClientToolCallState.executing
|
||||
}
|
||||
}
|
||||
|
||||
function getOverrideDisplayTitle(tc: NonNullable<ContentBlock['toolCall']>): string | undefined {
|
||||
if (tc.name === 'read' || tc.name.endsWith('_respond')) {
|
||||
return resolveToolDisplay(tc.name, mapToolStatusToClientState(tc.status), tc.id, tc.params)
|
||||
?.text
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function toToolData(tc: NonNullable<ContentBlock['toolCall']>): ToolCallData {
|
||||
const overrideDisplayTitle = getOverrideDisplayTitle(tc)
|
||||
const displayTitle =
|
||||
overrideDisplayTitle ||
|
||||
tc.displayTitle ||
|
||||
TOOL_UI_METADATA[tc.name as keyof typeof TOOL_UI_METADATA]?.title ||
|
||||
formatToolName(tc.name)
|
||||
|
||||
return {
|
||||
id: tc.id,
|
||||
toolName: tc.name,
|
||||
displayTitle,
|
||||
displayTitle:
|
||||
tc.displayTitle ?? TOOL_UI_METADATA[tc.name as MothershipToolName]?.title ?? tc.name,
|
||||
status: tc.status,
|
||||
params: tc.params,
|
||||
result: tc.result,
|
||||
|
||||
@@ -23,34 +23,95 @@ import {
|
||||
} from '@/components/emcn'
|
||||
import { Table as TableIcon } from '@/components/emcn/icons'
|
||||
import { AgentIcon } from '@/components/icons'
|
||||
import type { MothershipToolName, SubagentName } from '../../types'
|
||||
import type { MothershipToolName, SubagentName } from '@/app/workspace/[workspaceId]/home/types'
|
||||
|
||||
export type IconComponent = ComponentType<SVGProps<SVGSVGElement>>
|
||||
|
||||
const TOOL_ICONS: Record<MothershipToolName | SubagentName | 'mothership', IconComponent> = {
|
||||
mothership: Blimp,
|
||||
// Workspace
|
||||
glob: FolderCode,
|
||||
grep: Search,
|
||||
read: File,
|
||||
// Search
|
||||
search_online: Search,
|
||||
scrape_page: Search,
|
||||
get_page_contents: Search,
|
||||
search_library_docs: Library,
|
||||
manage_mcp_tool: Settings,
|
||||
manage_skill: Asterisk,
|
||||
user_memory: Database,
|
||||
crawl_website: Search,
|
||||
// Execution
|
||||
function_execute: TerminalWindow,
|
||||
superagent: Blimp,
|
||||
user_table: TableIcon,
|
||||
workspace_file: File,
|
||||
run_workflow: PlayOutline,
|
||||
run_block: PlayOutline,
|
||||
run_from_block: PlayOutline,
|
||||
run_workflow_until_block: PlayOutline,
|
||||
complete_job: PlayOutline,
|
||||
get_execution_summary: ClipboardList,
|
||||
get_job_logs: ClipboardList,
|
||||
get_workflow_logs: ClipboardList,
|
||||
get_workflow_data: Layout,
|
||||
get_block_outputs: ClipboardList,
|
||||
get_block_upstream_references: ClipboardList,
|
||||
get_deployed_workflow_state: Rocket,
|
||||
check_deployment_status: Rocket,
|
||||
// Workflows & folders
|
||||
create_workflow: Layout,
|
||||
delete_workflow: Layout,
|
||||
edit_workflow: Pencil,
|
||||
rename_workflow: Pencil,
|
||||
move_workflow: Layout,
|
||||
create_folder: FolderCode,
|
||||
delete_folder: FolderCode,
|
||||
move_folder: FolderCode,
|
||||
list_folders: FolderCode,
|
||||
list_user_workspaces: Layout,
|
||||
revert_to_version: Rocket,
|
||||
get_deployment_version: Rocket,
|
||||
open_resource: Eye,
|
||||
// Files
|
||||
workspace_file: File,
|
||||
download_to_workspace_file: File,
|
||||
materialize_file: File,
|
||||
generate_image: File,
|
||||
generate_visualization: File,
|
||||
// Tables & knowledge
|
||||
user_table: TableIcon,
|
||||
knowledge_base: Database,
|
||||
// Jobs
|
||||
create_job: Calendar,
|
||||
manage_job: Calendar,
|
||||
update_job_history: Calendar,
|
||||
// Management
|
||||
manage_mcp_tool: Settings,
|
||||
manage_skill: Asterisk,
|
||||
manage_credential: Integration,
|
||||
manage_custom_tool: Wrench,
|
||||
update_workspace_mcp_server: Settings,
|
||||
delete_workspace_mcp_server: Settings,
|
||||
create_workspace_mcp_server: Settings,
|
||||
list_workspace_mcp_servers: Settings,
|
||||
oauth_get_auth_link: Integration,
|
||||
oauth_request_access: Integration,
|
||||
set_environment_variables: Settings,
|
||||
set_global_workflow_variables: Settings,
|
||||
get_platform_actions: Settings,
|
||||
search_documentation: Library,
|
||||
search_patterns: Search,
|
||||
deploy_api: Rocket,
|
||||
deploy_chat: Rocket,
|
||||
deploy_mcp: Rocket,
|
||||
redeploy: Rocket,
|
||||
generate_api_key: Asterisk,
|
||||
user_memory: Database,
|
||||
context_write: Pencil,
|
||||
context_compaction: Asterisk,
|
||||
// Subagents
|
||||
build: Hammer,
|
||||
run: PlayOutline,
|
||||
deploy: Rocket,
|
||||
auth: Integration,
|
||||
knowledge: Database,
|
||||
knowledge_base: Database,
|
||||
table: TableIcon,
|
||||
job: Calendar,
|
||||
agent: AgentIcon,
|
||||
@@ -60,8 +121,6 @@ const TOOL_ICONS: Record<MothershipToolName | SubagentName | 'mothership', IconC
|
||||
debug: Bug,
|
||||
edit: Pencil,
|
||||
fast_edit: Pencil,
|
||||
context_compaction: Asterisk,
|
||||
open_resource: Eye,
|
||||
file_write: File,
|
||||
}
|
||||
|
||||
|
||||
@@ -166,6 +166,13 @@ function buildReplayStream(events: StreamEventEnvelope[]): ReadableStream<Uint8A
|
||||
}
|
||||
|
||||
function mapStoredBlock(block: TaskStoredContentBlock): ContentBlock {
|
||||
if (block.type === 'thinking') {
|
||||
return {
|
||||
type: 'text',
|
||||
content: block.content ? `<thinking>${block.content}</thinking>` : '',
|
||||
}
|
||||
}
|
||||
|
||||
const mapped: ContentBlock = {
|
||||
type: block.type as ContentBlockType,
|
||||
content: block.content,
|
||||
@@ -1078,6 +1085,36 @@ export function useChat(
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'reasoning': {
|
||||
const d = (
|
||||
parsed.data && typeof parsed.data === 'object' ? parsed.data : {}
|
||||
) as Record<string, unknown>
|
||||
const phase = d.phase as string | undefined
|
||||
if (phase === 'start') {
|
||||
const tb = ensureTextBlock()
|
||||
tb.content = `${tb.content ?? ''}<thinking>`
|
||||
runningText += '<thinking>'
|
||||
streamingContentRef.current = runningText
|
||||
flush()
|
||||
} else if (phase === 'end') {
|
||||
const tb = ensureTextBlock()
|
||||
tb.content = `${tb.content ?? ''}</thinking>`
|
||||
runningText += '</thinking>'
|
||||
streamingContentRef.current = runningText
|
||||
flush()
|
||||
} else {
|
||||
const chunk =
|
||||
typeof d.data === 'string' ? d.data : (parsed.content as string | undefined)
|
||||
if (chunk) {
|
||||
const tb = ensureTextBlock()
|
||||
tb.content = (tb.content ?? '') + chunk
|
||||
runningText += chunk
|
||||
streamingContentRef.current = runningText
|
||||
flush()
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'tool_generating':
|
||||
case 'tool_call': {
|
||||
const id = parsed.toolCallId
|
||||
|
||||
@@ -73,13 +73,63 @@ export type MothershipToolName =
|
||||
| 'search_library_docs'
|
||||
| 'manage_mcp_tool'
|
||||
| 'manage_skill'
|
||||
| 'manage_credential'
|
||||
| 'manage_custom_tool'
|
||||
| 'manage_job'
|
||||
| 'user_memory'
|
||||
| 'function_execute'
|
||||
| 'superagent'
|
||||
| 'user_table'
|
||||
| 'workspace_file'
|
||||
| 'create_workflow'
|
||||
| 'delete_workflow'
|
||||
| 'edit_workflow'
|
||||
| 'rename_workflow'
|
||||
| 'move_workflow'
|
||||
| 'run_workflow'
|
||||
| 'run_block'
|
||||
| 'run_from_block'
|
||||
| 'run_workflow_until_block'
|
||||
| 'create_folder'
|
||||
| 'delete_folder'
|
||||
| 'move_folder'
|
||||
| 'list_folders'
|
||||
| 'list_user_workspaces'
|
||||
| 'create_job'
|
||||
| 'complete_job'
|
||||
| 'update_job_history'
|
||||
| 'download_to_workspace_file'
|
||||
| 'materialize_file'
|
||||
| 'context_write'
|
||||
| 'generate_image'
|
||||
| 'generate_visualization'
|
||||
| 'crawl_website'
|
||||
| 'get_execution_summary'
|
||||
| 'get_job_logs'
|
||||
| 'get_deployment_version'
|
||||
| 'revert_to_version'
|
||||
| 'check_deployment_status'
|
||||
| 'get_deployed_workflow_state'
|
||||
| 'get_workflow_data'
|
||||
| 'get_workflow_logs'
|
||||
| 'get_block_outputs'
|
||||
| 'get_block_upstream_references'
|
||||
| 'set_global_workflow_variables'
|
||||
| 'set_environment_variables'
|
||||
| 'get_platform_actions'
|
||||
| 'search_documentation'
|
||||
| 'search_patterns'
|
||||
| 'update_workspace_mcp_server'
|
||||
| 'delete_workspace_mcp_server'
|
||||
| 'create_workspace_mcp_server'
|
||||
| 'list_workspace_mcp_servers'
|
||||
| 'deploy_api'
|
||||
| 'deploy_chat'
|
||||
| 'deploy_mcp'
|
||||
| 'redeploy'
|
||||
| 'generate_api_key'
|
||||
| 'oauth_get_auth_link'
|
||||
| 'oauth_request_access'
|
||||
| 'build'
|
||||
| 'run'
|
||||
| 'deploy'
|
||||
@@ -237,132 +287,171 @@ export interface ToolUIMetadata {
|
||||
}
|
||||
|
||||
/**
|
||||
* Default UI metadata for tools observed in the SSE stream.
|
||||
* The Go backend sends `ui` on some tool_call events; this map provides
|
||||
* fallback metadata for tools that arrive via `tool_generating` without `ui`.
|
||||
* Primary UI metadata for tools observed in the SSE stream.
|
||||
* Maps tool IDs to human-readable display names shown in the chat.
|
||||
* This is the single source of truth — server-sent `ui.title` values are not used.
|
||||
*/
|
||||
export const TOOL_UI_METADATA: Partial<Record<MothershipToolName, ToolUIMetadata>> = {
|
||||
glob: {
|
||||
title: 'Searching files',
|
||||
phaseLabel: 'Workspace',
|
||||
phase: 'workspace',
|
||||
},
|
||||
grep: {
|
||||
title: 'Searching code',
|
||||
phaseLabel: 'Workspace',
|
||||
phase: 'workspace',
|
||||
},
|
||||
export const TOOL_UI_METADATA: Record<MothershipToolName, ToolUIMetadata> = {
|
||||
// Workspace
|
||||
glob: { title: 'Searching workspace', phaseLabel: 'Workspace', phase: 'workspace' },
|
||||
grep: { title: 'Searching workspace', phaseLabel: 'Workspace', phase: 'workspace' },
|
||||
read: { title: 'Reading file', phaseLabel: 'Workspace', phase: 'workspace' },
|
||||
search_online: {
|
||||
title: 'Searching online',
|
||||
phaseLabel: 'Search',
|
||||
phase: 'search',
|
||||
},
|
||||
scrape_page: {
|
||||
title: 'Scraping page',
|
||||
phaseLabel: 'Search',
|
||||
phase: 'search',
|
||||
},
|
||||
get_page_contents: {
|
||||
title: 'Getting page contents',
|
||||
phaseLabel: 'Search',
|
||||
phase: 'search',
|
||||
},
|
||||
search_library_docs: {
|
||||
title: 'Searching library docs',
|
||||
phaseLabel: 'Search',
|
||||
phase: 'search',
|
||||
},
|
||||
manage_mcp_tool: {
|
||||
title: 'Managing MCP tool',
|
||||
phaseLabel: 'Management',
|
||||
phase: 'management',
|
||||
},
|
||||
manage_skill: {
|
||||
title: 'Managing skill',
|
||||
phaseLabel: 'Management',
|
||||
phase: 'management',
|
||||
},
|
||||
user_memory: {
|
||||
title: 'Accessing memory',
|
||||
phaseLabel: 'Management',
|
||||
phase: 'management',
|
||||
},
|
||||
function_execute: {
|
||||
title: 'Running code',
|
||||
phaseLabel: 'Code',
|
||||
// Search
|
||||
search_online: { title: 'Searching online', phaseLabel: 'Search', phase: 'search' },
|
||||
scrape_page: { title: 'Reading webpage', phaseLabel: 'Search', phase: 'search' },
|
||||
get_page_contents: { title: 'Reading page', phaseLabel: 'Search', phase: 'search' },
|
||||
search_library_docs: { title: 'Searching docs', phaseLabel: 'Search', phase: 'search' },
|
||||
crawl_website: { title: 'Browsing website', phaseLabel: 'Search', phase: 'search' },
|
||||
// Execution
|
||||
function_execute: { title: 'Running code', phaseLabel: 'Code', phase: 'execution' },
|
||||
superagent: { title: 'Taking action', phaseLabel: 'Action', phase: 'execution' },
|
||||
run_workflow: { title: 'Running workflow', phaseLabel: 'Execution', phase: 'execution' },
|
||||
run_block: { title: 'Running block', phaseLabel: 'Execution', phase: 'execution' },
|
||||
run_from_block: { title: 'Running from block', phaseLabel: 'Execution', phase: 'execution' },
|
||||
run_workflow_until_block: {
|
||||
title: 'Running partial workflow',
|
||||
phaseLabel: 'Execution',
|
||||
phase: 'execution',
|
||||
},
|
||||
superagent: {
|
||||
title: 'Executing action',
|
||||
phaseLabel: 'Action',
|
||||
complete_job: { title: 'Completing job', phaseLabel: 'Execution', phase: 'execution' },
|
||||
get_execution_summary: { title: 'Checking results', phaseLabel: 'Execution', phase: 'execution' },
|
||||
get_job_logs: { title: 'Checking logs', phaseLabel: 'Execution', phase: 'execution' },
|
||||
get_workflow_logs: { title: 'Checking logs', phaseLabel: 'Execution', phase: 'execution' },
|
||||
get_workflow_data: { title: 'Loading workflow', phaseLabel: 'Execution', phase: 'execution' },
|
||||
get_block_outputs: {
|
||||
title: 'Checking block outputs',
|
||||
phaseLabel: 'Execution',
|
||||
phase: 'execution',
|
||||
},
|
||||
user_table: {
|
||||
title: 'Managing table',
|
||||
get_block_upstream_references: {
|
||||
title: 'Checking references',
|
||||
phaseLabel: 'Execution',
|
||||
phase: 'execution',
|
||||
},
|
||||
get_deployed_workflow_state: {
|
||||
title: 'Checking deployment',
|
||||
phaseLabel: 'Execution',
|
||||
phase: 'execution',
|
||||
},
|
||||
check_deployment_status: {
|
||||
title: 'Checking deployment',
|
||||
phaseLabel: 'Execution',
|
||||
phase: 'execution',
|
||||
},
|
||||
// Workflows & folders
|
||||
create_workflow: { title: 'Creating workflow', phaseLabel: 'Resource', phase: 'resource' },
|
||||
delete_workflow: { title: 'Deleting workflow', phaseLabel: 'Resource', phase: 'resource' },
|
||||
edit_workflow: { title: 'Editing workflow', phaseLabel: 'Resource', phase: 'resource' },
|
||||
rename_workflow: { title: 'Renaming workflow', phaseLabel: 'Resource', phase: 'resource' },
|
||||
move_workflow: { title: 'Moving workflow', phaseLabel: 'Resource', phase: 'resource' },
|
||||
create_folder: { title: 'Creating folder', phaseLabel: 'Resource', phase: 'resource' },
|
||||
delete_folder: { title: 'Deleting folder', phaseLabel: 'Resource', phase: 'resource' },
|
||||
move_folder: { title: 'Moving folder', phaseLabel: 'Resource', phase: 'resource' },
|
||||
list_folders: { title: 'Browsing folders', phaseLabel: 'Resource', phase: 'resource' },
|
||||
list_user_workspaces: { title: 'Browsing workspaces', phaseLabel: 'Resource', phase: 'resource' },
|
||||
revert_to_version: { title: 'Restoring version', phaseLabel: 'Resource', phase: 'resource' },
|
||||
get_deployment_version: {
|
||||
title: 'Checking deployment',
|
||||
phaseLabel: 'Resource',
|
||||
phase: 'resource',
|
||||
},
|
||||
workspace_file: {
|
||||
title: 'Managing file',
|
||||
open_resource: { title: 'Opening resource', phaseLabel: 'Resource', phase: 'resource' },
|
||||
// Files
|
||||
workspace_file: { title: 'Working with files', phaseLabel: 'Resource', phase: 'resource' },
|
||||
download_to_workspace_file: {
|
||||
title: 'Downloading file',
|
||||
phaseLabel: 'Resource',
|
||||
phase: 'resource',
|
||||
},
|
||||
create_workflow: {
|
||||
title: 'Creating workflow',
|
||||
materialize_file: { title: 'Saving file', phaseLabel: 'Resource', phase: 'resource' },
|
||||
generate_image: { title: 'Generating image', phaseLabel: 'Resource', phase: 'resource' },
|
||||
generate_visualization: {
|
||||
title: 'Generating visualization',
|
||||
phaseLabel: 'Resource',
|
||||
phase: 'resource',
|
||||
},
|
||||
edit_workflow: {
|
||||
title: 'Editing workflow',
|
||||
phaseLabel: 'Resource',
|
||||
phase: 'resource',
|
||||
// Tables & knowledge
|
||||
user_table: { title: 'Editing table', phaseLabel: 'Resource', phase: 'resource' },
|
||||
knowledge_base: { title: 'Updating knowledge base', phaseLabel: 'Resource', phase: 'resource' },
|
||||
// Jobs
|
||||
create_job: { title: 'Creating job', phaseLabel: 'Resource', phase: 'resource' },
|
||||
manage_job: { title: 'Updating job', phaseLabel: 'Management', phase: 'management' },
|
||||
update_job_history: { title: 'Updating job', phaseLabel: 'Management', phase: 'management' },
|
||||
// Management
|
||||
manage_mcp_tool: { title: 'Updating integration', phaseLabel: 'Management', phase: 'management' },
|
||||
manage_skill: { title: 'Updating skill', phaseLabel: 'Management', phase: 'management' },
|
||||
manage_credential: { title: 'Connecting account', phaseLabel: 'Management', phase: 'management' },
|
||||
manage_custom_tool: { title: 'Updating tool', phaseLabel: 'Management', phase: 'management' },
|
||||
update_workspace_mcp_server: {
|
||||
title: 'Updating MCP server',
|
||||
phaseLabel: 'Management',
|
||||
phase: 'management',
|
||||
},
|
||||
delete_workspace_mcp_server: {
|
||||
title: 'Removing MCP server',
|
||||
phaseLabel: 'Management',
|
||||
phase: 'management',
|
||||
},
|
||||
create_workspace_mcp_server: {
|
||||
title: 'Creating MCP server',
|
||||
phaseLabel: 'Management',
|
||||
phase: 'management',
|
||||
},
|
||||
list_workspace_mcp_servers: {
|
||||
title: 'Browsing MCP servers',
|
||||
phaseLabel: 'Management',
|
||||
phase: 'management',
|
||||
},
|
||||
oauth_get_auth_link: {
|
||||
title: 'Connecting account',
|
||||
phaseLabel: 'Management',
|
||||
phase: 'management',
|
||||
},
|
||||
oauth_request_access: {
|
||||
title: 'Connecting account',
|
||||
phaseLabel: 'Management',
|
||||
phase: 'management',
|
||||
},
|
||||
set_environment_variables: {
|
||||
title: 'Updating environment',
|
||||
phaseLabel: 'Management',
|
||||
phase: 'management',
|
||||
},
|
||||
set_global_workflow_variables: {
|
||||
title: 'Updating variables',
|
||||
phaseLabel: 'Management',
|
||||
phase: 'management',
|
||||
},
|
||||
get_platform_actions: { title: 'Loading actions', phaseLabel: 'Management', phase: 'management' },
|
||||
search_documentation: { title: 'Searching docs', phaseLabel: 'Search', phase: 'search' },
|
||||
search_patterns: { title: 'Searching patterns', phaseLabel: 'Search', phase: 'search' },
|
||||
deploy_api: { title: 'Deploying API', phaseLabel: 'Deploy', phase: 'management' },
|
||||
deploy_chat: { title: 'Deploying chat', phaseLabel: 'Deploy', phase: 'management' },
|
||||
deploy_mcp: { title: 'Deploying MCP', phaseLabel: 'Deploy', phase: 'management' },
|
||||
redeploy: { title: 'Redeploying', phaseLabel: 'Deploy', phase: 'management' },
|
||||
generate_api_key: { title: 'Generating API key', phaseLabel: 'Deploy', phase: 'management' },
|
||||
user_memory: { title: 'Updating memory', phaseLabel: 'Management', phase: 'management' },
|
||||
context_write: { title: 'Writing notes', phaseLabel: 'Management', phase: 'management' },
|
||||
context_compaction: {
|
||||
title: 'Optimizing context',
|
||||
phaseLabel: 'Management',
|
||||
phase: 'management',
|
||||
},
|
||||
// Subagents
|
||||
build: { title: 'Building', phaseLabel: 'Build', phase: 'subagent' },
|
||||
run: { title: 'Running', phaseLabel: 'Run', phase: 'subagent' },
|
||||
deploy: { title: 'Deploying', phaseLabel: 'Deploy', phase: 'subagent' },
|
||||
auth: {
|
||||
title: 'Connecting credentials',
|
||||
phaseLabel: 'Auth',
|
||||
phase: 'subagent',
|
||||
},
|
||||
knowledge: {
|
||||
title: 'Managing knowledge',
|
||||
phaseLabel: 'Knowledge',
|
||||
phase: 'subagent',
|
||||
},
|
||||
knowledge_base: {
|
||||
title: 'Managing knowledge base',
|
||||
phaseLabel: 'Resource',
|
||||
phase: 'resource',
|
||||
},
|
||||
table: { title: 'Managing tables', phaseLabel: 'Table', phase: 'subagent' },
|
||||
job: { title: 'Managing jobs', phaseLabel: 'Job', phase: 'subagent' },
|
||||
agent: { title: 'Agent action', phaseLabel: 'Agent', phase: 'subagent' },
|
||||
custom_tool: {
|
||||
title: 'Creating tool',
|
||||
phaseLabel: 'Tool',
|
||||
phase: 'subagent',
|
||||
},
|
||||
auth: { title: 'Connecting integration', phaseLabel: 'Auth', phase: 'subagent' },
|
||||
knowledge: { title: 'Working with knowledge', phaseLabel: 'Knowledge', phase: 'subagent' },
|
||||
table: { title: 'Working with tables', phaseLabel: 'Table', phase: 'subagent' },
|
||||
job: { title: 'Working with jobs', phaseLabel: 'Job', phase: 'subagent' },
|
||||
agent: { title: 'Taking action', phaseLabel: 'Agent', phase: 'subagent' },
|
||||
custom_tool: { title: 'Creating tool', phaseLabel: 'Tool', phase: 'subagent' },
|
||||
research: { title: 'Researching', phaseLabel: 'Research', phase: 'subagent' },
|
||||
plan: { title: 'Planning', phaseLabel: 'Plan', phase: 'subagent' },
|
||||
debug: { title: 'Debugging', phaseLabel: 'Debug', phase: 'subagent' },
|
||||
edit: { title: 'Editing workflow', phaseLabel: 'Edit', phase: 'subagent' },
|
||||
fast_edit: {
|
||||
title: 'Editing workflow',
|
||||
phaseLabel: 'Edit',
|
||||
phase: 'subagent',
|
||||
},
|
||||
open_resource: {
|
||||
title: 'Opening resource',
|
||||
phaseLabel: 'Resource',
|
||||
phase: 'resource',
|
||||
},
|
||||
context_compaction: {
|
||||
title: 'Compacted context',
|
||||
phaseLabel: 'Context',
|
||||
phase: 'management',
|
||||
},
|
||||
fast_edit: { title: 'Editing workflow', phaseLabel: 'Edit', phase: 'subagent' },
|
||||
}
|
||||
|
||||
export interface SSEPayloadUI {
|
||||
|
||||
@@ -78,7 +78,7 @@ function abortPendingToolIfStreamDead(
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the `ui` object from a Go SSE event. The Go backend enriches
|
||||
* Extract the `ui` object from an SSE event. The server enriches
|
||||
* tool_call events with `ui: { requiresConfirmation, clientExecutable, ... }`.
|
||||
*/
|
||||
function getEventUI(event: SSEEvent): {
|
||||
@@ -206,7 +206,7 @@ function handleClientCompletion(
|
||||
|
||||
/**
|
||||
* Emit a synthetic tool_result SSE event to the client after a client-executable
|
||||
* tool completes. The Go backend's actual tool_result is skipped (markToolResultSeen),
|
||||
* tool completes. The server's actual tool_result is skipped (markToolResultSeen),
|
||||
* so the client would never learn the outcome without this.
|
||||
*/
|
||||
async function emitSyntheticToolResult(
|
||||
@@ -580,7 +580,7 @@ export const sseHandlers: Record<string, SSEHandler> = {
|
||||
context.currentThinkingBlock.content = `${context.currentThinkingBlock.content || ''}${chunk}`
|
||||
},
|
||||
content: (event, context) => {
|
||||
// Go backend sends content as a plain string in event.data, not wrapped in an object.
|
||||
// Server sends content as a plain string in event.data, not wrapped in an object.
|
||||
let chunk: string | undefined
|
||||
if (typeof event.data === 'string') {
|
||||
chunk = event.data
|
||||
@@ -639,7 +639,7 @@ export const subAgentHandlers: Record<string, SSEHandler> = {
|
||||
content: (event, context) => {
|
||||
const parentToolCallId = context.subAgentParentToolCallId
|
||||
if (!parentToolCallId || !event.data) return
|
||||
// Go backend sends content as a plain string in event.data
|
||||
// Server sends content as a plain string in event.data
|
||||
let chunk: string | undefined
|
||||
if (typeof event.data === 'string') {
|
||||
chunk = event.data
|
||||
|
||||
@@ -747,11 +747,11 @@ export async function executeToolAndReport(
|
||||
}
|
||||
|
||||
// Fire-and-forget: notify the copilot backend that the tool completed.
|
||||
// IMPORTANT: We must NOT await this — the Go backend may block on the
|
||||
// IMPORTANT: We must NOT await this — the server may block on the
|
||||
// mark-complete handler until it can write back on the SSE stream, but
|
||||
// the SSE reader (our for-await loop) is paused while we're in this
|
||||
// handler. Awaiting here would deadlock: sim waits for Go's response,
|
||||
// Go waits for sim to drain the SSE stream.
|
||||
// handler. Awaiting here would deadlock: sim waits for the server's response,
|
||||
// the server waits for sim to drain the SSE stream.
|
||||
markToolComplete(
|
||||
toolCall.id,
|
||||
toolCall.name,
|
||||
|
||||
@@ -864,7 +864,6 @@ async function generateOAuthLink(
|
||||
},
|
||||
})
|
||||
|
||||
// Use Better Auth's server-side API with the real request headers (session cookie).
|
||||
const { auth } = await import('@/lib/auth/auth')
|
||||
const { headers: getHeaders } = await import('next/headers')
|
||||
const reqHeaders = await getHeaders()
|
||||
@@ -1163,10 +1162,10 @@ const SIM_WORKFLOW_TOOL_HANDLERS: Record<
|
||||
/**
|
||||
* Check whether a tool can be executed on the Sim (TypeScript) side.
|
||||
*
|
||||
* Tools that are only available on the Go backend (e.g. search_patterns)
|
||||
* Tools that are only available server-side (e.g. search_patterns)
|
||||
* will return false. The subagent tool_call
|
||||
* handler uses this to decide whether to execute a tool locally or let the
|
||||
* Go backend's own tool_result SSE event handle it.
|
||||
* server's own tool_result SSE event handle it.
|
||||
*/
|
||||
export function isToolAvailableOnSimSide(toolName: string): boolean {
|
||||
if (SERVER_TOOLS.has(toolName)) return true
|
||||
|
||||
@@ -23,7 +23,7 @@ export type SSEEventType =
|
||||
|
||||
export interface SSEEvent {
|
||||
type: SSEEventType
|
||||
/** Authoritative tool call state set by the Go backend */
|
||||
/** Authoritative tool call state set by the server */
|
||||
state?: string
|
||||
data?: Record<string, unknown>
|
||||
/** Parent agent that produced this event */
|
||||
|
||||
@@ -17,15 +17,13 @@ export type {
|
||||
|
||||
const logger = createLogger('CopilotResources')
|
||||
|
||||
type ChatResource = MothershipResource
|
||||
|
||||
/**
|
||||
* Appends resources to a chat's JSONB resources column, deduplicating by type+id.
|
||||
* Updates the title of existing resources if the new title is more specific.
|
||||
*/
|
||||
export async function persistChatResources(
|
||||
chatId: string,
|
||||
newResources: ChatResource[]
|
||||
newResources: MothershipResource[]
|
||||
): Promise<void> {
|
||||
const toMerge = newResources.filter((r) => r.id !== 'streaming-file')
|
||||
if (toMerge.length === 0) return
|
||||
@@ -39,8 +37,8 @@ export async function persistChatResources(
|
||||
|
||||
if (!chat) return
|
||||
|
||||
const existing = Array.isArray(chat.resources) ? (chat.resources as ChatResource[]) : []
|
||||
const map = new Map<string, ChatResource>()
|
||||
const existing = Array.isArray(chat.resources) ? (chat.resources as MothershipResource[]) : []
|
||||
const map = new Map<string, MothershipResource>()
|
||||
const GENERIC = new Set(['Table', 'File', 'Workflow', 'Knowledge Base'])
|
||||
|
||||
for (const r of existing) {
|
||||
@@ -72,7 +70,10 @@ export async function persistChatResources(
|
||||
/**
|
||||
* Removes resources from a chat's JSONB resources column by type+id.
|
||||
*/
|
||||
export async function removeChatResources(chatId: string, toRemove: ChatResource[]): Promise<void> {
|
||||
export async function removeChatResources(
|
||||
chatId: string,
|
||||
toRemove: MothershipResource[]
|
||||
): Promise<void> {
|
||||
if (toRemove.length === 0) return
|
||||
|
||||
try {
|
||||
@@ -84,7 +85,7 @@ export async function removeChatResources(chatId: string, toRemove: ChatResource
|
||||
|
||||
if (!chat) return
|
||||
|
||||
const existing = Array.isArray(chat.resources) ? (chat.resources as ChatResource[]) : []
|
||||
const existing = Array.isArray(chat.resources) ? (chat.resources as MothershipResource[]) : []
|
||||
const removeKeys = new Set(toRemove.map((r) => `${r.type}:${r.id}`))
|
||||
const filtered = existing.filter((r) => !removeKeys.has(`${r.type}:${r.id}`))
|
||||
|
||||
|
||||
@@ -1,39 +1,3 @@
|
||||
import type { LucideIcon } from 'lucide-react'
|
||||
|
||||
export enum ClientToolCallState {
|
||||
generating = 'generating',
|
||||
pending = 'pending',
|
||||
executing = 'executing',
|
||||
aborted = 'aborted',
|
||||
rejected = 'rejected',
|
||||
success = 'success',
|
||||
error = 'error',
|
||||
cancelled = 'cancelled',
|
||||
review = 'review',
|
||||
background = 'background',
|
||||
}
|
||||
|
||||
export interface ClientToolDisplay {
|
||||
text: string
|
||||
icon: LucideIcon
|
||||
}
|
||||
|
||||
export interface BaseClientToolMetadata {
|
||||
displayNames: Partial<Record<ClientToolCallState, ClientToolDisplay>>
|
||||
uiConfig?: Record<string, unknown>
|
||||
getDynamicText?: (
|
||||
params: Record<string, unknown>,
|
||||
state: ClientToolCallState
|
||||
) => string | undefined
|
||||
}
|
||||
|
||||
export type DynamicTextFormatter = (
|
||||
params: Record<string, unknown>,
|
||||
state: ClientToolCallState
|
||||
) => string | undefined
|
||||
|
||||
export const WORKFLOW_EXECUTION_TIMEOUT_MS = 10 * 60 * 1000
|
||||
|
||||
/** Event detail for OAuth connect events dispatched by the copilot. */
|
||||
export interface OAuthConnectEventDetail {
|
||||
providerName: string
|
||||
|
||||
@@ -5,7 +5,7 @@ export const WORKFLOW_TOOL_NAMES = [
|
||||
'run_from_block',
|
||||
] as const
|
||||
|
||||
export const WORKFLOW_TOOL_NAME_SET = new Set<string>(WORKFLOW_TOOL_NAMES)
|
||||
const WORKFLOW_TOOL_NAME_SET = new Set<string>(WORKFLOW_TOOL_NAMES)
|
||||
|
||||
export function isWorkflowToolName(name: string): boolean {
|
||||
return WORKFLOW_TOOL_NAME_SET.has(name)
|
||||
|
||||
Reference in New Issue
Block a user