diff --git a/src/main/modules/ipc/main.ts b/src/main/modules/ipc/main.ts index 55154b123..38804d401 100644 --- a/src/main/modules/ipc/main.ts +++ b/src/main/modules/ipc/main.ts @@ -1032,6 +1032,12 @@ class MainProcessBridge implements MainIpcModule { error?: string needsReconnect?: boolean }> => { + // If connection type is null, the debugger was intentionally disconnected. + // Return a silent failure so the renderer polling ignores it. + if (this.debuggerConnectionType === null) { + return { success: false, error: 'Debugger not connected' } + } + if (this.debuggerConnectionType === 'websocket') { if (!this.debuggerWebSocketClient) { if (this.debuggerReconnecting) { diff --git a/src/renderer/components/_molecules/workspace-activity-bar/tooltip-button.tsx b/src/renderer/components/_molecules/workspace-activity-bar/tooltip-button.tsx index 2bcff9bf3..e0c3a0cd5 100644 --- a/src/renderer/components/_molecules/workspace-activity-bar/tooltip-button.tsx +++ b/src/renderer/components/_molecules/workspace-activity-bar/tooltip-button.tsx @@ -9,7 +9,9 @@ export const TooltipSidebarWrapperButton = ({ return ( - {children} + +
{children}
+
{tooltipContent}
diff --git a/src/renderer/components/_organisms/workspace-activity-bar/default.tsx b/src/renderer/components/_organisms/workspace-activity-bar/default.tsx index ba7c675d1..c129a8cc9 100644 --- a/src/renderer/components/_organisms/workspace-activity-bar/default.tsx +++ b/src/renderer/components/_organisms/workspace-activity-bar/default.tsx @@ -1,17 +1,21 @@ import { StopIcon } from '@root/renderer/assets' -import { StandardFunctionBlocks } from '@root/renderer/data/library/standard-function-blocks' import { compileOnlySelectors } from '@root/renderer/hooks' import { useOpenPLCStore } from '@root/renderer/store' import type { RuntimeConnection } from '@root/renderer/store/slices/device/types' -import { buildDebugTree } from '@root/renderer/utils/debug-tree-builder' -import type { DebugTreeNode, FbInstanceInfo } from '@root/types/debugger' +import { + buildDebugVariableTreeMap, + buildFbInstanceMap, + buildVariableIndexMap, + connectAndActivateDebugger, + disconnectDebugger, +} from '@root/renderer/utils/debugger-session' +import type { DebugTreeNode } from '@root/types/debugger' import { PLCPou, PLCProjectData } from '@root/types/PLC/open-plc' import { BufferToStringArray, cn, isOpenPLCRuntimeTarget, isSimulatorTarget } from '@root/utils' import { addCppLocalVariables } from '@root/utils/cpp/addCppLocalVariables' import { generateSTCode as generateCppSTCode } from '@root/utils/cpp/generateSTCode' import { validateCppCode } from '@root/utils/cpp/validateCppCode' import { parseDebugFile } from '@root/utils/debug-parser' -import { findGlobalVariableIndex, findVariableIndexWithFallback } from '@root/utils/debug-variable-finder' type CppPouData = { name: string @@ -27,7 +31,7 @@ import { parsePlcStatus } from '@root/utils/plc-status' import { addPythonLocalVariables } from '@root/utils/python/addPythonLocalVariables' import { generateSTCode } from '@root/utils/python/generateSTCode' import { injectPythonCode } from '@root/utils/python/injectPythonCode' -import { useEffect, useState } from 'react' +import { useEffect, useRef, useState } from 'react' import { DebuggerButton, @@ -86,6 +90,7 @@ export const DefaultWorkspaceActivityBar = ({ zoom }: DefaultWorkspaceActivityBa const [isCompiling, setIsCompiling] = useState(false) const [isDebuggerProcessing, setIsDebuggerProcessing] = useState(false) const [simulatorRunning, setSimulatorRunning] = useState(false) + const pendingSimulatorDebugRef = useRef(false) const disabledButtonClass = 'disabled cursor-not-allowed opacity-50 [&>*:first-child]:hover:bg-transparent' @@ -115,7 +120,13 @@ export const DefaultWorkspaceActivityBar = ({ zoom }: DefaultWorkspaceActivityBa // (e.g. on project open/create) so the UI reflects the actual state. useEffect(() => { const cleanup = (window.bridge.onSimulatorStopped as (cb: () => void) => () => void)(() => { + pendingSimulatorDebugRef.current = false setSimulatorRunning(false) + // Also clean up debugger state if it was connected via simulator + const { workspace, workspaceActions } = useOpenPLCStore.getState() + if (workspace.isDebuggerVisible) { + void disconnectDebugger(workspaceActions) + } }) return cleanup }, []) @@ -330,11 +341,18 @@ export const DefaultWorkspaceActivityBar = ({ zoom }: DefaultWorkspaceActivityBa ;(window.bridge.simulatorLoadFirmware as (p: string) => Promise<{ success: boolean; error?: string }>)( data.simulatorFirmwarePath, ) - .then((result) => { + .then(async (result) => { if (result.success) { setSimulatorRunning(true) addLog({ id: crypto.randomUUID(), level: 'info', message: 'Simulator is running.' }) + + // Auto-connect debugger after build when triggered from the Start button + if (pendingSimulatorDebugRef.current) { + pendingSimulatorDebugRef.current = false + await connectDebuggerAfterBuild() + } } else { + pendingSimulatorDebugRef.current = false addLog({ id: crypto.randomUUID(), level: 'error', @@ -343,6 +361,7 @@ export const DefaultWorkspaceActivityBar = ({ zoom }: DefaultWorkspaceActivityBa } }) .catch((err: unknown) => { + pendingSimulatorDebugRef.current = false addLog({ id: crypto.randomUUID(), level: 'error', @@ -418,14 +437,23 @@ export const DefaultWorkspaceActivityBar = ({ zoom }: DefaultWorkspaceActivityBa const handleSimulatorControl = async (): Promise => { try { if (simulatorRunning) { + // Stop: disconnect debugger first, then stop simulator + const { workspace, workspaceActions } = useOpenPLCStore.getState() + if (workspace.isDebuggerVisible) { + await disconnectDebugger(workspaceActions) + } await (window.bridge.simulatorStop as () => Promise<{ success: boolean }>)() setSimulatorRunning(false) addLog({ id: crypto.randomUUID(), level: 'info', message: 'Simulator stopped.' }) } else { - // Re-build to get a fresh firmware and start the simulator - void verifyAndCompile() + // Start: build, load firmware, then auto-connect debugger + pendingSimulatorDebugRef.current = true + verifyAndCompile().catch(() => { + pendingSimulatorDebugRef.current = false + }) } } catch (error) { + pendingSimulatorDebugRef.current = false addLog({ id: crypto.randomUUID(), level: 'error', @@ -434,16 +462,110 @@ export const DefaultWorkspaceActivityBar = ({ zoom }: DefaultWorkspaceActivityBa } } + const connectDebuggerAfterBuild = async () => { + const { project, workspaceActions, consoleActions } = useOpenPLCStore.getState() + const boardTarget = deviceDefinitions.configuration.deviceBoard + const projectPath = project.meta.path + + consoleActions.addLog({ + id: crypto.randomUUID(), + level: 'info', + message: 'Starting debugger for simulator...', + }) + + const debugFileResult = await window.bridge.readDebugFile(projectPath, boardTarget) + + if (!debugFileResult.success || !debugFileResult.content) { + consoleActions.addLog({ + id: crypto.randomUUID(), + level: 'error', + message: 'Failed to read debug.c file after compilation.', + }) + return + } + + const parsed = parseDebugFile(debugFileResult.content) + const instances = project.data.configuration.resource.instances + + // Build variable index map + const { indexMap, warnings } = buildVariableIndexMap(project.data.pous, instances, parsed) + for (const w of warnings) { + consoleActions.addLog({ id: crypto.randomUUID(), level: 'warning', message: w }) + } + + // Build debug variable tree + let treeMap = new Map() + try { + const treeResult = buildDebugVariableTreeMap(project.data.pous, instances, parsed.variables, project) + treeMap = treeResult.treeMap + + if (process.env.NODE_ENV === 'development') { + ;(window as Window & { debugTrees?: DebugTreeNode[] }).debugTrees = treeResult.trees + } + + consoleActions.addLog({ + id: crypto.randomUUID(), + level: 'info', + message: `Debug tree builder: Built ${treeResult.trees.length} trees (${treeResult.complexCount} complex).`, + }) + } catch { + consoleActions.addLog({ + id: crypto.randomUUID(), + level: 'warning', + message: 'Debug tree builder encountered errors.', + }) + } + + // Build FB instance map + const fbDebugInstancesMap = buildFbInstanceMap(project.data.pous, instances) + + const fbTypesCount = fbDebugInstancesMap.size + const totalFbInstances = Array.from(fbDebugInstancesMap.values()).reduce((sum, list) => sum + list.length, 0) + if (fbTypesCount > 0) { + consoleActions.addLog({ + id: crypto.randomUUID(), + level: 'info', + message: `FB instance map: Found ${totalFbInstances} instances across ${fbTypesCount} FB types.`, + }) + } + + // Connect and activate debugger + const connectResult = await connectAndActivateDebugger( + { + connectionType: 'simulator', + connectionParams: {}, + indexMap, + treeMap, + fbDebugInstancesMap, + }, + workspaceActions, + ) + + if (!connectResult.success) { + consoleActions.addLog({ + id: crypto.randomUUID(), + level: 'error', + message: `Failed to establish debugger connection: ${connectResult.error || 'Unknown error'}`, + }) + return + } + + consoleActions.addLog({ + id: crypto.randomUUID(), + level: 'info', + message: `Debugger started successfully. Found ${indexMap.size} debug variables.`, + }) + } + const handleDebuggerClick = async () => { + // Simulator target uses the unified Start/Stop flow instead + if (isCurrentBoardSimulator) return + const { workspace, project, deviceDefinitions, workspaceActions, consoleActions, deviceActions } = useOpenPLCStore.getState() if (workspace.isDebuggerVisible) { - const _disconnectResult: { success: boolean } = await window.bridge.debuggerDisconnect() - workspaceActions.setDebuggerVisible(false) - workspaceActions.setDebuggerTargetIp(null) - workspaceActions.setDebugForcedVariables(new Map()) - workspaceActions.clearFbDebugContext() + await disconnectDebugger(workspaceActions) return } @@ -951,161 +1073,41 @@ export const DefaultWorkspaceActivityBar = ({ zoom }: DefaultWorkspaceActivityBa if (debugFileResult.success && debugFileResult.content) { const parsed = parseDebugFile(debugFileResult.content) - const indexMap = new Map() const { project } = useOpenPLCStore.getState() const instances = project.data.configuration.resource.instances - project.data.pous.forEach((pou) => { - if (pou.type !== 'program') return - - const instance = instances.find((inst) => inst.program === pou.data.name) - if (!instance) { - consoleActions.addLog({ - id: crypto.randomUUID(), - level: 'warning', - message: `No instance found for program '${pou.data.name}', skipping debug variable parsing.`, - }) - return - } - - const allVariables = pou.data.variables - - allVariables.forEach((v) => { - // Use fallback to try both FB-style and struct-style paths - // This ensures consistent behavior with OPC-UA index resolution - const index = - v.class === 'external' - ? findGlobalVariableIndex(v.name, parsed.variables) - : findVariableIndexWithFallback(instance.name, v.name, parsed.variables) - if (index !== null) { - const compositeKey = `${pou.data.name}:${v.name}` - indexMap.set(compositeKey, index) - } - }) - }) - - parsed.variables.forEach((debugVar) => { - if (!indexMap.has(debugVar.name)) { - indexMap.set(debugVar.name, debugVar.index) - } - }) + // Build variable index map + const { indexMap, warnings } = buildVariableIndexMap(project.data.pous, instances, parsed) + for (const w of warnings) { + consoleActions.addLog({ id: crypto.randomUUID(), level: 'warning', message: w }) + } + // Build debug variable tree + let treeMap = new Map() try { - const trees: DebugTreeNode[] = [] - const treeMap = new Map() - let complexCount = 0 - - // Helper function to recursively add a node and all its children to the treeMap - // This ensures nested FB variables like main:MOTOR_CONTROL0.TON0 are directly accessible - const addNodeAndChildrenToMap = (node: DebugTreeNode) => { - treeMap.set(node.compositeKey, node) - if (node.children) { - for (const child of node.children) { - addNodeAndChildrenToMap(child) - } - } - } - - project.data.pous.forEach((pou) => { - if (pou.type !== 'program') return - - const instance = instances.find((inst) => inst.program === pou.data.name) - if (!instance) { - return - } - - pou.data.variables.forEach((v) => { - try { - const node = buildDebugTree(v, pou.data.name, instance.name, parsed.variables, project) - trees.push(node) - addNodeAndChildrenToMap(node) - if (node.isComplex) { - complexCount++ - } - } catch { - // Tree building failed for this variable - } - }) - }) - - workspaceActions.setDebugVariableTree(treeMap) + const treeResult = buildDebugVariableTreeMap(project.data.pous, instances, parsed.variables, project) + treeMap = treeResult.treeMap if (process.env.NODE_ENV === 'development') { - ;(window as Window & { debugTrees?: DebugTreeNode[] }).debugTrees = trees + ;(window as Window & { debugTrees?: DebugTreeNode[] }).debugTrees = treeResult.trees } consoleActions.addLog({ id: crypto.randomUUID(), level: 'info', - message: `Debug tree builder: Built ${trees.length} trees (${complexCount} complex).`, + message: `Debug tree builder: Built ${treeResult.trees.length} trees (${treeResult.complexCount} complex).`, }) } catch { consoleActions.addLog({ id: crypto.randomUUID(), level: 'warning', - message: `Debug tree builder encountered errors.`, + message: 'Debug tree builder encountered errors.', }) } - // Build FB instance map for function block debugging - const fbDebugInstancesMap = new Map() - - // Helper function to normalize type strings for comparison - const normalizeTypeString = (typeStr: string): string => { - return typeStr.toLowerCase().replace(/[-_]/g, '') - } - - // Iterate all program POUs to find FB instances - project.data.pous.forEach((pou) => { - if (pou.type !== 'program') return - - const programInstance = instances.find((inst) => inst.program === pou.data.name) - if (!programInstance) return - - // Check each variable in the program to see if it's an FB instance - pou.data.variables.forEach((v) => { - if (v.type.definition !== 'derived') return - - const fbTypeNameRaw = v.type.value - const fbTypeKey = fbTypeNameRaw.toUpperCase() // Canonical key for map lookups - - // Check if this is a standard function block - const isStandardFB = StandardFunctionBlocks.pous.some( - (sfb) => sfb.name.toUpperCase() === fbTypeKey && normalizeTypeString(sfb.type) === 'functionblock', - ) - - // Check if this is a custom function block - const isCustomFB = project.data.pous.some( - (p) => normalizeTypeString(p.type) === 'functionblock' && p.data.name.toUpperCase() === fbTypeKey, - ) - - if (isStandardFB || isCustomFB) { - const instanceInfo: FbInstanceInfo = { - fbTypeName: fbTypeNameRaw, // Keep original name for display - programName: pou.data.name, - programInstanceName: programInstance.name, - fbVariableName: v.name, - key: `${pou.data.name}:${v.name}`, - } - - // Use uppercase key for consistent lookups - const existingInstances = fbDebugInstancesMap.get(fbTypeKey) || [] - existingInstances.push(instanceInfo) - fbDebugInstancesMap.set(fbTypeKey, existingInstances) - } - }) - }) - - // Store FB debug instances and set default selections - workspaceActions.setFbDebugInstances(fbDebugInstancesMap) - - // Set default selected instance for each FB type (first instance) - fbDebugInstancesMap.forEach((instanceList, fbTypeName) => { - if (instanceList.length > 0) { - workspaceActions.setFbSelectedInstance(fbTypeName, instanceList[0].key) - } - }) + // Build FB instance map + const fbDebugInstancesMap = buildFbInstanceMap(project.data.pous, instances) const fbTypesCount = fbDebugInstancesMap.size const totalFbInstances = Array.from(fbDebugInstancesMap.values()).reduce((sum, list) => sum + list.length, 0) @@ -1117,10 +1119,20 @@ export const DefaultWorkspaceActivityBar = ({ zoom }: DefaultWorkspaceActivityBa }) } - const connectResult: { success: boolean; error?: string } = await window.bridge.debuggerConnect( - connectionType, - connectionParams, + // Connect and activate debugger + const connectResult = await connectAndActivateDebugger( + { + connectionType, + connectionParams, + indexMap, + treeMap, + fbDebugInstancesMap, + targetIpAddress, + isRuntimeTarget, + }, + workspaceActions, ) + if (!connectResult.success) { consoleActions.addLog({ id: crypto.randomUUID(), @@ -1131,11 +1143,6 @@ export const DefaultWorkspaceActivityBar = ({ zoom }: DefaultWorkspaceActivityBa return } - workspaceActions.setDebugVariableIndexes(indexMap) - if (!isRuntimeTarget) { - workspaceActions.setDebuggerTargetIp(targetIpAddress ?? null) - } - workspaceActions.setDebuggerVisible(true) consoleActions.addLog({ id: crypto.randomUUID(), level: 'info', @@ -1250,10 +1257,10 @@ export const DefaultWorkspaceActivityBar = ({ zoom }: DefaultWorkspaceActivityBa - + verifyAndCompile()} /> @@ -1287,12 +1294,12 @@ export const DefaultWorkspaceActivityBar = ({ zoom }: DefaultWorkspaceActivityBa {(isCurrentBoardSimulator ? simulatorRunning : plcStatus === 'RUNNING') ? : null} - + void handleDebuggerClick()} - disabled={isDebuggerProcessing} + disabled={isDebuggerProcessing || isCurrentBoardSimulator} isActive={isDebuggerVisible} - className={cn(isDebuggerProcessing && 'cursor-not-allowed opacity-50')} + className={cn((isDebuggerProcessing || isCurrentBoardSimulator) && 'cursor-not-allowed opacity-50')} /> diff --git a/src/renderer/utils/debugger-session.ts b/src/renderer/utils/debugger-session.ts new file mode 100644 index 000000000..755c6f0c1 --- /dev/null +++ b/src/renderer/utils/debugger-session.ts @@ -0,0 +1,257 @@ +/** + * Shared debugger session helpers. + * + * Extracted from the workspace activity bar to eliminate code duplication + * between `connectDebuggerAfterBuild` (simulator flow) and + * `handleMd5Verification` (non-simulator flow). + */ + +import { StandardFunctionBlocks } from '@root/renderer/data/library/standard-function-blocks' +import type { WorkspaceActions } from '@root/renderer/store/slices/workspace/types' +import { buildDebugTree } from '@root/renderer/utils/debug-tree-builder' +import type { DebugTreeNode, FbInstanceInfo } from '@root/types/debugger' +import type { PLCInstance, PLCPou, PLCProject, PLCVariable } from '@root/types/PLC/open-plc' +import type { DebugVariableEntry } from '@root/utils/debug-parser' +import { findGlobalVariableIndex, findVariableIndexWithFallback } from '@root/utils/debug-variable-finder' +import { normalizeTypeString } from '@root/utils/pou-helpers' + +// --------------------------------------------------------------------------- +// 1. disconnectDebugger +// --------------------------------------------------------------------------- + +/** + * Disconnect the debugger and reset all related workspace state. + * Replaces the 5-line disconnect+cleanup pattern used in 3 call sites. + */ +export async function disconnectDebugger(workspaceActions: WorkspaceActions): Promise { + await window.bridge.debuggerDisconnect() + workspaceActions.setDebuggerVisible(false) + workspaceActions.setDebuggerTargetIp(null) + workspaceActions.setDebugForcedVariables(new Map()) + workspaceActions.clearFbDebugContext() +} + +// --------------------------------------------------------------------------- +// 2. buildVariableIndexMap +// --------------------------------------------------------------------------- + +export interface VariableIndexMapResult { + indexMap: Map + warnings: string[] +} + +/** + * Build a composite-key → debug-index map from parsed debug variables. + * Pure function — caller is responsible for logging warnings. + */ +export function buildVariableIndexMap( + pous: PLCPou[], + instances: PLCInstance[], + parsed: { variables: DebugVariableEntry[] }, +): VariableIndexMapResult { + const indexMap = new Map() + const warnings: string[] = [] + + pous.forEach((pou) => { + if (pou.type !== 'program') return + + const instance = instances.find((inst) => inst.program === pou.data.name) + if (!instance) { + warnings.push(`No instance found for program '${pou.data.name}', skipping debug variable parsing.`) + return + } + + pou.data.variables.forEach((v: PLCVariable) => { + const index = + v.class === 'external' + ? findGlobalVariableIndex(v.name, parsed.variables) + : findVariableIndexWithFallback(instance.name, v.name, parsed.variables) + if (index !== null) { + const compositeKey = `${pou.data.name}:${v.name}` + indexMap.set(compositeKey, index) + } + }) + }) + + // Append any unmatched parsed variables as fallback entries + parsed.variables.forEach((debugVar) => { + if (!indexMap.has(debugVar.name)) { + indexMap.set(debugVar.name, debugVar.index) + } + }) + + return { indexMap, warnings } +} + +// --------------------------------------------------------------------------- +// 3. buildDebugVariableTreeMap +// --------------------------------------------------------------------------- + +export interface DebugVariableTreeMapResult { + treeMap: Map + trees: DebugTreeNode[] + complexCount: number +} + +/** + * Build a flat compositeKey → DebugTreeNode map by traversing all program + * POU variables. Pure function — swallows per-variable errors to match + * existing behaviour. + */ +export function buildDebugVariableTreeMap( + pous: PLCPou[], + instances: PLCInstance[], + debugVariables: DebugVariableEntry[], + project: PLCProject, +): DebugVariableTreeMapResult { + const trees: DebugTreeNode[] = [] + const treeMap = new Map() + let complexCount = 0 + + const addNodeAndChildrenToMap = (node: DebugTreeNode) => { + treeMap.set(node.compositeKey, node) + if (node.children) { + for (const child of node.children) { + addNodeAndChildrenToMap(child) + } + } + } + + pous.forEach((pou) => { + if (pou.type !== 'program') return + + const instance = instances.find((inst) => inst.program === pou.data.name) + if (!instance) return + + pou.data.variables.forEach((v: PLCVariable) => { + try { + const node = buildDebugTree(v, pou.data.name, instance.name, debugVariables, project) + trees.push(node) + addNodeAndChildrenToMap(node) + if (node.isComplex) { + complexCount++ + } + } catch { + // Tree building failed for this variable — swallow to match existing behaviour + } + }) + }) + + return { treeMap, trees, complexCount } +} + +// --------------------------------------------------------------------------- +// 4. buildFbInstanceMap +// --------------------------------------------------------------------------- + +/** + * Build a map of FB type name (uppercased) → FbInstanceInfo[] for all + * derived-type variables that are function blocks. + * Uses the canonical `normalizeTypeString` from pou-helpers instead of + * redefining it inline. + */ +export function buildFbInstanceMap(pous: PLCPou[], instances: PLCInstance[]): Map { + const fbDebugInstancesMap = new Map() + + pous.forEach((pou) => { + if (pou.type !== 'program') return + + const programInstance = instances.find((inst) => inst.program === pou.data.name) + if (!programInstance) return + + pou.data.variables.forEach((v: PLCVariable) => { + if (v.type.definition !== 'derived') return + + const fbTypeNameRaw = v.type.value + const fbTypeKey = fbTypeNameRaw.toUpperCase() + + const isStandardFB = StandardFunctionBlocks.pous.some( + (sfb) => sfb.name.toUpperCase() === fbTypeKey && normalizeTypeString(sfb.type) === 'functionblock', + ) + + const isCustomFB = pous.some( + (p) => normalizeTypeString(p.type) === 'functionblock' && p.data.name.toUpperCase() === fbTypeKey, + ) + + if (isStandardFB || isCustomFB) { + const instanceInfo: FbInstanceInfo = { + fbTypeName: fbTypeNameRaw, + programName: pou.data.name, + programInstanceName: programInstance.name, + fbVariableName: v.name, + key: `${pou.data.name}:${v.name}`, + } + + const existingInstances = fbDebugInstancesMap.get(fbTypeKey) || [] + existingInstances.push(instanceInfo) + fbDebugInstancesMap.set(fbTypeKey, existingInstances) + } + }) + }) + + return fbDebugInstancesMap +} + +// --------------------------------------------------------------------------- +// 5. connectAndActivateDebugger +// --------------------------------------------------------------------------- + +export interface ConnectAndActivateParams { + connectionType: 'tcp' | 'rtu' | 'websocket' | 'simulator' + connectionParams: { + ipAddress?: string + port?: string + baudRate?: number + slaveId?: number + jwtToken?: string + } + indexMap: Map + treeMap: Map + fbDebugInstancesMap: Map + targetIpAddress?: string + isRuntimeTarget?: boolean +} + +export interface ConnectAndActivateResult { + success: boolean + error?: string +} + +/** + * Connect to the debugger backend and commit all debug artifacts to the + * Zustand store. Returns `{ success: false, error }` when the bridge + * connection fails. + */ +export async function connectAndActivateDebugger( + params: ConnectAndActivateParams, + workspaceActions: WorkspaceActions, +): Promise { + const connectResult: { success: boolean; error?: string } = await window.bridge.debuggerConnect( + params.connectionType, + params.connectionParams, + ) + + if (!connectResult.success) { + return { success: false, error: connectResult.error || 'Unknown error' } + } + + workspaceActions.setDebugVariableIndexes(params.indexMap) + workspaceActions.setDebugVariableTree(params.treeMap) + workspaceActions.setFbDebugInstances(params.fbDebugInstancesMap) + + // Set default selected instance for each FB type (first instance) + params.fbDebugInstancesMap.forEach((instanceList, fbTypeName) => { + if (instanceList.length > 0) { + workspaceActions.setFbSelectedInstance(fbTypeName, instanceList[0].key) + } + }) + + // Set target IP for non-runtime, non-simulator connections + if (!params.isRuntimeTarget && params.connectionType !== 'simulator') { + workspaceActions.setDebuggerTargetIp(params.targetIpAddress ?? null) + } + + workspaceActions.setDebuggerVisible(true) + + return { success: true } +}