mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
feat(copilot): add user feedback options (#867)
* Feedback v1 * Add yaml previews * Remove logs * Lint * Add user id and chat id to feedback * Lint
This commit is contained in:
committed by
GitHub
parent
eb51d6d3f5
commit
1035aca71e
155
apps/sim/app/api/copilot/feedback/route.ts
Normal file
155
apps/sim/app/api/copilot/feedback/route.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
authenticateCopilotRequestSessionOnly,
|
||||
createBadRequestResponse,
|
||||
createInternalServerErrorResponse,
|
||||
createRequestTracker,
|
||||
createUnauthorizedResponse,
|
||||
} from '@/lib/copilot/auth'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { db } from '@/db'
|
||||
import { copilotFeedback } from '@/db/schema'
|
||||
|
||||
const logger = createLogger('CopilotFeedbackAPI')
|
||||
|
||||
// Schema for feedback submission
|
||||
const FeedbackSchema = z.object({
|
||||
chatId: z.string().uuid('Chat ID must be a valid UUID'),
|
||||
userQuery: z.string().min(1, 'User query is required'),
|
||||
agentResponse: z.string().min(1, 'Agent response is required'),
|
||||
isPositiveFeedback: z.boolean(),
|
||||
feedback: z.string().optional(),
|
||||
workflowYaml: z.string().optional(), // Optional workflow YAML when edit/build workflow tools were used
|
||||
})
|
||||
|
||||
/**
|
||||
* POST /api/copilot/feedback
|
||||
* Submit feedback for a copilot interaction
|
||||
*/
|
||||
export async function POST(req: NextRequest) {
|
||||
const tracker = createRequestTracker()
|
||||
|
||||
try {
|
||||
// Authenticate user using the same pattern as other copilot routes
|
||||
const { userId: authenticatedUserId, isAuthenticated } =
|
||||
await authenticateCopilotRequestSessionOnly()
|
||||
|
||||
if (!isAuthenticated || !authenticatedUserId) {
|
||||
return createUnauthorizedResponse()
|
||||
}
|
||||
|
||||
const body = await req.json()
|
||||
const { chatId, userQuery, agentResponse, isPositiveFeedback, feedback, workflowYaml } =
|
||||
FeedbackSchema.parse(body)
|
||||
|
||||
logger.info(`[${tracker.requestId}] Processing copilot feedback submission`, {
|
||||
userId: authenticatedUserId,
|
||||
chatId,
|
||||
isPositiveFeedback,
|
||||
userQueryLength: userQuery.length,
|
||||
agentResponseLength: agentResponse.length,
|
||||
hasFeedback: !!feedback,
|
||||
hasWorkflowYaml: !!workflowYaml,
|
||||
workflowYamlLength: workflowYaml?.length || 0,
|
||||
})
|
||||
|
||||
// Insert feedback into the database
|
||||
const [feedbackRecord] = await db
|
||||
.insert(copilotFeedback)
|
||||
.values({
|
||||
userId: authenticatedUserId,
|
||||
chatId,
|
||||
userQuery,
|
||||
agentResponse,
|
||||
isPositive: isPositiveFeedback,
|
||||
feedback: feedback || null,
|
||||
workflowYaml: workflowYaml || null,
|
||||
})
|
||||
.returning()
|
||||
|
||||
logger.info(`[${tracker.requestId}] Successfully saved copilot feedback`, {
|
||||
feedbackId: feedbackRecord.feedbackId,
|
||||
userId: authenticatedUserId,
|
||||
isPositive: isPositiveFeedback,
|
||||
duration: tracker.getDuration(),
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
feedbackId: feedbackRecord.feedbackId,
|
||||
message: 'Feedback submitted successfully',
|
||||
metadata: {
|
||||
requestId: tracker.requestId,
|
||||
duration: tracker.getDuration(),
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
const duration = tracker.getDuration()
|
||||
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.error(`[${tracker.requestId}] Validation error:`, {
|
||||
duration,
|
||||
errors: error.errors,
|
||||
})
|
||||
return createBadRequestResponse(
|
||||
`Invalid request data: ${error.errors.map((e) => e.message).join(', ')}`
|
||||
)
|
||||
}
|
||||
|
||||
logger.error(`[${tracker.requestId}] Error submitting copilot feedback:`, {
|
||||
duration,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
})
|
||||
|
||||
return createInternalServerErrorResponse('Failed to submit feedback')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/copilot/feedback
|
||||
* Get all feedback records (for analytics)
|
||||
*/
|
||||
export async function GET(req: NextRequest) {
|
||||
const tracker = createRequestTracker()
|
||||
|
||||
try {
|
||||
// Authenticate user
|
||||
const { userId: authenticatedUserId, isAuthenticated } =
|
||||
await authenticateCopilotRequestSessionOnly()
|
||||
|
||||
if (!isAuthenticated || !authenticatedUserId) {
|
||||
return createUnauthorizedResponse()
|
||||
}
|
||||
|
||||
// Get all feedback records
|
||||
const feedbackRecords = await db
|
||||
.select({
|
||||
feedbackId: copilotFeedback.feedbackId,
|
||||
userId: copilotFeedback.userId,
|
||||
chatId: copilotFeedback.chatId,
|
||||
userQuery: copilotFeedback.userQuery,
|
||||
agentResponse: copilotFeedback.agentResponse,
|
||||
isPositive: copilotFeedback.isPositive,
|
||||
feedback: copilotFeedback.feedback,
|
||||
workflowYaml: copilotFeedback.workflowYaml,
|
||||
createdAt: copilotFeedback.createdAt,
|
||||
})
|
||||
.from(copilotFeedback)
|
||||
|
||||
logger.info(`[${tracker.requestId}] Retrieved ${feedbackRecords.length} feedback records`)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
feedback: feedbackRecords,
|
||||
metadata: {
|
||||
requestId: tracker.requestId,
|
||||
duration: tracker.getDuration(),
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error(`[${tracker.requestId}] Error retrieving copilot feedback:`, error)
|
||||
return createInternalServerErrorResponse('Failed to retrieve feedback')
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import remarkGfm from 'remark-gfm'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { InlineToolCall } from '@/lib/copilot/tools/inline-tool-call'
|
||||
import { usePreviewStore } from '@/stores/copilot/preview-store'
|
||||
import { useCopilotStore } from '@/stores/copilot/store'
|
||||
import type { CopilotMessage as CopilotMessageType } from '@/stores/copilot/types'
|
||||
|
||||
@@ -214,8 +215,17 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
|
||||
messageCheckpoints: allMessageCheckpoints,
|
||||
revertToCheckpoint,
|
||||
isRevertingCheckpoint,
|
||||
currentChat,
|
||||
messages,
|
||||
workflowId,
|
||||
} = useCopilotStore()
|
||||
|
||||
// Get preview store for accessing workflow YAML after rejection
|
||||
const { getPreviewByToolCall, getLatestPendingPreview } = usePreviewStore()
|
||||
|
||||
// Import COPILOT_TOOL_IDS - placing it here since it's needed in multiple functions
|
||||
const WORKFLOW_TOOL_NAMES = ['build_workflow', 'edit_workflow']
|
||||
|
||||
// Get checkpoints for this message if it's a user message
|
||||
const messageCheckpoints = isUser ? allMessageCheckpoints[message.id] || [] : []
|
||||
const hasCheckpoints = messageCheckpoints.length > 0
|
||||
@@ -226,16 +236,187 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
|
||||
setShowCopySuccess(true)
|
||||
}
|
||||
|
||||
const handleUpvote = () => {
|
||||
// Helper function to get the full assistant response content
|
||||
const getFullAssistantContent = (message: CopilotMessageType) => {
|
||||
// First try the direct content
|
||||
if (message.content?.trim()) {
|
||||
return message.content
|
||||
}
|
||||
|
||||
// If no direct content, build from content blocks
|
||||
if (message.contentBlocks && message.contentBlocks.length > 0) {
|
||||
return message.contentBlocks
|
||||
.filter((block) => block.type === 'text')
|
||||
.map((block) => block.content)
|
||||
.join('')
|
||||
}
|
||||
|
||||
return message.content || ''
|
||||
}
|
||||
|
||||
// Helper function to find the last user query before this assistant message
|
||||
const getLastUserQuery = () => {
|
||||
const messageIndex = messages.findIndex((msg) => msg.id === message.id)
|
||||
if (messageIndex === -1) return null
|
||||
|
||||
// Look backwards from this message to find the last user message
|
||||
for (let i = messageIndex - 1; i >= 0; i--) {
|
||||
if (messages[i].role === 'user') {
|
||||
return messages[i].content
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// Helper function to extract workflow YAML from workflow tool calls
|
||||
const getWorkflowYaml = () => {
|
||||
// Step 1: Check both toolCalls array and contentBlocks for workflow tools
|
||||
const allToolCalls = [
|
||||
...(message.toolCalls || []),
|
||||
...(message.contentBlocks || [])
|
||||
.filter((block) => block.type === 'tool_call')
|
||||
.map((block) => (block as any).toolCall),
|
||||
]
|
||||
|
||||
// Find workflow tools (build_workflow or edit_workflow)
|
||||
const workflowTools = allToolCalls.filter((toolCall) =>
|
||||
WORKFLOW_TOOL_NAMES.includes(toolCall?.name)
|
||||
)
|
||||
|
||||
// Extract YAML content from workflow tools in the current message
|
||||
for (const toolCall of workflowTools) {
|
||||
// Try various locations where YAML content might be stored
|
||||
const yamlContent =
|
||||
toolCall.result?.yamlContent ||
|
||||
toolCall.result?.data?.yamlContent ||
|
||||
toolCall.input?.yamlContent ||
|
||||
toolCall.input?.data?.yamlContent
|
||||
|
||||
if (yamlContent && typeof yamlContent === 'string' && yamlContent.trim()) {
|
||||
console.log('Found workflow YAML in tool call:', {
|
||||
toolCallId: toolCall.id,
|
||||
toolName: toolCall.name,
|
||||
yamlLength: yamlContent.length,
|
||||
})
|
||||
return yamlContent
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Check copilot store's preview YAML (set when workflow tools execute)
|
||||
if (currentChat?.previewYaml?.trim()) {
|
||||
console.log('Found workflow YAML in copilot store preview:', {
|
||||
yamlLength: currentChat.previewYaml.length,
|
||||
})
|
||||
return currentChat.previewYaml
|
||||
}
|
||||
|
||||
// Step 3: Check preview store for recent workflow tool calls from this message
|
||||
for (const toolCall of workflowTools) {
|
||||
if (toolCall.id) {
|
||||
const preview = getPreviewByToolCall(toolCall.id)
|
||||
if (preview?.yamlContent?.trim()) {
|
||||
console.log('Found workflow YAML in preview store:', {
|
||||
toolCallId: toolCall.id,
|
||||
previewId: preview.id,
|
||||
yamlLength: preview.yamlContent.length,
|
||||
})
|
||||
return preview.yamlContent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: If this message contains workflow tools but no YAML found yet,
|
||||
// try to get the latest pending preview for this workflow (fallback)
|
||||
if (workflowTools.length > 0 && workflowId) {
|
||||
const latestPreview = getLatestPendingPreview(workflowId, currentChat?.id)
|
||||
if (latestPreview?.yamlContent?.trim()) {
|
||||
console.log('Found workflow YAML in latest pending preview:', {
|
||||
previewId: latestPreview.id,
|
||||
yamlLength: latestPreview.yamlContent.length,
|
||||
})
|
||||
return latestPreview.yamlContent
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// Function to submit feedback
|
||||
const submitFeedback = async (isPositive: boolean) => {
|
||||
// Ensure we have a chat ID
|
||||
if (!currentChat?.id) {
|
||||
console.error('No current chat ID available for feedback submission')
|
||||
return
|
||||
}
|
||||
|
||||
const userQuery = getLastUserQuery()
|
||||
if (!userQuery) {
|
||||
console.error('No user query found for feedback submission')
|
||||
return
|
||||
}
|
||||
|
||||
const agentResponse = getFullAssistantContent(message)
|
||||
if (!agentResponse.trim()) {
|
||||
console.error('No agent response content available for feedback submission')
|
||||
return
|
||||
}
|
||||
|
||||
// Get workflow YAML if this message contains workflow tools
|
||||
const workflowYaml = getWorkflowYaml()
|
||||
|
||||
try {
|
||||
const requestBody: any = {
|
||||
chatId: currentChat.id,
|
||||
userQuery,
|
||||
agentResponse,
|
||||
isPositiveFeedback: isPositive,
|
||||
}
|
||||
|
||||
// Only include workflowYaml if it exists
|
||||
if (workflowYaml) {
|
||||
requestBody.workflowYaml = workflowYaml
|
||||
console.log('Including workflow YAML in feedback:', {
|
||||
yamlLength: workflowYaml.length,
|
||||
yamlPreview: workflowYaml.substring(0, 100),
|
||||
})
|
||||
}
|
||||
|
||||
const response = await fetch('/api/copilot/feedback', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to submit feedback: ${response.statusText}`)
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
console.log('Feedback submitted successfully:', result)
|
||||
} catch (error) {
|
||||
console.error('Error submitting feedback:', error)
|
||||
// Could show a toast or error message to user here
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpvote = async () => {
|
||||
// Reset downvote if it was active
|
||||
setShowDownvoteSuccess(false)
|
||||
setShowUpvoteSuccess(true)
|
||||
|
||||
// Submit positive feedback
|
||||
await submitFeedback(true)
|
||||
}
|
||||
|
||||
const handleDownvote = () => {
|
||||
const handleDownvote = async () => {
|
||||
// Reset upvote if it was active
|
||||
setShowUpvoteSuccess(false)
|
||||
setShowDownvoteSuccess(true)
|
||||
|
||||
// Submit negative feedback
|
||||
await submitFeedback(false)
|
||||
}
|
||||
|
||||
const handleRevertToCheckpoint = () => {
|
||||
|
||||
21
apps/sim/db/migrations/0066_talented_mentor.sql
Normal file
21
apps/sim/db/migrations/0066_talented_mentor.sql
Normal file
@@ -0,0 +1,21 @@
|
||||
CREATE TABLE "copilot_feedback" (
|
||||
"feedback_id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"user_id" text NOT NULL,
|
||||
"chat_id" uuid NOT NULL,
|
||||
"user_query" text NOT NULL,
|
||||
"agent_response" text NOT NULL,
|
||||
"is_positive" boolean NOT NULL,
|
||||
"feedback" text,
|
||||
"workflow_yaml" text,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "user_stats" ALTER COLUMN "current_usage_limit" SET DEFAULT '10';--> statement-breakpoint
|
||||
ALTER TABLE "copilot_feedback" ADD CONSTRAINT "copilot_feedback_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "copilot_feedback" ADD CONSTRAINT "copilot_feedback_chat_id_copilot_chats_id_fk" FOREIGN KEY ("chat_id") REFERENCES "public"."copilot_chats"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
CREATE INDEX "copilot_feedback_user_id_idx" ON "copilot_feedback" USING btree ("user_id");--> statement-breakpoint
|
||||
CREATE INDEX "copilot_feedback_chat_id_idx" ON "copilot_feedback" USING btree ("chat_id");--> statement-breakpoint
|
||||
CREATE INDEX "copilot_feedback_user_chat_idx" ON "copilot_feedback" USING btree ("user_id","chat_id");--> statement-breakpoint
|
||||
CREATE INDEX "copilot_feedback_is_positive_idx" ON "copilot_feedback" USING btree ("is_positive");--> statement-breakpoint
|
||||
CREATE INDEX "copilot_feedback_created_at_idx" ON "copilot_feedback" USING btree ("created_at");
|
||||
5829
apps/sim/db/migrations/meta/0066_snapshot.json
Normal file
5829
apps/sim/db/migrations/meta/0066_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -456,6 +456,13 @@
|
||||
"when": 1754171385971,
|
||||
"tag": "0065_solid_newton_destine",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 66,
|
||||
"version": "7",
|
||||
"when": 1754352106989,
|
||||
"tag": "0066_talented_mentor",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1137,3 +1137,35 @@ export const templateStars = pgTable(
|
||||
),
|
||||
})
|
||||
)
|
||||
|
||||
export const copilotFeedback = pgTable(
|
||||
'copilot_feedback',
|
||||
{
|
||||
feedbackId: uuid('feedback_id').primaryKey().defaultRandom(),
|
||||
userId: text('user_id')
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: 'cascade' }),
|
||||
chatId: uuid('chat_id')
|
||||
.notNull()
|
||||
.references(() => copilotChats.id, { onDelete: 'cascade' }),
|
||||
userQuery: text('user_query').notNull(),
|
||||
agentResponse: text('agent_response').notNull(),
|
||||
isPositive: boolean('is_positive').notNull(),
|
||||
feedback: text('feedback'), // Optional feedback text
|
||||
workflowYaml: text('workflow_yaml'), // Optional workflow YAML if edit/build workflow was triggered
|
||||
createdAt: timestamp('created_at').notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at').notNull().defaultNow(),
|
||||
},
|
||||
(table) => ({
|
||||
// Access patterns
|
||||
userIdIdx: index('copilot_feedback_user_id_idx').on(table.userId),
|
||||
chatIdIdx: index('copilot_feedback_chat_id_idx').on(table.chatId),
|
||||
userChatIdx: index('copilot_feedback_user_chat_idx').on(table.userId, table.chatId),
|
||||
|
||||
// Query patterns
|
||||
isPositiveIdx: index('copilot_feedback_is_positive_idx').on(table.isPositive),
|
||||
|
||||
// Ordering indexes
|
||||
createdAtIdx: index('copilot_feedback_created_at_idx').on(table.createdAt),
|
||||
})
|
||||
)
|
||||
|
||||
@@ -167,12 +167,13 @@ export class RunWorkflowTool extends BaseTool {
|
||||
}
|
||||
// Execution failed
|
||||
const errorMessage = (result as any)?.error || 'Workflow execution failed'
|
||||
const failedDependency = (result as any)?.failedDependency
|
||||
|
||||
// Check if error message is exactly 'skipped' to notify 'rejected' instead of 'errored'
|
||||
const targetState = errorMessage === 'skipped' ? 'rejected' : 'errored'
|
||||
// Check if failedDependency is true to notify 'rejected' instead of 'errored'
|
||||
const targetState = failedDependency === true ? 'rejected' : 'errored'
|
||||
const message =
|
||||
targetState === 'rejected'
|
||||
? `Workflow execution skipped: ${errorMessage}`
|
||||
? `Workflow execution skipped (failed dependency): ${errorMessage}`
|
||||
: `Workflow execution failed: ${errorMessage}`
|
||||
await this.notify(toolCall.id, targetState, message)
|
||||
|
||||
@@ -188,9 +189,10 @@ export class RunWorkflowTool extends BaseTool {
|
||||
setIsExecuting(false)
|
||||
|
||||
const errorMessage = error?.message || 'An unknown error occurred'
|
||||
const failedDependency = error?.failedDependency
|
||||
|
||||
// Check if error message is exactly 'skipped' to notify 'rejected' instead of 'errored'
|
||||
const targetState = errorMessage === 'skipped' ? 'rejected' : 'errored'
|
||||
// Check if failedDependency is true to notify 'rejected' instead of 'errored'
|
||||
const targetState = failedDependency === true ? 'rejected' : 'errored'
|
||||
await this.notify(toolCall.id, targetState, `Workflow execution failed: ${errorMessage}`)
|
||||
|
||||
options?.onStateChange?.(targetState)
|
||||
|
||||
@@ -71,8 +71,12 @@ async function clientAcceptTool(
|
||||
} catch (error) {
|
||||
console.error('Error executing client tool:', error)
|
||||
const errorMessage = error instanceof Error ? error.message : 'Tool execution failed'
|
||||
// Check if error message is exactly 'skipped' to set 'rejected' state instead of 'errored'
|
||||
const targetState = errorMessage === 'skipped' ? 'rejected' : 'errored'
|
||||
const failedDependency =
|
||||
error && typeof error === 'object' && 'failedDependency' in error
|
||||
? (error as any).failedDependency
|
||||
: false
|
||||
// Check if failedDependency is true to set 'rejected' state instead of 'errored'
|
||||
const targetState = failedDependency === true ? 'rejected' : 'errored'
|
||||
setToolCallState(toolCall, targetState, {
|
||||
error: errorMessage,
|
||||
})
|
||||
|
||||
@@ -72,7 +72,7 @@ export const SERVER_TOOL_METADATA: Record<ServerToolId, ToolMetadata> = {
|
||||
ready_for_review: { displayName: 'Workflow ready for review', icon: 'network' },
|
||||
executing: { displayName: 'Building workflow', icon: 'spinner' },
|
||||
success: { displayName: 'Built workflow', icon: 'network' },
|
||||
rejected: { displayName: 'Rejected workflow changes', icon: 'skip' },
|
||||
rejected: { displayName: 'Workflow changes not applied', icon: 'skip' },
|
||||
errored: { displayName: 'Failed to build workflow', icon: 'error' },
|
||||
aborted: { displayName: 'Workflow build aborted', icon: 'x' },
|
||||
accepted: { displayName: 'Built workflow', icon: 'network' },
|
||||
@@ -92,7 +92,7 @@ export const SERVER_TOOL_METADATA: Record<ServerToolId, ToolMetadata> = {
|
||||
ready_for_review: { displayName: 'Workflow changes ready for review', icon: 'network' },
|
||||
executing: { displayName: 'Editing workflow', icon: 'spinner' },
|
||||
success: { displayName: 'Edited workflow', icon: 'network' },
|
||||
rejected: { displayName: 'Rejected workflow changes', icon: 'skip' },
|
||||
rejected: { displayName: 'Workflow changes not applied', icon: 'skip' },
|
||||
errored: { displayName: 'Failed to edit workflow', icon: 'error' },
|
||||
aborted: { displayName: 'Workflow edit aborted', icon: 'x' },
|
||||
accepted: { displayName: 'Edited workflow', icon: 'network' },
|
||||
|
||||
@@ -378,7 +378,7 @@ function processWorkflowToolResult(toolCall: any, result: any, get: () => Copilo
|
||||
/**
|
||||
* Helper function to handle tool execution failure
|
||||
*/
|
||||
function handleToolFailure(toolCall: any, error: string): void {
|
||||
function handleToolFailure(toolCall: any, error: string, failedDependency?: boolean): void {
|
||||
// Don't override terminal states for tools with ready_for_review and interrupt tools
|
||||
if (
|
||||
(toolSupportsReadyForReview(toolCall.name) || toolRequiresInterrupt(toolCall.name)) &&
|
||||
@@ -396,8 +396,8 @@ function handleToolFailure(toolCall: any, error: string): void {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if error message is exactly 'skipped' to set 'rejected' state instead of 'errored'
|
||||
toolCall.state = error === 'skipped' ? 'rejected' : 'errored'
|
||||
// Check if failedDependency is true to set 'rejected' state instead of 'errored'
|
||||
toolCall.state = failedDependency === true ? 'rejected' : 'errored'
|
||||
toolCall.error = error
|
||||
|
||||
// Update displayName to match the error state
|
||||
@@ -681,7 +681,7 @@ const sseHandlers: Record<string, SSEHandler> = {
|
||||
|
||||
// Handle tool result events - simplified
|
||||
tool_result: (data, context, get, set) => {
|
||||
const { toolCallId, result, success, error } = data
|
||||
const { toolCallId, result, success, error, failedDependency } = data
|
||||
|
||||
if (!toolCallId) return
|
||||
|
||||
@@ -723,11 +723,20 @@ const sseHandlers: Record<string, SSEHandler> = {
|
||||
if (toolSupportsReadyForReview(toolCall.name)) {
|
||||
processWorkflowToolResult(toolCall, parsedResult, get)
|
||||
}
|
||||
|
||||
// COMMENTED OUT OLD LOGIC:
|
||||
// finalizeToolCall(toolCall, true, parsedResult, get)
|
||||
} else {
|
||||
// NEW LOGIC: Use centralized state management
|
||||
// Check if failedDependency is true to set 'rejected' state instead of 'errored'
|
||||
// Use the error field first, then fall back to result field, then default message
|
||||
const errorMessage = error || result || 'Tool execution failed'
|
||||
const targetState = errorMessage === 'skipped' ? 'rejected' : 'errored'
|
||||
const targetState = failedDependency === true ? 'rejected' : 'errored'
|
||||
|
||||
setToolCallState(toolCall, targetState, { error: errorMessage })
|
||||
|
||||
// COMMENTED OUT OLD LOGIC:
|
||||
// handleToolFailure(toolCall, result || 'Tool execution failed')
|
||||
}
|
||||
|
||||
// Update both contentBlocks and toolCalls atomically before UI update
|
||||
@@ -990,7 +999,9 @@ const sseHandlers: Record<string, SSEHandler> = {
|
||||
tool_error: (data, context, get, set) => {
|
||||
const toolCall = context.toolCalls.find((tc) => tc.id === data.toolCallId)
|
||||
if (toolCall) {
|
||||
handleToolFailure(toolCall, data.error)
|
||||
// Check if failedDependency is available in the data
|
||||
const failedDependency = data.failedDependency
|
||||
handleToolFailure(toolCall, data.error, failedDependency)
|
||||
updateContentBlockToolCall(context.contentBlocks, data.toolCallId, toolCall)
|
||||
updateStreamingMessage(set, context)
|
||||
}
|
||||
@@ -1002,36 +1013,11 @@ const sseHandlers: Record<string, SSEHandler> = {
|
||||
},
|
||||
}
|
||||
|
||||
// Cache workflow tool IDs for diff-related functionality (keep for diff store integration)
|
||||
const WORKFLOW_TOOL_IDS = new Set<string>([
|
||||
COPILOT_TOOL_IDS.BUILD_WORKFLOW,
|
||||
COPILOT_TOOL_IDS.EDIT_WORKFLOW,
|
||||
])
|
||||
|
||||
// Cache tools that support ready_for_review state for faster lookup
|
||||
function getToolsWithReadyForReview(): Set<string> {
|
||||
const toolsWithReadyForReview = new Set<string>()
|
||||
|
||||
// For now, just return the known workflow tools since we don't have getAllServerToolMetadata
|
||||
// This can be expanded when the registry provides that method
|
||||
return new Set([COPILOT_TOOL_IDS.BUILD_WORKFLOW, COPILOT_TOOL_IDS.EDIT_WORKFLOW])
|
||||
|
||||
/* TODO: Implement when getAllServerToolMetadata is available
|
||||
Object.keys(toolRegistry.getAllServerToolMetadata?.() || {}).forEach(toolId => {
|
||||
if (toolSupportsReadyForReview(toolId)) {
|
||||
toolsWithReadyForReview.add(toolId)
|
||||
}
|
||||
})
|
||||
|
||||
// Check all client tools
|
||||
toolRegistry.getAllTools().forEach(tool => {
|
||||
if (toolSupportsReadyForReview(tool.metadata.id)) {
|
||||
toolsWithReadyForReview.add(tool.metadata.id)
|
||||
}
|
||||
})
|
||||
|
||||
return toolsWithReadyForReview
|
||||
*/
|
||||
}
|
||||
|
||||
// Cache for ready_for_review tools
|
||||
|
||||
Reference in New Issue
Block a user