From 3e18c6f5e1fde6f09e2bf71cf0d12175efe69782 Mon Sep 17 00:00:00 2001 From: Florian Magin Date: Thu, 22 Jan 2026 12:31:34 +0100 Subject: [PATCH 1/2] Migrate AI Decomp window to SDK --- .../aidecompiler/AIDecompilationdWindow.java | 50 ++++--- .../services/api/GhidraRevengService.java | 10 +- .../services/api/TypedApiImplementation.java | 18 ++- .../core/services/api/TypedApiInterface.java | 2 +- .../api/types/AIDecompilationStatus.java | 130 ------------------ src/test/java/AIDecompTests.java | 34 ----- .../ai/reveng/AIDecompilerComponentTest.java | 63 ++++----- 7 files changed, 74 insertions(+), 233 deletions(-) delete mode 100644 src/main/java/ai/reveng/toolkit/ghidra/core/services/api/types/AIDecompilationStatus.java delete mode 100644 src/test/java/AIDecompTests.java diff --git a/src/main/java/ai/reveng/toolkit/ghidra/binarysimilarity/ui/aidecompiler/AIDecompilationdWindow.java b/src/main/java/ai/reveng/toolkit/ghidra/binarysimilarity/ui/aidecompiler/AIDecompilationdWindow.java index d28ba7e2..3268bca1 100644 --- a/src/main/java/ai/reveng/toolkit/ghidra/binarysimilarity/ui/aidecompiler/AIDecompilationdWindow.java +++ b/src/main/java/ai/reveng/toolkit/ghidra/binarysimilarity/ui/aidecompiler/AIDecompilationdWindow.java @@ -1,11 +1,11 @@ package ai.reveng.toolkit.ghidra.binarysimilarity.ui.aidecompiler; import ai.reveng.invoker.ApiException; +import ai.reveng.model.GetAiDecompilationTask; import ai.reveng.toolkit.ghidra.core.services.api.GhidraRevengService; import ai.reveng.toolkit.ghidra.core.services.api.TypedApiInterface; import ai.reveng.toolkit.ghidra.core.services.logging.ReaiLoggingService; import ai.reveng.toolkit.ghidra.plugins.ReaiPluginPackage; -import ai.reveng.toolkit.ghidra.core.services.api.types.AIDecompilationStatus; import docking.ActionContext; import docking.action.DockingAction; import docking.action.ToolBarData; @@ -23,6 +23,7 @@ import org.fife.ui.rtextarea.RTextScrollPane; import javax.swing.*; +import javax.swing.text.*; import java.awt.*; import java.util.Map; import java.util.Objects; @@ -32,11 +33,12 @@ public class AIDecompilationdWindow extends ComponentProviderAdapter { private RSyntaxTextArea textArea; private RTextScrollPane sp; - private JTextArea descriptionArea; + private JEditorPane descriptionArea; private JComponent component; private Function function; private TaskMonitorComponent taskMonitorComponent; - private final Map cache = new java.util.HashMap<>(); + private final Map cache = new java.util.HashMap<>(); + public AIDecompilationdWindow(PluginTool tool, String owner) { super(tool, ReaiPluginPackage.WINDOW_PREFIX + "AI Decompilation", owner); @@ -126,8 +128,10 @@ private JComponent buildComponent() { component = new JPanel(new BorderLayout()); - descriptionArea = new JTextArea(10, 60); - descriptionArea.setLineWrap(true); + descriptionArea = new JEditorPane(); + descriptionArea.setContentType("text/html"); + descriptionArea.setEditable(false); +// descriptionArea.setLineWrap(true); descriptionArea.setText("No function selected or binary not analysed yet with RevEng.AI"); descriptionArea.setEditable(false); component.add(descriptionArea, BorderLayout.NORTH); @@ -152,12 +156,14 @@ public JComponent getComponent() { } - public void setDisplayedValuesBasedOnStatus(Function function, AIDecompilationStatus status) { + + public void setDisplayedValuesBasedOnStatus(Function function, GetAiDecompilationTask status) { this.function = function; - if (status.status().equals("success")) { - setCode(status.decompilation()); - descriptionArea.setText(status.getMarkedUpSummary()); - } else if (status.status().equals("error")) { + if (status.getStatus().equals("success")) { + setCode(status.getDecompilation()); + descriptionArea.setText("%s".formatted(status.getSummary())); + // TODO: Add action to rename to suggested name + } else if (status.getStatus().equals("error")) { setCode(""); descriptionArea.setText("Decompilation failed"); } @@ -215,20 +221,20 @@ public void locationChanged(ProgramLocation loc) { } - void newStatusForFunction(Function function, AIDecompilationStatus status) { + void newStatusForFunction(Function function, GetAiDecompilationTask status) { cache.put(function, status); if (function == this.function) { SwingUtilities.invokeLater(() -> setDisplayedValuesBasedOnStatus(function, status) ); } - if (status.status().equals("success")) { + if (status.getStatus().equals("success")) { var logger = tool.getService(ReaiLoggingService.class); - logger.info("AI Decompilation finished for function %s: %s".formatted(function.getName(), status.decompilation())); + logger.info("AI Decompilation finished for function %s: %s".formatted(function.getName(), status.getDecompilation())); if (!hasPendingDecompilations()) { taskMonitorComponent.setVisible(false); } - } else if (status.status().equals("error")) { + } else if (status.getStatus().equals("error")) { if (!hasPendingDecompilations()) { taskMonitorComponent.setVisible(false); } @@ -236,7 +242,7 @@ void newStatusForFunction(Function function, AIDecompilationStatus status) { } private boolean hasPendingDecompilations() { - return cache.values().stream().anyMatch(s -> s.status().equals("pending") || s.status().equals("running") || s.status().equals("queued")); + return cache.values().stream().anyMatch(s -> s.getStatus().equals("pending") || s.getStatus().equals("running") || s.getStatus().equals("queued")); } class AIDecompTask extends Task { @@ -253,7 +259,7 @@ public AIDecompTask(PluginTool tool, GhidraRevengService.FunctionWithID function public void run(TaskMonitor monitor) throws CancelledException { var fID = functionWithID.functionID(); // Check if there is an existing process already, because the trigger API will fail with 400 if there is - if (service.getApi().pollAIDecompileStatus(fID).status().equals("uninitialised")) { + if (service.getApi().pollAIDecompileStatus(fID).getStatus().equals("uninitialised")) { // Trigger the decompilation service.getApi().triggerAIDecompilationForFunctionID(fID); } @@ -265,17 +271,17 @@ public void run(TaskMonitor monitor) throws CancelledException { private void waitForDecomp(TypedApiInterface.FunctionID id, TaskMonitor monitor) throws CancelledException { var logger = tool.getService(ReaiLoggingService.class); var api = service.getApi(); - AIDecompilationStatus lastDecompStatus = null; + GetAiDecompilationTask lastDecompStatus = null; while (true) { var newStatus = api.pollAIDecompileStatus(id); - if (lastDecompStatus == null || !Objects.equals(newStatus.status(), lastDecompStatus.status())) { + if (lastDecompStatus == null || !Objects.equals(newStatus.getStatus(), lastDecompStatus.getStatus())) { lastDecompStatus = newStatus; newStatusForFunction(functionWithID.function(), newStatus); } - monitor.setMessage("Waiting for AI Decompilation for %s ... Current status: %s".formatted(functionWithID.function().getName(), lastDecompStatus.status())); + monitor.setMessage("Waiting for AI Decompilation for %s ... Current status: %s".formatted(functionWithID.function().getName(), lastDecompStatus.getStatus())); monitor.checkCancelled(); - switch (newStatus.status()) { + switch (newStatus.getStatus()) { case "pending": case "uninitialised": case "queued": @@ -291,10 +297,10 @@ private void waitForDecomp(TypedApiInterface.FunctionID id, TaskMonitor monitor) monitor.setProgress(monitor.getMaximum()); return; case "error": - logger.error("Decompilation failed: %s".formatted(newStatus.decompilation())); + logger.error("Decompilation failed: %s".formatted(newStatus.getDecompilation())); return; default: - throw new RuntimeException("Unknown status: %s".formatted(newStatus.status())); + throw new RuntimeException("Unknown status: %s".formatted(newStatus.getStatus())); } } diff --git a/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/GhidraRevengService.java b/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/GhidraRevengService.java index 39827f61..74a0e94d 100644 --- a/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/GhidraRevengService.java +++ b/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/GhidraRevengService.java @@ -641,7 +641,7 @@ public String decompileFunctionViaAI(FunctionWithID functionWithID, TaskMonitor // Check if there is an existing process already, because the trigger API will fail with 400 if there is var fID = functionWithID.functionID; var function = functionWithID.function; - if (api.pollAIDecompileStatus(fID).status().equals("uninitialised")){ + if (api.pollAIDecompileStatus(fID).getStatus().equals("uninitialised")){ // Trigger the decompilation api.triggerAIDecompilationForFunctionID(fID); } @@ -655,7 +655,7 @@ public String decompileFunctionViaAI(FunctionWithID functionWithID, TaskMonitor var status = api.pollAIDecompileStatus(fID); window.setDisplayedValuesBasedOnStatus(function, status); - switch (status.status()) { + switch (status.getStatus()) { case "pending": case "uninitialised": case "queued": @@ -670,11 +670,11 @@ public String decompileFunctionViaAI(FunctionWithID functionWithID, TaskMonitor case "success": monitor.setProgress(monitor.getMaximum()); window.setDisplayedValuesBasedOnStatus(function, status); - return status.decompilation(); + return status.getDecompilation(); case "error": - return "Decompilation failed: %s".formatted(status.decompilation()); + return "Decompilation failed: %s".formatted(status.getStatus()); default: - throw new RuntimeException("Unknown status: %s".formatted(status.status())); + throw new RuntimeException("Unknown status: %s".formatted(status.getStatus())); } diff --git a/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/TypedApiImplementation.java b/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/TypedApiImplementation.java index b8fc0aeb..4243feb8 100644 --- a/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/TypedApiImplementation.java +++ b/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/TypedApiImplementation.java @@ -458,13 +458,17 @@ public boolean triggerAIDecompilationForFunctionID(FunctionID functionID) { } @Override - public AIDecompilationStatus pollAIDecompileStatus(FunctionID functionID) { - - HttpRequest request = requestBuilderForEndpoint("ai-decompilation/" + functionID.value(), "?summarise=true") - .GET() - .build(); - return AIDecompilationStatus.fromJSONObject(sendVersion2Request(request).getJsonData()); - + public GetAiDecompilationTask pollAIDecompileStatus(FunctionID functionID) { + try { + var response = functionsAiDecompilationApi.getAiDecompilationTaskResult( + functionID.value(), // Long functionId + true, // summarise + true // generateInlineComments + ); + return response.getData(); + } catch (ApiException e) { + throw new RuntimeException("Failed to poll AI decompilation status", e); + } } /** diff --git a/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/TypedApiInterface.java b/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/TypedApiInterface.java index e7b1250f..0104dcb1 100644 --- a/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/TypedApiInterface.java +++ b/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/TypedApiInterface.java @@ -149,7 +149,7 @@ default boolean triggerAIDecompilationForFunctionID(FunctionID functionID) { throw new UnsupportedOperationException("triggerAIDecompilationForFunctionID not implemented yet"); } - default AIDecompilationStatus pollAIDecompileStatus(FunctionID functionID) { + default GetAiDecompilationTask pollAIDecompileStatus(FunctionID functionID) { throw new UnsupportedOperationException("pollAIDecompileStatus not implemented yet"); } diff --git a/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/types/AIDecompilationStatus.java b/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/types/AIDecompilationStatus.java deleted file mode 100644 index 331430f5..00000000 --- a/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/types/AIDecompilationStatus.java +++ /dev/null @@ -1,130 +0,0 @@ -package ai.reveng.toolkit.ghidra.core.services.api.types; - -import ghidra.program.model.listing.Program; -import org.json.JSONObject; - -import java.util.HashMap; -import java.util.Map; -import java.util.stream.Collectors; - -/** - * - * @param status - * @param decompilation Pure text, suitable for directly displaying - * @param rawDecompilation decompilation output that still includes all the placeholders - * @param functionMapping - * @param functionMappingFull - */ -public record AIDecompilationStatus( - String status, // TODO: Change to enum - String decompilation, - String rawDecompilation, - String aiSummary, - String rawAiSummary, - Map functionMapping, // TODO: Figure out what this is, and why it's relevant - FullFunctionMapping functionMappingFull // Unclear how that is different to the previous one -) { - public static AIDecompilationStatus fromJSONObject(JSONObject data) { - Map functionMapping = new HashMap<>(); - if (!data.isNull("function_mapping")) { - JSONObject funcMapping = data.getJSONObject("function_mapping"); - for (String key : funcMapping.keySet()) { - functionMapping.put(key, AIDecompFuncReference.fromJSONObject(funcMapping.getJSONObject(key))); - } - } - - FullFunctionMapping functionMappingFull = null; - if (!data.isNull("function_mapping_full")) { - functionMappingFull = FullFunctionMapping.fromJson(data.getJSONObject("function_mapping_full")); - } - - - return new AIDecompilationStatus( - data.getString("status"), - !data.isNull("decompilation") ? data.getString("decompilation") : null, - !data.isNull("raw_decompilation") ? data.getString("raw_decompilation") : null, - !data.isNull("ai_summary") ? data.getString("ai_summary") : null, - !data.isNull("raw_ai_summary") ? data.getString("raw_ai_summary") : null, - functionMapping, - functionMappingFull - ); - } - - public String getMarkedUpSummary() { - return aiSummary; - } - - record AIDecompFuncReference(String name, Integer addr, boolean isExternal) { - public static AIDecompFuncReference fromJSONObject(JSONObject data) { - return new AIDecompFuncReference( - data.getString("name"), - data.getInt("addr"), - data.getBoolean("is_external") - ); - } - } - - public record FullFunctionMapping( - JSONObject inverse_string_map, - Map inverse_function_map, - JSONObject unmatched_functions, - JSONObject unmatched_external_vars, - Map unmatched_custom_types, - JSONObject unmatched_strings, - Map unmatched_vars, - JSONObject unmatched_go_to_labels, - JSONObject unmatched_custom_function_pointers, - JSONObject unmatched_variadic_lists, - Map> fields - ) { - record MapValue(String value) { - } - - record FunctionMapValue(String name, Integer addr, boolean is_external) { - public static FunctionMapValue fromJson(JSONObject json) { - return new FunctionMapValue( - json.getString("name"), - json.getInt("addr"), - json.getBoolean("is_external") - ); - } - } - - record FieldValue(String value) { - } - - public static FullFunctionMapping fromJson(JSONObject json) { - if (json.isNull("inverse_string_map")) { - throw new IllegalArgumentException("JSON does not contain 'inverse_string_map'"); - } - Map inverse_function_map = null; - if (!json.isNull("inverse_function_map")) { - inverse_function_map = json.getJSONObject("inverse_function_map").keySet() - .stream() - .collect(Collectors.toMap( - PlaceholderToken::new, - value -> FunctionMapValue.fromJson(json.getJSONObject("inverse_function_map").getJSONObject(value)) - )); - } - return new FullFunctionMapping( - json.getJSONObject("inverse_string_map"), - inverse_function_map, - json.getJSONObject("unmatched_functions"), - json.getJSONObject("unmatched_external_vars"), - null, - json.getJSONObject("unmatched_strings"), - null, - json.getJSONObject("unmatched_go_to_labels"), - json.getJSONObject("unmatched_custom_function_pointers"), - json.getJSONObject("unmatched_variadic_lists"), - null - ); - } - } - - /** - * a placeholder like `` - * @param placeHolderString - */ - public record PlaceholderToken(String placeHolderString) {} -} diff --git a/src/test/java/AIDecompTests.java b/src/test/java/AIDecompTests.java deleted file mode 100644 index a3f5ed5d..00000000 --- a/src/test/java/AIDecompTests.java +++ /dev/null @@ -1,34 +0,0 @@ -import ai.reveng.toolkit.ghidra.core.services.api.V2Response; -import ai.reveng.toolkit.ghidra.core.services.api.types.AIDecompilationStatus; -import org.junit.Test; - -public class AIDecompTests extends AbstractRevEngIntegrationTest { - @Test - public void testDecompilation() { - V2Response mockResponse = getMockResponseFromFile("ai_decomp_example.json"); - var decompStatus = AIDecompilationStatus.fromJSONObject(mockResponse.getJsonData()); - - } - - @Test - public void testFullMapping() { - V2Response mockResponse = getMockResponseFromFile("ai_decomp_type_field.json"); - var decompStatus = AIDecompilationStatus.fromJSONObject(mockResponse.getJsonData()); - assert decompStatus.functionMappingFull().inverse_function_map().containsKey( - new AIDecompilationStatus.PlaceholderToken("")); -// assert new AIDecompilationStatus.PlaceholderToken("") - System.out.println(decompStatus.functionMappingFull()); - } - - - @Test - public void testDecompilationWithCustomTypeAndField() { - V2Response mockResponse = getMockResponseFromFile("ai_decomp_type_field.json"); - var decompStatus = AIDecompilationStatus.fromJSONObject(mockResponse.getJsonData()); - var markedUp = decompStatus.getMarkedUpSummary(); - assert !markedUp.contains(""); -// assert !markedUp.contains(""); - assert !markedUp.contains(""); - - } -} diff --git a/src/test/java/ai/reveng/AIDecompilerComponentTest.java b/src/test/java/ai/reveng/AIDecompilerComponentTest.java index 7f7ec1fb..898b58a1 100644 --- a/src/test/java/ai/reveng/AIDecompilerComponentTest.java +++ b/src/test/java/ai/reveng/AIDecompilerComponentTest.java @@ -1,10 +1,14 @@ package ai.reveng; import ai.reveng.invoker.ApiException; +import ai.reveng.model.GetAiDecompilationTask; import ai.reveng.toolkit.ghidra.binarysimilarity.ui.aidecompiler.AIDecompilationdWindow; import ai.reveng.toolkit.ghidra.core.services.api.AnalysisOptionsBuilder; import ai.reveng.toolkit.ghidra.core.services.api.mocks.UnimplementedAPI; -import ai.reveng.toolkit.ghidra.core.services.api.types.*; +import ai.reveng.toolkit.ghidra.core.services.api.TypedApiInterface.FunctionID; +import ai.reveng.toolkit.ghidra.core.services.api.TypedApiInterface.AnalysisID; +import ai.reveng.toolkit.ghidra.core.services.api.types.AnalysisStatus; +import ai.reveng.toolkit.ghidra.core.services.api.types.FunctionInfo; import ai.reveng.toolkit.ghidra.plugins.BinarySimilarityPlugin; import docking.widgets.dialogs.InputDialog; import ghidra.app.context.ProgramLocationActionContext; @@ -56,33 +60,26 @@ public List getFunctionInfo(AnalysisID analysisID) { } @Override - public AIDecompilationStatus pollAIDecompileStatus(FunctionID functionID) { - + public GetAiDecompilationTask pollAIDecompileStatus(FunctionID functionID) { if (functionID.value() == 2) { - return new AIDecompilationStatus( - "success", - "int func2(int a) { return a + 1; }", - "int func2(int a) { return a + 1; }", - "Mocked Description Summary for func2", - "Summary for func2", - null, - null - ); + return new GetAiDecompilationTask() + .status("success") + .decompilation("int func2(int a) { return a + 1; }") + .rawDecompilation("int func2(int a) { return a + 1; }") + .summary("Mocked Description Summary for func2") + .aiSummary("Mocked Description Summary for func2") + .rawAiSummary("Summary for func2"); } else if (functionID.value() == 1) { - - return new AIDecompilationStatus( - "success", - "void func1() { return; }", - "void func1() { return; }", - "Mocked Description Summary", - "Summary", - null, - null - ); + return new GetAiDecompilationTask() + .status("success") + .decompilation("void func1() { return; }") + .rawDecompilation("void func1() { return; }") + .summary("Mocked Description Summary") + .aiSummary("Mocked Description Summary") + .rawAiSummary("Summary"); } else { throw new RuntimeException("Unknown FunctionID"); } - } @Override @@ -109,7 +106,7 @@ public boolean triggerAIDecompilationForFunctionID(FunctionID functionID) { // get AIDecompiledWindow, and some internal fields for testing var aiDecompComponent = getComponentProvider(AIDecompilationdWindow.class); - Map aiDecompCache = (Map) getInstanceField("cache", aiDecompComponent); + Map aiDecompCache = (Map) getInstanceField("cache", aiDecompComponent); RSyntaxTextArea textArea = (RSyntaxTextArea) getInstanceField("textArea", aiDecompComponent); // Make sure it's hidden to start with @@ -226,16 +223,14 @@ public List getFunctionInfo(AnalysisID analysisID) { } @Override - public AIDecompilationStatus pollAIDecompileStatus(FunctionID functionID) { - return new AIDecompilationStatus( - "success", - "void func1() { return; }", - "void func1() { return; }", - "Mocked Description Summary", - "Summary", - null, - null - ); + public GetAiDecompilationTask pollAIDecompileStatus(FunctionID functionID) { + return new GetAiDecompilationTask() + .status("success") + .decompilation("void func1() { return; }") + .rawDecompilation("void func1() { return; }") + .summary("Mocked Description Summary") + .aiSummary("Mocked Description Summary") + .rawAiSummary("Summary"); } @Override From 88b4178743ac727cf3dcd817d78cce8624e6c6eb Mon Sep 17 00:00:00 2001 From: Florian Magin Date: Thu, 22 Jan 2026 12:44:30 +0100 Subject: [PATCH 2/2] Add button to apply predicted name in Decomp panel --- .../aidecompiler/AIDecompilationdWindow.java | 66 +++++++++++++++++-- 1 file changed, 61 insertions(+), 5 deletions(-) diff --git a/src/main/java/ai/reveng/toolkit/ghidra/binarysimilarity/ui/aidecompiler/AIDecompilationdWindow.java b/src/main/java/ai/reveng/toolkit/ghidra/binarysimilarity/ui/aidecompiler/AIDecompilationdWindow.java index 3268bca1..ee0a30fd 100644 --- a/src/main/java/ai/reveng/toolkit/ghidra/binarysimilarity/ui/aidecompiler/AIDecompilationdWindow.java +++ b/src/main/java/ai/reveng/toolkit/ghidra/binarysimilarity/ui/aidecompiler/AIDecompilationdWindow.java @@ -23,7 +23,6 @@ import org.fife.ui.rtextarea.RTextScrollPane; import javax.swing.*; -import javax.swing.text.*; import java.awt.*; import java.util.Map; import java.util.Objects; @@ -34,6 +33,9 @@ public class AIDecompilationdWindow extends ComponentProviderAdapter { private RSyntaxTextArea textArea; private RTextScrollPane sp; private JEditorPane descriptionArea; + private JLabel predictedNameLabel; + private JButton usePredictedNameButton; + private JPanel predictedNamePanel; private JComponent component; private Function function; private TaskMonitorComponent taskMonitorComponent; @@ -127,16 +129,33 @@ private JComponent buildComponent() { component = new JPanel(new BorderLayout()); + // Create header panel to hold description and predicted name panel + JPanel headerPanel = new JPanel(); + headerPanel.setLayout(new BoxLayout(headerPanel, BoxLayout.Y_AXIS)); + // Description area descriptionArea = new JEditorPane(); descriptionArea.setContentType("text/html"); descriptionArea.setEditable(false); -// descriptionArea.setLineWrap(true); descriptionArea.setText("No function selected or binary not analysed yet with RevEng.AI"); - descriptionArea.setEditable(false); - component.add(descriptionArea, BorderLayout.NORTH); + headerPanel.add(descriptionArea); + + // Visual divider + headerPanel.add(new JSeparator(SwingConstants.HORIZONTAL)); + // Predicted name panel (between description and code) + predictedNamePanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); + predictedNameLabel = new JLabel("Predicted name: "); + usePredictedNameButton = new JButton("Use Predicted Name"); + usePredictedNameButton.addActionListener(e -> applyPredictedName()); + predictedNamePanel.add(predictedNameLabel); + predictedNamePanel.add(usePredictedNameButton); + predictedNamePanel.setVisible(false); // Hidden until we have a prediction + headerPanel.add(predictedNamePanel); + component.add(headerPanel, BorderLayout.NORTH); + + // Code area textArea = new RSyntaxTextArea(20, 60); textArea.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_C); textArea.setEditable(false); @@ -150,6 +169,33 @@ private JComponent buildComponent() { return component; } + /** + * Apply the predicted function name to the current function + */ + private void applyPredictedName() { + if (function == null) { + return; + } + + var cachedStatus = cache.get(function); + if (cachedStatus == null || cachedStatus.getPredictedFunctionName() == null) { + return; + } + + String predictedName = cachedStatus.getPredictedFunctionName(); + var program = function.getProgram(); + + int txId = program.startTransaction("Rename function to predicted name"); + try { + function.setName(predictedName, ghidra.program.model.symbol.SourceType.USER_DEFINED); + Msg.info(this, "Renamed function to predicted name: " + predictedName); + } catch (Exception ex) { + Msg.showError(this, this.component, "Failed to rename function:", ex.getMessage(), ex); + } finally { + program.endTransaction(txId, true); + } + } + @Override public JComponent getComponent() { return component; @@ -162,10 +208,19 @@ public void setDisplayedValuesBasedOnStatus(Function function, GetAiDecompilatio if (status.getStatus().equals("success")) { setCode(status.getDecompilation()); descriptionArea.setText("%s".formatted(status.getSummary())); - // TODO: Add action to rename to suggested name + + // Show predicted name if available + String predictedName = status.getPredictedFunctionName(); + if (predictedName != null && !predictedName.isEmpty()) { + predictedNameLabel.setText("Predicted name: " + predictedName); + predictedNamePanel.setVisible(true); + } else { + predictedNamePanel.setVisible(false); + } } else if (status.getStatus().equals("error")) { setCode(""); descriptionArea.setText("Decompilation failed"); + predictedNamePanel.setVisible(false); } } @@ -178,6 +233,7 @@ private void clear() { this.function = null; setCode(""); descriptionArea.setText(""); + predictedNamePanel.setVisible(false); } public void refresh(GhidraRevengService.FunctionWithID function) {