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:
Waleed
2026-03-25 22:53:58 -07:00
committed by GitHub
parent 6610c37e10
commit 2771b679cb
13 changed files with 340 additions and 218 deletions
@@ -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 })
}
+1 -1
View File
@@ -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')
@@ -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
+192 -103
View File
@@ -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
+1 -1
View File
@@ -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 */
+8 -7
View File
@@ -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
+1 -1
View File
@@ -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)