mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-10 00:05:27 +00:00
feat(debug): wire ScriptEditor to the agent controller and human preempt
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
e5f5611676
commit
b513998d0e
@@ -73,8 +73,11 @@
|
||||
fetchContextualVariables,
|
||||
signDebugRequest,
|
||||
signMultiplayerRequest,
|
||||
getDebugErrorMessage
|
||||
getDebugErrorMessage,
|
||||
preemptToHuman,
|
||||
type DebugController
|
||||
} from '$lib/components/debug'
|
||||
import type { DAPClient } from '$lib/components/debug'
|
||||
import { SvelteSet } from 'svelte/reactivity'
|
||||
import { setLicense } from '$lib/enterpriseUtils'
|
||||
import type { ScriptEditorWhitelabelCustomUi } from './custom_ui'
|
||||
@@ -1205,51 +1208,96 @@
|
||||
monacoEditor.revealLineInCenter(line)
|
||||
}
|
||||
|
||||
// `onClientReady` fires after connect+initialize but before launch, so the agent can
|
||||
// attach its transition listener in time to catch the first breakpoint stop. Rethrows
|
||||
// so the agent path can report a signing/connection failure to the model.
|
||||
async function runDebugSession(opts?: {
|
||||
agentArgs?: Record<string, unknown>
|
||||
agentOriginated?: boolean
|
||||
onClientReady?: (client: DAPClient) => void
|
||||
}): Promise<void> {
|
||||
showDebugConsole = true
|
||||
selectedDebugFrameId = null
|
||||
|
||||
if (!debugMode) {
|
||||
debugMode = true
|
||||
if (testPanelSize === 0) {
|
||||
expandTestPanel()
|
||||
}
|
||||
}
|
||||
|
||||
resetDAPClient()
|
||||
dapClient = getDAPClient(dapServerUrl)
|
||||
|
||||
const env = await fetchContextualVariables(opWs ?? '')
|
||||
|
||||
const signedPayload = await signDebugRequest(
|
||||
opWs ?? '',
|
||||
code ?? '',
|
||||
lang ?? 'python3',
|
||||
opts?.agentOriginated
|
||||
)
|
||||
debugSessionJobId = signedPayload.job_id
|
||||
|
||||
await dapClient.connect()
|
||||
await dapClient.initialize()
|
||||
opts?.onClientReady?.(dapClient)
|
||||
await dapClient.setBreakpoints(debugFilePath, Array.from(debugBreakpoints))
|
||||
await dapClient.configurationDone()
|
||||
await dapClient.launch({
|
||||
code,
|
||||
cwd: '/tmp',
|
||||
args: opts?.agentArgs ?? args ?? {},
|
||||
callMain: true,
|
||||
env,
|
||||
token: signedPayload.token
|
||||
})
|
||||
}
|
||||
|
||||
async function startDebugging(): Promise<void> {
|
||||
try {
|
||||
// Show console when starting a debug session
|
||||
showDebugConsole = true
|
||||
// Reset selected frame when starting new session
|
||||
selectedDebugFrameId = null
|
||||
|
||||
// Always reset and create a fresh DAP client with the correct URL for the current language
|
||||
// This ensures we connect to the correct endpoint even if language changed
|
||||
resetDAPClient()
|
||||
dapClient = getDAPClient(dapServerUrl)
|
||||
|
||||
// Fetch contextual variables (WM_WORKSPACE, WM_TOKEN, etc.) from backend
|
||||
const env = await fetchContextualVariables(opWs ?? '')
|
||||
|
||||
// Sign the debug request (creates audit log entry)
|
||||
let signedPayload
|
||||
try {
|
||||
signedPayload = await signDebugRequest(opWs ?? '', code ?? '', lang ?? 'python3')
|
||||
debugSessionJobId = signedPayload.job_id
|
||||
} catch (signError) {
|
||||
sendUserToast(getDebugErrorMessage(signError), true)
|
||||
return
|
||||
}
|
||||
|
||||
await dapClient.connect()
|
||||
await dapClient.initialize()
|
||||
await dapClient.setBreakpoints(debugFilePath, Array.from(debugBreakpoints))
|
||||
await dapClient.configurationDone()
|
||||
// Pass the signed token along with other launch parameters
|
||||
await dapClient.launch({
|
||||
code,
|
||||
cwd: '/tmp',
|
||||
args: args ?? {},
|
||||
callMain: true,
|
||||
env,
|
||||
// JWT token for verification by the debugger
|
||||
token: signedPayload.token
|
||||
})
|
||||
// Claim after the reset (runDebugSession's resetDAPClient clears the owner);
|
||||
// claiming before it would be wiped, leaving the session seizable by the agent.
|
||||
await runDebugSession({ onClientReady: () => preemptToHuman() })
|
||||
} catch (error) {
|
||||
console.error('Failed to start debugging:', error)
|
||||
sendUserToast(getDebugErrorMessage(error), true)
|
||||
}
|
||||
}
|
||||
|
||||
const agentDebugController: DebugController = {
|
||||
language: () => lang ?? 'python3',
|
||||
start: async (agentArgs, onClientReady) => {
|
||||
await runDebugSession({ agentArgs, agentOriginated: true, onClientReady })
|
||||
},
|
||||
stop: () => stopDebugging(),
|
||||
signExpression: (expression) => signAgentExpression(expression),
|
||||
setBreakpoints: async (lines) => {
|
||||
debugBreakpoints.clear()
|
||||
for (const line of lines) {
|
||||
debugBreakpoints.add(line)
|
||||
}
|
||||
updateBreakpointDecorations()
|
||||
await syncBreakpointsWithServer()
|
||||
}
|
||||
}
|
||||
|
||||
async function signAgentExpression(expression: string): Promise<string | undefined> {
|
||||
if (!opWs || !debugSessionJobId) return undefined
|
||||
try {
|
||||
const response = await fetch(`/api/w/${opWs}/debug/sign_expression`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ expression, job_id: debugSessionJobId, agent_originated: true })
|
||||
})
|
||||
if (!response.ok) return undefined
|
||||
const result = await response.json()
|
||||
return result.token
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
async function stopDebugging(): Promise<void> {
|
||||
if (!dapClient) return
|
||||
try {
|
||||
@@ -1264,21 +1312,25 @@
|
||||
}
|
||||
|
||||
async function continueExecution(): Promise<void> {
|
||||
preemptToHuman()
|
||||
if (!dapClient) return
|
||||
await dapClient.continue_()
|
||||
}
|
||||
|
||||
async function stepOver(): Promise<void> {
|
||||
preemptToHuman()
|
||||
if (!dapClient) return
|
||||
await dapClient.stepOver()
|
||||
}
|
||||
|
||||
async function stepIn(): Promise<void> {
|
||||
preemptToHuman()
|
||||
if (!dapClient) return
|
||||
await dapClient.stepIn()
|
||||
}
|
||||
|
||||
async function stepOut(): Promise<void> {
|
||||
preemptToHuman()
|
||||
if (!dapClient) return
|
||||
await dapClient.stepOut()
|
||||
}
|
||||
@@ -1437,6 +1489,10 @@
|
||||
await inferSchema(code, { applyInitialArgs: true })
|
||||
}
|
||||
aiChatManager.saveAndClear()
|
||||
// Register before changeMode so the debug tools are gated in on first SCRIPT setup.
|
||||
aiChatManager.scriptEditorDebugController = isDebuggableScript
|
||||
? agentDebugController
|
||||
: undefined
|
||||
aiChatManager.changeMode(AIMode.SCRIPT)
|
||||
if (customUi?.previewPanel?.loadLastRunOnMount) {
|
||||
void loadLastRunIntoTestPanel()
|
||||
@@ -1551,6 +1607,7 @@
|
||||
aiChatManager.scriptEditorApplyCode = undefined
|
||||
aiChatManager.scriptEditorShowDiffMode = undefined
|
||||
aiChatManager.scriptEditorGetLintErrors = undefined
|
||||
aiChatManager.scriptEditorDebugController = undefined
|
||||
aiChatManager.scriptEditorOptions = undefined
|
||||
aiChatManager.saveAndClear()
|
||||
aiChatManager.changeMode(AIMode.NAVIGATOR)
|
||||
@@ -1742,6 +1799,9 @@
|
||||
editor?.getLintErrors() ?? { errorCount: 0, warningCount: 0, errors: [], warnings: [] }
|
||||
)
|
||||
}
|
||||
aiChatManager.scriptEditorDebugController = isDebuggableScript
|
||||
? agentDebugController
|
||||
: undefined
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { X, AlertCircle, Trash2 } from 'lucide-svelte'
|
||||
import type { DAPClient } from './dapClient'
|
||||
import { preemptToHuman } from './agentDebugSession'
|
||||
|
||||
interface Props {
|
||||
client: DAPClient | null
|
||||
@@ -94,6 +95,9 @@
|
||||
const expression = inputValue.trim()
|
||||
if (!expression || !client || isEvaluating) return
|
||||
|
||||
// A human typing in the console takes control back from the agent.
|
||||
preemptToHuman()
|
||||
|
||||
// Add to command history
|
||||
if (commandHistory[commandHistory.length - 1] !== expression) {
|
||||
commandHistory = [...commandHistory, expression]
|
||||
@@ -226,7 +230,10 @@
|
||||
<div class="flex items-center gap-1">
|
||||
<button
|
||||
class="p-0.5 hover:bg-[#3c3c3c] rounded text-[#969696] hover:text-[#d4d4d4]"
|
||||
onclick={(e) => { e.stopPropagation(); clearConsole(); }}
|
||||
onclick={(e) => {
|
||||
e.stopPropagation()
|
||||
clearConsole()
|
||||
}}
|
||||
title="Clear console (Ctrl+L)"
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
@@ -234,7 +241,10 @@
|
||||
{#if onClose}
|
||||
<button
|
||||
class="p-0.5 hover:bg-[#3c3c3c] rounded text-[#969696] hover:text-[#d4d4d4]"
|
||||
onclick={(e) => { e.stopPropagation(); onClose?.(); }}
|
||||
onclick={(e) => {
|
||||
e.stopPropagation()
|
||||
onClose?.()
|
||||
}}
|
||||
title="Close console (Esc)"
|
||||
>
|
||||
<X size={14} />
|
||||
@@ -244,10 +254,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Console output -->
|
||||
<div
|
||||
bind:this={consoleRef}
|
||||
class="flex-1 overflow-auto min-h-0"
|
||||
>
|
||||
<div bind:this={consoleRef} class="flex-1 overflow-auto min-h-0">
|
||||
{#if history.length === 0}
|
||||
<div class="px-3 py-2 text-[#969696] text-[11px]">
|
||||
Evaluate expressions in the current scope. Use ↑↓ for history.
|
||||
@@ -263,7 +270,9 @@
|
||||
<span class="text-[#ce9178] break-all whitespace-pre-wrap">{entry.content}</span>
|
||||
{:else if entry.type === 'output'}
|
||||
<span class="text-[#569cd6] mr-2 select-none opacity-0">></span>
|
||||
<span class="{getValueClass(entry.content, 'output')} break-all whitespace-pre-wrap">{formatValue(entry.content, 'output')}</span>
|
||||
<span class="{getValueClass(entry.content, 'output')} break-all whitespace-pre-wrap"
|
||||
>{formatValue(entry.content, 'output')}</span
|
||||
>
|
||||
{:else}
|
||||
<AlertCircle size={12} class="text-red-500 mr-2 mt-0.5 flex-shrink-0" />
|
||||
<span class="text-red-400 break-all whitespace-pre-wrap">{entry.content}</span>
|
||||
@@ -289,7 +298,9 @@
|
||||
spellcheck="false"
|
||||
/>
|
||||
{#if isEvaluating}
|
||||
<div class="w-3 h-3 border border-[#569cd6] border-t-transparent rounded-full animate-spin ml-2"></div>
|
||||
<div
|
||||
class="w-3 h-3 border border-[#569cd6] border-t-transparent rounded-full animate-spin ml-2"
|
||||
></div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -50,7 +50,8 @@ export async function fetchContextualVariables(workspace: string): Promise<Recor
|
||||
export async function signDebugRequest(
|
||||
workspace: string,
|
||||
code: string,
|
||||
language: string
|
||||
language: string,
|
||||
agentOriginated?: boolean
|
||||
): Promise<{ token: string; code: string; job_id: string }> {
|
||||
if (!workspace) {
|
||||
throw new Error('No workspace selected')
|
||||
@@ -59,7 +60,7 @@ export async function signDebugRequest(
|
||||
const response = await fetch(`/api/w/${workspace}/debug/sign`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ code, language })
|
||||
body: JSON.stringify({ code, language, agent_originated: agentOriginated ?? false })
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
|
||||
Reference in New Issue
Block a user