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 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 tokenRef = new AtomicReference<>(""); private final AtomicReference validateThreadRef = new AtomicReference<>(); private final DefaultListModel testListModel = new DefaultListModel<>(); private final JList testList = new JList<>(testListModel); private final DefaultListModel filteredListModel = new DefaultListModel<>(); private final JList filteredList = new JList<>(filteredListModel); private final JTextField searchField = new JTextField(); private final Map runs = new HashMap<>(); private final Map 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 { 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 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; } }