mirror of
https://github.com/simstudioai/sim.git
synced 2026-02-10 14:45:16 -05:00
* v0 * v1 * Basic ss tes * Ss tests * Stuff * Add mcp * mcp v1 * Improvement * Fix * BROKEN * Checkpoint * Streaming * Fix abort * Things are broken * Streaming seems to work but copilot is dumb * Fix edge issue * LUAAAA * Fix stream buffer * Fix lint * Checkpoint * Initial temp state, in the middle of a refactor * Initial test shows diff store still working * Tool refactor * First cleanup pass complete - untested * Continued cleanup * Refactor * Refactor complete - no testing yet * Fix - cursor makes me sad * Fix mcp * Clean up mcp * Updated mcp * Add respond to subagents * Fix definitions * Add tools * Add tools * Add copilot mcp tracking * Fix lint * Fix mcp * Fix * Updates * Clean up mcp * Fix copilot mcp tool names to be sim prefixed * Add opus 4.6 * Fix discovery tool * Fix * Remove logs * Fix go side tool rendering * Update docs * Fix hydration * Fix tool call resolution * Fix * Fix lint * Fix superagent and autoallow integrations * Fix always allow * Update block * Remove plan docs * Fix hardcoded ff * Fix dropped provider * Fix lint * Fix tests * Fix dead messages array * Fix discovery * Fix run workflow * Fix run block * Fix run from block in copilot * Fix lint * Fix skip and mtb * Fix typing * Fix tool call * Bump api version * Fix bun lock * Nuke bad files
202 lines
6.5 KiB
TypeScript
202 lines
6.5 KiB
TypeScript
import { createLogger } from '@sim/logger'
|
|
import { Loader2 } from 'lucide-react'
|
|
import {
|
|
ClientToolCallState,
|
|
type ClientToolDisplay,
|
|
TOOL_DISPLAY_REGISTRY,
|
|
} from '@/lib/copilot/tools/client/tool-display-registry'
|
|
import type { CopilotStore } from '@/stores/panel/copilot/types'
|
|
|
|
const logger = createLogger('CopilotStoreUtils')
|
|
|
|
type StoreSet = (
|
|
partial: Partial<CopilotStore> | ((state: CopilotStore) => Partial<CopilotStore>)
|
|
) => void
|
|
|
|
/** Respond tools are internal to copilot subagents and should never be shown in the UI */
|
|
const HIDDEN_TOOL_SUFFIX = '_respond'
|
|
|
|
export function resolveToolDisplay(
|
|
toolName: string | undefined,
|
|
state: ClientToolCallState,
|
|
_toolCallId?: string,
|
|
params?: Record<string, any>
|
|
): ClientToolDisplay | undefined {
|
|
if (!toolName) return undefined
|
|
if (toolName.endsWith(HIDDEN_TOOL_SUFFIX)) return undefined
|
|
const entry = TOOL_DISPLAY_REGISTRY[toolName]
|
|
if (!entry) return humanizedFallback(toolName, state)
|
|
|
|
if (entry.uiConfig?.dynamicText && params) {
|
|
const dynamicText = entry.uiConfig.dynamicText(params, state)
|
|
const stateDisplay = entry.displayNames[state]
|
|
if (dynamicText && stateDisplay?.icon) {
|
|
return { text: dynamicText, icon: stateDisplay.icon }
|
|
}
|
|
}
|
|
|
|
const display = entry.displayNames[state]
|
|
if (display?.text || display?.icon) return display
|
|
|
|
const fallbackOrder = [
|
|
ClientToolCallState.generating,
|
|
ClientToolCallState.executing,
|
|
ClientToolCallState.success,
|
|
]
|
|
for (const fallbackState of fallbackOrder) {
|
|
const fallback = entry.displayNames[fallbackState]
|
|
if (fallback?.text || fallback?.icon) return fallback
|
|
}
|
|
|
|
return humanizedFallback(toolName, state)
|
|
}
|
|
|
|
export function humanizedFallback(
|
|
toolName: string,
|
|
state: ClientToolCallState
|
|
): ClientToolDisplay | undefined {
|
|
const formattedName = toolName.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase())
|
|
const stateVerb =
|
|
state === ClientToolCallState.success
|
|
? 'Executed'
|
|
: state === ClientToolCallState.error
|
|
? 'Failed'
|
|
: state === ClientToolCallState.rejected || state === ClientToolCallState.aborted
|
|
? 'Skipped'
|
|
: 'Executing'
|
|
return { text: `${stateVerb} ${formattedName}`, icon: Loader2 }
|
|
}
|
|
|
|
export function isRejectedState(state: string): boolean {
|
|
return state === 'rejected'
|
|
}
|
|
|
|
export function isReviewState(state: string): boolean {
|
|
return state === 'review'
|
|
}
|
|
|
|
export function isBackgroundState(state: string): boolean {
|
|
return state === 'background'
|
|
}
|
|
|
|
export function isTerminalState(state: string): boolean {
|
|
return (
|
|
state === ClientToolCallState.success ||
|
|
state === ClientToolCallState.error ||
|
|
state === ClientToolCallState.rejected ||
|
|
state === ClientToolCallState.aborted ||
|
|
isReviewState(state) ||
|
|
isBackgroundState(state)
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Resolves the appropriate terminal state for a non-terminal tool call.
|
|
* 'executing' → 'success': the server was running it, assume it completed.
|
|
* Everything else → 'aborted': never reached execution.
|
|
*/
|
|
function resolveAbortState(currentState: string): ClientToolCallState {
|
|
return currentState === ClientToolCallState.executing
|
|
? ClientToolCallState.success
|
|
: ClientToolCallState.aborted
|
|
}
|
|
|
|
export function abortAllInProgressTools(set: StoreSet, get: () => CopilotStore) {
|
|
try {
|
|
const { toolCallsById, messages } = get()
|
|
const updatedMap = { ...toolCallsById }
|
|
const resolvedIds = new Map<string, ClientToolCallState>()
|
|
let hasUpdates = false
|
|
for (const [id, tc] of Object.entries(toolCallsById)) {
|
|
const st = tc.state
|
|
const isTerminal =
|
|
st === ClientToolCallState.success ||
|
|
st === ClientToolCallState.error ||
|
|
st === ClientToolCallState.rejected ||
|
|
st === ClientToolCallState.aborted
|
|
if (!isTerminal || isReviewState(st)) {
|
|
const resolved = resolveAbortState(st)
|
|
resolvedIds.set(id, resolved)
|
|
updatedMap[id] = {
|
|
...tc,
|
|
state: resolved,
|
|
subAgentStreaming: false,
|
|
display: resolveToolDisplay(tc.name, resolved, id, tc.params),
|
|
}
|
|
hasUpdates = true
|
|
} else if (tc.subAgentStreaming) {
|
|
updatedMap[id] = {
|
|
...tc,
|
|
subAgentStreaming: false,
|
|
}
|
|
hasUpdates = true
|
|
}
|
|
}
|
|
if (resolvedIds.size > 0 || hasUpdates) {
|
|
set({ toolCallsById: updatedMap })
|
|
set((s: CopilotStore) => {
|
|
const msgs = [...s.messages]
|
|
for (let mi = msgs.length - 1; mi >= 0; mi--) {
|
|
const m = msgs[mi]
|
|
if (m.role !== 'assistant' || !Array.isArray(m.contentBlocks)) continue
|
|
let changed = false
|
|
const blocks = m.contentBlocks.map((b: any) => {
|
|
if (b?.type === 'tool_call' && b.toolCall?.id && resolvedIds.has(b.toolCall.id)) {
|
|
changed = true
|
|
const prev = b.toolCall
|
|
const resolved = resolvedIds.get(b.toolCall.id)!
|
|
return {
|
|
...b,
|
|
toolCall: {
|
|
...prev,
|
|
state: resolved,
|
|
display: resolveToolDisplay(prev?.name, resolved, prev?.id, prev?.params),
|
|
},
|
|
}
|
|
}
|
|
return b
|
|
})
|
|
if (changed) {
|
|
msgs[mi] = { ...m, contentBlocks: blocks }
|
|
break
|
|
}
|
|
}
|
|
return { messages: msgs }
|
|
})
|
|
}
|
|
} catch (error) {
|
|
logger.warn('Failed to abort in-progress tools', {
|
|
error: error instanceof Error ? error.message : String(error),
|
|
})
|
|
}
|
|
}
|
|
|
|
export function cleanupActiveState(
|
|
set: (partial: Record<string, unknown>) => void,
|
|
get: () => Record<string, unknown>
|
|
): void {
|
|
abortAllInProgressTools(set as unknown as StoreSet, get as unknown as () => CopilotStore)
|
|
try {
|
|
const { useWorkflowDiffStore } = require('@/stores/workflow-diff/store') as {
|
|
useWorkflowDiffStore: {
|
|
getState: () => { clearDiff: (options?: { restoreBaseline?: boolean }) => void }
|
|
}
|
|
}
|
|
useWorkflowDiffStore.getState().clearDiff({ restoreBaseline: false })
|
|
} catch (error) {
|
|
logger.warn('Failed to clear diff during cleanup', {
|
|
error: error instanceof Error ? error.message : String(error),
|
|
})
|
|
}
|
|
}
|
|
|
|
export function stripTodoTags(text: string): string {
|
|
if (!text) return text
|
|
return text
|
|
.replace(/<marktodo>[\s\S]*?<\/marktodo>/g, '')
|
|
.replace(/<checkofftodo>[\s\S]*?<\/checkofftodo>/g, '')
|
|
.replace(/<design_workflow>[\s\S]*?<\/design_workflow>/g, '')
|
|
.replace(/[ \t]+\n/g, '\n')
|
|
.replace(/\n{2,}/g, '\n')
|
|
}
|