Files
CyberStrikeAI/plugins/burp-suite/cyberstrikeai-burp-extension/src/main/java/burp/CyberStrikeAITab.java
T
2026-06-02 13:32:05 +08:00

871 lines
32 KiB
Java

package burp;
import javax.swing.*;
import java.awt.*;
import java.awt.datatransfer.StringSelection;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
final class CyberStrikeAITab implements ITab {
private final JPanel root = new JPanel(new BorderLayout());
private final JTextField hostField = new JTextField("127.0.0.1");
private final JTextField portField = new JTextField("8080");
private final JCheckBox useHttpsBox = new JCheckBox("HTTPS", true);
private final JPasswordField passwordField = new JPasswordField();
private final JComboBox<String> agentModeBox = new JComboBox<>(agentModeLabels());
private static String[] agentModeLabels() {
CyberStrikeAIClient.AgentMode[] modes = CyberStrikeAIClient.AgentMode.values();
String[] labels = new String[modes.length];
for (int i = 0; i < modes.length; i++) {
labels[i] = modes[i].displayName;
}
return labels;
}
private final JButton validateButton = new JButton("Validate");
private final JButton clearButton = new JButton("Clear Output");
private final JButton stopButton = new JButton("Stop");
private final JButton copyButton = new JButton("Copy");
private final JButton clearAllButton = new JButton("Clear All");
private final JLabel statusLabel = new JLabel("Not validated");
private final JCheckBox showDebugEventsBox = new JCheckBox("Show debug events", false);
private final JCheckBox renderMarkdownBox = new JCheckBox("Render Markdown", true);
private final JTextArea progressArea = new JTextArea();
private final JTextArea finalRawArea = new JTextArea(); // raw final stream / final response
private JScrollPane progressScrollPane;
private JScrollPane finalRawScrollPane;
/** 距底部在此像素内视为「跟随滚动」,否则用户上拉阅读时不抢滚动条 */
private static final int SCROLL_FOLLOW_THRESHOLD_PX = 48;
private final JEditorPane markdownPane = new JEditorPane("text/html", "");
private final CardLayout outputCardsLayout = new CardLayout();
private final JPanel outputCards = new JPanel(outputCardsLayout);
private final JPanel outputRoot = new JPanel(new BorderLayout());
private final JPanel progressContainer = new JPanel(new CardLayout());
private final JToggleButton progressToggle = new JToggleButton("Progress ▾", true);
private final JTextArea requestArea = new JTextArea();
private final JTextArea responseArea = new JTextArea();
private final JTabbedPane rightTabs = new JTabbedPane();
private final CyberStrikeAIClient client = new CyberStrikeAIClient();
private final AtomicReference<String> tokenRef = new AtomicReference<>("");
private final AtomicReference<Thread> validateThreadRef = new AtomicReference<>();
private final DefaultListModel<TestRun> testListModel = new DefaultListModel<>();
private final JList<TestRun> testList = new JList<>(testListModel);
private final DefaultListModel<TestRun> filteredListModel = new DefaultListModel<>();
private final JList<TestRun> filteredList = new JList<>(filteredListModel);
private final JTextField searchField = new JTextField();
private final Map<String, TestRun> runs = new HashMap<>();
private final Map<String, Integer> runIdToIndex = new HashMap<>();
private final AtomicInteger runSeq = new AtomicInteger(1);
private String selectedRunId = null;
private static final class TestRun {
final String id;
final String title;
final String agentMode;
final StringBuilder buffer = new StringBuilder();
final StringBuilder progressBuffer = new StringBuilder();
final StringBuilder finalBuffer = new StringBuilder();
final StringBuilder thinkingPending = new StringBuilder();
String status;
String conversationId;
String requestRaw;
String responseRaw;
String finalResponse;
TestRun(String id, String title, String agentMode) {
this.id = id;
this.title = title;
this.agentMode = agentMode;
this.status = "running";
this.conversationId = "";
this.requestRaw = "";
this.responseRaw = "";
this.finalResponse = "";
}
@Override
public String toString() {
return id;
}
}
CyberStrikeAITab() {
root.add(buildConfigPanel(), BorderLayout.NORTH);
root.add(buildMainPane(), BorderLayout.CENTER);
wireActions();
}
private JComponent buildConfigPanel() {
// Best-practice toolbar layout:
// Row 1 = connection settings
// Row 2 = run controls + view options
JPanel rootPanel = new JPanel();
rootPanel.setLayout(new BoxLayout(rootPanel, BoxLayout.Y_AXIS));
rootPanel.setBorder(BorderFactory.createEmptyBorder(4, 6, 4, 6));
hostField.setColumns(14);
portField.setColumns(6);
passwordField.setColumns(12);
agentModeBox.setPreferredSize(new Dimension(200, agentModeBox.getPreferredSize().height));
JPanel row1 = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 2));
row1.add(new JLabel("Host"));
row1.add(hostField);
row1.add(new JLabel("Port"));
row1.add(portField);
useHttpsBox.setToolTipText("Use https:// for CyberStrikeAI (self-signed certs are trusted automatically)");
row1.add(useHttpsBox);
row1.add(new JLabel("Password"));
row1.add(passwordField);
row1.add(validateButton);
row1.add(statusLabel);
JPanel row2 = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 2));
row2.add(new JLabel("Agent"));
row2.add(agentModeBox);
row2.add(stopButton);
row2.add(copyButton);
row2.add(clearButton);
row2.add(showDebugEventsBox);
row2.add(renderMarkdownBox);
rootPanel.add(row1);
rootPanel.add(row2);
return rootPanel;
}
private JComponent buildMainPane() {
JPanel sidebarPanel = buildSidebarPanel();
JComponent right = buildRightPanel();
JSplitPane split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, sidebarPanel, right);
split.setResizeWeight(0.25);
split.setBorder(null);
return split;
}
private JPanel buildSidebarPanel() {
JPanel p = new JPanel(new BorderLayout());
filteredList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
filteredList.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12));
filteredList.setCellRenderer(new TestRunCellRenderer());
filteredList.addListSelectionListener(e -> {
if (!e.getValueIsAdjusting()) {
String id = getSelectedRunIdFromList();
if (id != null) {
setLogAreaToRun(id);
}
}
});
JLabel title = new JLabel("Test History");
title.setBorder(BorderFactory.createEmptyBorder(6, 8, 6, 8));
JPanel top = new JPanel(new BorderLayout(8, 6));
top.setBorder(BorderFactory.createEmptyBorder(0, 8, 0, 8));
top.add(title, BorderLayout.NORTH);
searchField.setToolTipText("Search runs (title)");
top.add(searchField, BorderLayout.SOUTH);
JScrollPane sp = new JScrollPane(filteredList);
sp.setBorder(BorderFactory.createTitledBorder("Runs"));
clearAllButton.addActionListener(e -> clearAllRuns());
JPanel bottom = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 6));
bottom.add(clearAllButton);
p.add(top, BorderLayout.NORTH);
p.add(sp, BorderLayout.CENTER);
p.add(bottom, BorderLayout.SOUTH);
p.setPreferredSize(new Dimension(320, 200));
return p;
}
private JComponent buildRightPanel() {
configureTextArea(progressArea, true);
configureTextArea(finalRawArea, true);
markdownPane.setEditable(false);
markdownPane.putClientProperty(JEditorPane.HONOR_DISPLAY_PROPERTIES, Boolean.TRUE);
markdownPane.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12));
markdownPane.setOpaque(true);
markdownPane.setBackground(Color.WHITE);
configureTextArea(requestArea, false);
configureTextArea(responseArea, false);
finalRawScrollPane = new JScrollPane(finalRawArea);
finalRawScrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
finalRawScrollPane.getVerticalScrollBar().setUnitIncrement(16);
outputCards.add(finalRawScrollPane, "raw");
outputCards.add(new JScrollPane(markdownPane), "md");
outputRoot.add(buildOutputHeader(), BorderLayout.NORTH);
outputRoot.add(buildOutputBody(), BorderLayout.CENTER);
rightTabs.addTab("Output", outputRoot);
JScrollPane requestScroll = new JScrollPane(requestArea);
requestScroll.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
rightTabs.addTab("Request", requestScroll);
JScrollPane responseScroll = new JScrollPane(responseArea);
responseScroll.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
rightTabs.addTab("Response", responseScroll);
return rightTabs;
}
private JComponent buildOutputHeader() {
JPanel header = new JPanel(new BorderLayout(8, 0));
header.setBorder(BorderFactory.createEmptyBorder(6, 8, 6, 8));
JPanel left = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 0));
left.add(progressToggle);
header.add(left, BorderLayout.WEST);
return header;
}
private JComponent buildOutputBody() {
progressScrollPane = new JScrollPane(progressArea);
progressScrollPane.setBorder(BorderFactory.createTitledBorder("Progress"));
progressScrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
progressScrollPane.getVerticalScrollBar().setUnitIncrement(16);
JPanel empty = new JPanel();
progressContainer.add(progressScrollPane, "show");
progressContainer.add(empty, "hide");
((CardLayout) progressContainer.getLayout()).show(progressContainer, "show");
JPanel finalPanel = new JPanel(new BorderLayout());
finalPanel.add(outputCards, BorderLayout.CENTER);
finalPanel.setBorder(BorderFactory.createTitledBorder("Final Response"));
JSplitPane split = new JSplitPane(JSplitPane.VERTICAL_SPLIT, progressContainer, finalPanel);
split.setResizeWeight(0.15);
split.setBorder(null);
split.setDividerSize(6);
final int[] lastDividerLocation = new int[]{140}; // sensible default
progressToggle.addActionListener(e -> {
boolean show = progressToggle.isSelected();
progressToggle.setText(show ? "Progress ▾" : "Progress ▸");
CardLayout cl = (CardLayout) progressContainer.getLayout();
cl.show(progressContainer, show ? "show" : "hide");
if (!show) {
int current = split.getDividerLocation();
if (current > 0) {
lastDividerLocation[0] = current;
}
split.setDividerLocation(0);
split.setDividerSize(0);
} else {
split.setDividerSize(6);
// Restore previous divider location (or fallback to 20% of height)
int restore = lastDividerLocation[0];
if (restore <= 0) {
int h = split.getHeight();
restore = (h > 0) ? Math.max(80, (int) (h * 0.2)) : 140;
}
split.setDividerLocation(restore);
}
split.revalidate();
split.repaint();
});
return split;
}
private static boolean isScrollNearBottom(JScrollPane scrollPane) {
if (scrollPane == null) {
return true;
}
JScrollBar bar = scrollPane.getVerticalScrollBar();
int max = Math.max(0, bar.getMaximum() - bar.getVisibleAmount());
return bar.getValue() >= max - SCROLL_FOLLOW_THRESHOLD_PX;
}
private static void scrollPaneToBottom(JScrollPane scrollPane) {
if (scrollPane == null) {
return;
}
JScrollBar bar = scrollPane.getVerticalScrollBar();
bar.setValue(bar.getMaximum());
}
private static void configureTextArea(JTextArea area, boolean monospaced) {
area.setEditable(false);
area.setLineWrap(true);
area.setWrapStyleWord(true);
if (monospaced) {
area.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
} else {
area.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
}
}
private static Color colorForStatus(String status) {
if (status == null) return new Color(120, 120, 120);
switch (status) {
case "running":
return new Color(33, 150, 243);
case "done":
return new Color(76, 175, 80);
case "error":
return new Color(244, 67, 54);
case "cancelled":
case "cancelling":
return new Color(255, 152, 0);
default:
return new Color(120, 120, 120);
}
}
private static final class DotIcon implements Icon {
private final int size;
private Color color;
DotIcon(int size, Color color) {
this.size = size;
this.color = color;
}
void setColor(Color color) {
this.color = color;
}
@Override
public int getIconWidth() {
return size;
}
@Override
public int getIconHeight() {
return size;
}
@Override
public void paintIcon(Component c, Graphics g, int x, int y) {
Graphics2D g2 = (Graphics2D) g.create();
try {
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setColor(color != null ? color : Color.GRAY);
g2.fillOval(x, y, size, size);
} finally {
g2.dispose();
}
}
}
private static final class TestRunCellRenderer implements ListCellRenderer<TestRun> {
private final JPanel panel = new JPanel(new BorderLayout(8, 0));
private final JLabel dotLabel = new JLabel();
private final JLabel titleLabel = new JLabel();
private final JLabel metaLabel = new JLabel();
private final JPanel textPanel = new JPanel();
private final DotIcon dotIcon = new DotIcon(10, new Color(120, 120, 120));
TestRunCellRenderer() {
panel.setBorder(BorderFactory.createEmptyBorder(6, 8, 6, 8));
dotLabel.setIcon(dotIcon);
textPanel.setLayout(new BoxLayout(textPanel, BoxLayout.Y_AXIS));
titleLabel.setFont(titleLabel.getFont().deriveFont(Font.BOLD));
metaLabel.setFont(metaLabel.getFont().deriveFont(Font.PLAIN, 11f));
metaLabel.setForeground(new Color(102, 102, 102));
textPanel.add(titleLabel);
textPanel.add(metaLabel);
panel.add(dotLabel, BorderLayout.WEST);
panel.add(textPanel, BorderLayout.CENTER);
panel.setOpaque(true);
textPanel.setOpaque(false);
}
@Override
public Component getListCellRendererComponent(JList<? extends TestRun> list, TestRun value, int index, boolean isSelected, boolean cellHasFocus) {
String titleText = value != null ? value.title : "";
String modeText = value != null ? value.agentMode : "";
String statusText = value != null ? value.status : "";
String shownTitle = titleText;
if (shownTitle.length() > 80) {
shownTitle = shownTitle.substring(0, 77) + "...";
}
titleLabel.setText(shownTitle);
metaLabel.setText(modeText + " · " + statusText);
dotIcon.setColor(colorForStatus(statusText));
if (isSelected) {
panel.setBackground(list.getSelectionBackground());
titleLabel.setForeground(list.getSelectionForeground());
metaLabel.setForeground(list.getSelectionForeground());
} else {
panel.setBackground(list.getBackground());
titleLabel.setForeground(list.getForeground());
metaLabel.setForeground(new Color(102, 102, 102));
}
return panel;
}
}
// right panel builds scroll panes for each tab
private void wireActions() {
validateButton.addActionListener(e -> {
if ("Cancel".equals(validateButton.getText())) {
cancelValidateInProgress();
return;
}
validateButton.setText("Cancel");
validateButton.setEnabled(true);
stopButton.setEnabled(true);
statusLabel.setText("Validating...");
log("Validating connection... (max ~10s; click Cancel or Stop to abort)");
Thread worker = new Thread(() -> {
try {
CyberStrikeAIClient.Config cfg = currentConfig();
String token = client.loginAndValidate(cfg);
if (Thread.currentThread().isInterrupted()) {
return;
}
tokenRef.set(token);
SwingUtilities.invokeLater(() -> statusLabel.setText("OK (token saved)"));
log("Validation OK.");
} catch (Exception ex) {
tokenRef.set("");
if (Thread.currentThread().isInterrupted()) {
SwingUtilities.invokeLater(() -> statusLabel.setText("Cancelled"));
log("Validation cancelled.");
} else {
SwingUtilities.invokeLater(() -> statusLabel.setText("Failed: " + ex.getMessage()));
log("Validation failed: " + ex.getMessage());
}
} finally {
validateThreadRef.set(null);
SwingUtilities.invokeLater(() -> {
validateButton.setText("Validate");
validateButton.setEnabled(true);
});
}
}, "CyberStrikeAI-Validate");
validateThreadRef.set(worker);
worker.start();
});
clearButton.addActionListener(e -> {
if (selectedRunId == null) {
progressArea.setText("");
finalRawArea.setText("");
markdownPane.setText("");
return;
}
TestRun run = runs.get(selectedRunId);
if (run == null) return;
synchronized (run) {
run.buffer.setLength(0);
run.progressBuffer.setLength(0);
run.finalBuffer.setLength(0);
}
progressArea.setText("");
finalRawArea.setText("");
markdownPane.setText("");
});
copyButton.addActionListener(e -> {
String text;
int idx = rightTabs.getSelectedIndex();
String tabName = idx >= 0 ? rightTabs.getTitleAt(idx) : "";
if ("Request".equals(tabName)) {
text = requestArea.getText();
} else if ("Response".equals(tabName)) {
text = responseArea.getText();
} else {
text = finalRawArea.getText();
}
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(text == null ? "" : text), null);
});
stopButton.addActionListener(e -> {
if ("Cancel".equals(validateButton.getText())) {
cancelValidateInProgress();
return;
}
String runId = selectedRunId;
if (runId != null && client.hasActiveRequest()) {
client.cancelActiveRequest();
appendProgressToRun(runId, "\n[info] Stream stopped.\n");
setRunStatus(runId, "cancelled");
return;
}
if (runId == null) return;
TestRun run = runs.get(runId);
if (run == null) return;
String token = getToken();
if (token == null || token.trim().isEmpty()) {
appendProgressToRun(runId, "\n[error] Not validated.\n");
return;
}
String convId;
synchronized (run) {
convId = run.conversationId;
}
if (convId == null || convId.trim().isEmpty()) {
appendProgressToRun(runId, "\n[info] conversationId not available yet (wait for server to create session).\n");
return;
}
stopButton.setEnabled(false);
new Thread(() -> {
try {
CyberStrikeAIClient.Config cfg = currentConfig();
client.cancelByConversationId(cfg.baseUrl, token, convId);
appendProgressToRun(runId, "\n[info] Cancel requested.\n");
setRunStatus(runId, "cancelling");
} catch (Exception ex) {
appendProgressToRun(runId, "\n[error] Cancel failed: " + ex.getMessage() + "\n");
} finally {
SwingUtilities.invokeLater(() -> stopButton.setEnabled(true));
}
}, "CyberStrikeAI-Cancel").start();
});
searchField.getDocument().addDocumentListener(new javax.swing.event.DocumentListener() {
@Override public void insertUpdate(javax.swing.event.DocumentEvent e) { applyFilter(); }
@Override public void removeUpdate(javax.swing.event.DocumentEvent e) { applyFilter(); }
@Override public void changedUpdate(javax.swing.event.DocumentEvent e) { applyFilter(); }
});
renderMarkdownBox.addActionListener(e -> refreshOutputView());
}
private static final CyberStrikeAIClient.AgentMode[] AGENT_MODES = CyberStrikeAIClient.AgentMode.values();
CyberStrikeAIClient.Config currentConfig() {
String host = hostField.getText().trim();
String port = portField.getText().trim();
String password = new String(passwordField.getPassword());
String scheme = useHttpsBox.isSelected() ? "https" : "http";
String baseUrl = scheme + "://" + host + ":" + port;
int idx = agentModeBox.getSelectedIndex();
CyberStrikeAIClient.AgentMode mode = (idx >= 0 && idx < AGENT_MODES.length)
? AGENT_MODES[idx]
: CyberStrikeAIClient.AgentMode.EINO_SINGLE;
return new CyberStrikeAIClient.Config(baseUrl, password, mode);
}
String getToken() {
return tokenRef.get();
}
boolean isShowDebugEvents() {
return showDebugEventsBox.isSelected();
}
private String nextRunId() {
return "run_" + runSeq.getAndIncrement();
}
private String formatRunDisplay(String title, String agentMode, String status) {
return title + " [" + agentMode + "] - " + status;
}
String startNewRun(String title, String agentMode, IHttpRequestResponse msg) {
String id = nextRunId();
TestRun run = new TestRun(id, title, agentMode);
if (msg != null) {
run.requestRaw = bytesToString(msg.getRequest());
run.responseRaw = bytesToString(msg.getResponse());
}
runs.put(id, run);
int index = testListModel.getSize();
runIdToIndex.put(id, index);
testListModel.addElement(run);
filteredListModel.addElement(run);
selectedRunId = id;
filteredList.setSelectedIndex(filteredListModel.getSize() - 1);
progressArea.setText("");
finalRawArea.setText("");
markdownPane.setText("");
requestArea.setText(run.requestRaw);
responseArea.setText(run.responseRaw);
refreshOutputView();
return id;
}
void setRunStatus(String runId, String status) {
TestRun run = runs.get(runId);
if (run == null) return;
synchronized (run) {
run.status = status;
}
Integer index = runIdToIndex.get(runId);
if (index != null) {
SwingUtilities.invokeLater(() -> filteredList.repaint());
}
}
void setRunConversationId(String runId, String conversationId) {
if (runId == null) return;
TestRun run = runs.get(runId);
if (run == null) return;
synchronized (run) {
run.conversationId = conversationId == null ? "" : conversationId;
}
}
void appendToRun(String runId, String s) {
// Backward compatibility: default to progress bucket
appendProgressToRun(runId, s);
}
void appendProgressToRun(String runId, String s) {
if (runId == null || s == null) return;
TestRun run = runs.get(runId);
if (run == null) return;
synchronized (run) {
run.buffer.append(s);
run.progressBuffer.append(s);
}
if (runId.equals(selectedRunId)) {
SwingUtilities.invokeLater(() -> appendProgressUi(s, false));
}
}
private void appendProgressUi(String s, boolean forceFollow) {
JScrollBar bar = progressScrollPane != null ? progressScrollPane.getVerticalScrollBar() : null;
int scrollBefore = bar != null ? bar.getValue() : 0;
boolean follow = forceFollow || isScrollNearBottom(progressScrollPane);
progressArea.append(s);
if (follow) {
scrollPaneToBottom(progressScrollPane);
} else if (bar != null) {
bar.setValue(scrollBefore);
}
}
private void appendFinalUi(String s, boolean forceFollow) {
JScrollBar bar = finalRawScrollPane != null ? finalRawScrollPane.getVerticalScrollBar() : null;
int scrollBefore = bar != null ? bar.getValue() : 0;
boolean follow = forceFollow || isScrollNearBottom(finalRawScrollPane);
finalRawArea.append(s);
if (follow) {
scrollPaneToBottom(finalRawScrollPane);
} else if (bar != null) {
bar.setValue(scrollBefore);
}
}
void resetThinkingStream(String runId) {
if (runId == null) return;
TestRun run = runs.get(runId);
if (run == null) return;
synchronized (run) {
run.thinkingPending.setLength(0);
}
appendProgressToRun(runId, "\n[thinking]\n");
}
void appendThinkingDelta(String runId, String delta) {
if (runId == null || delta == null) return;
TestRun run = runs.get(runId);
if (run == null) return;
StringBuilder toAppend = new StringBuilder();
synchronized (run) {
for (int i = 0; i < delta.length(); i++) {
char c = delta.charAt(i);
if (c == '\n') {
if (run.thinkingPending.length() > 0) {
toAppend.append(" ").append(run.thinkingPending).append("\n");
run.thinkingPending.setLength(0);
} else {
toAppend.append("\n");
}
} else if (c != '\r') {
run.thinkingPending.append(c);
}
}
}
if (toAppend.length() > 0) {
appendProgressToRun(runId, toAppend.toString());
}
}
void appendFinalToRun(String runId, String s) {
if (runId == null || s == null) return;
TestRun run = runs.get(runId);
if (run == null) return;
synchronized (run) {
run.buffer.append(s);
run.finalBuffer.append(s);
}
if (runId.equals(selectedRunId)) {
SwingUtilities.invokeLater(() -> appendFinalUi(s, false));
}
}
void setFinalResponse(String runId, String finalResponse) {
if (runId == null) return;
TestRun run = runs.get(runId);
if (run == null) return;
synchronized (run) {
run.finalResponse = finalResponse == null ? "" : finalResponse;
}
if (runId.equals(selectedRunId)) {
SwingUtilities.invokeLater(this::refreshOutputView);
}
}
private String getSelectedRunIdFromList() {
TestRun run = filteredList.getSelectedValue();
return run == null ? null : run.id;
}
private void setLogAreaToRun(String runId) {
TestRun run = runs.get(runId);
if (run == null) return;
selectedRunId = runId;
String progress;
String fin;
synchronized (run) {
progress = run.progressBuffer.toString();
fin = run.finalBuffer.toString();
}
SwingUtilities.invokeLater(() -> {
progressArea.setText(progress);
scrollPaneToBottom(progressScrollPane);
finalRawArea.setText(fin);
scrollPaneToBottom(finalRawScrollPane);
requestArea.setText(run.requestRaw == null ? "" : run.requestRaw);
responseArea.setText(run.responseRaw == null ? "" : run.responseRaw);
refreshOutputView();
});
}
private void clearAllRuns() {
runs.clear();
runIdToIndex.clear();
testListModel.clear();
filteredListModel.clear();
selectedRunId = null;
SwingUtilities.invokeLater(() -> {
progressArea.setText("");
finalRawArea.setText("");
markdownPane.setText("");
requestArea.setText("");
responseArea.setText("");
});
}
void clearAndShowStreamHeader(String title) {
SwingUtilities.invokeLater(() -> {
progressArea.setText("[*] " + title + "\n\n");
finalRawArea.setText("");
markdownPane.setText("");
});
}
// Legacy helpers kept for Validate logging
void appendStreamLine(String s) {
if (s == null) return;
SwingUtilities.invokeLater(() -> appendProgressUi(s + "\n", false));
}
private void log(String s) {
appendStreamLine("[*] " + s);
}
private void cancelValidateInProgress() {
client.cancelActiveRequest();
Thread t = validateThreadRef.getAndSet(null);
if (t != null) {
t.interrupt();
}
SwingUtilities.invokeLater(() -> {
statusLabel.setText("Cancelled");
validateButton.setText("Validate");
validateButton.setEnabled(true);
});
log("Validation cancelled.");
}
private void applyFilter() {
String q = searchField.getText();
if (q == null) q = "";
String query = q.trim().toLowerCase();
filteredListModel.clear();
for (int i = 0; i < testListModel.size(); i++) {
TestRun r = testListModel.getElementAt(i);
if (query.isEmpty() || (r.title != null && r.title.toLowerCase().contains(query))) {
filteredListModel.addElement(r);
}
}
if (filteredListModel.size() > 0 && filteredList.getSelectedIndex() < 0) {
filteredList.setSelectedIndex(0);
}
}
private void refreshOutputView() {
if (!renderMarkdownBox.isSelected()) {
outputCardsLayout.show(outputCards, "raw");
return;
}
if (selectedRunId == null) {
outputCardsLayout.show(outputCards, "raw");
return;
}
TestRun run = runs.get(selectedRunId);
if (run == null) {
outputCardsLayout.show(outputCards, "raw");
return;
}
String finalResp;
synchronized (run) {
finalResp = run.finalResponse;
}
if (finalResp == null || finalResp.trim().isEmpty()) {
// while streaming, stick to raw for performance
outputCardsLayout.show(outputCards, "raw");
return;
}
String html = MarkdownRenderer.toHtml(finalResp);
markdownPane.setText(html);
markdownPane.setCaretPosition(0);
outputCardsLayout.show(outputCards, "md");
}
private static String bytesToString(byte[] bytes) {
if (bytes == null || bytes.length == 0) return "";
return new String(bytes, StandardCharsets.ISO_8859_1);
}
@Override
public String getTabCaption() {
return "CyberStrikeAI";
}
@Override
public Component getUiComponent() {
return root;
}
}