diff --git a/src/ipc/ai_coding_agent.js b/src/ipc/ai_coding_agent.js
new file mode 100644
index 0000000..2fe2c0d
--- /dev/null
+++ b/src/ipc/ai_coding_agent.js
@@ -0,0 +1,268 @@
+import { ipcMain } from "electron";
+import { AiCodingAgentChannels } from "./channels.js";
+import LLM from "@themaximalist/llm.js";
+import { settings } from "../utils/store.js";
+import crypto from "crypto";
+
+// Select a coding assistant from settings
+function selectCodingAssistant() {
+ const list = settings.get("ai.coding", []) || [];
+ const assistantInfo = list.find(a => a.default) || list[0];
+ if (!assistantInfo) {
+ throw new Error("No AI Coding assistant found. Please add one in Settings.");
+ }
+ return assistantInfo;
+}
+
+// Create LLM instance for coding tasks
+async function createCodingLlm(assistantInfo, stream = false) {
+ const llm = new LLM({
+ service: assistantInfo.provider,
+ apiKey: assistantInfo.apiKey,
+ model: assistantInfo.model,
+ stream: stream,
+ extended: true,
+ max_tokens: 4096,
+ });
+
+ llm.system(`
+You are an expert coding assistant integrated into the FDO (FlexDevOps) code editor.
+
+Your role is to help developers with:
+- Code generation based on natural language descriptions
+- Code editing and refactoring
+- Code explanation and documentation
+- Bug fixing and error resolution
+
+Guidelines:
+1. Provide clean, production-ready code that follows best practices
+2. When generating code, match the style and patterns of the surrounding code
+3. When editing code, make minimal changes to achieve the desired result
+4. When explaining code, be concise but thorough
+5. When fixing bugs, explain what was wrong and how you fixed it
+6. Always consider the context of the file being edited (language, framework, etc.)
+7. Format your responses appropriately:
+ - For code generation/editing: return ONLY the code without explanations unless asked
+ - For explanations: provide clear, structured explanations
+ - For fixes: include both the fix and a brief explanation
+
+Remember: You are working within a code editor, so precision and correctness are paramount.
+`);
+
+ return llm;
+}
+
+// Handle code generation
+async function handleGenerateCode(event, data) {
+ const { prompt, language, context } = data;
+ const requestId = crypto.randomUUID();
+
+ try {
+ const assistantInfo = selectCodingAssistant();
+ const llm = await createCodingLlm(assistantInfo, true);
+
+ let fullPrompt = `Generate ${language || "code"} for the following request:\n\n${prompt}`;
+
+ if (context) {
+ fullPrompt += `\n\nContext:\n${context}`;
+ }
+
+ llm.user(fullPrompt);
+ const resp = await llm.chat({ stream: true });
+
+ let fullContent = "";
+
+ if (resp && typeof resp === "object" && "stream" in resp && typeof resp.complete === "function") {
+ for await (const chunk of resp.stream) {
+ if (!chunk) continue;
+ const { type, content: piece } = chunk;
+
+ if (type === "content" && piece && typeof piece === "string") {
+ fullContent += piece;
+ event.sender.send(AiCodingAgentChannels.on_off.STREAM_DELTA, {
+ requestId,
+ type: "content",
+ content: piece,
+ });
+ }
+ }
+
+ await resp.complete();
+ event.sender.send(AiCodingAgentChannels.on_off.STREAM_DONE, { requestId, fullContent });
+ return { success: true, requestId, content: fullContent };
+ }
+
+ return { success: false, error: "Invalid response from LLM" };
+ } catch (error) {
+ event.sender.send(AiCodingAgentChannels.on_off.STREAM_ERROR, {
+ requestId,
+ error: error.message,
+ });
+ return { success: false, error: error.message };
+ }
+}
+
+// Handle code editing
+async function handleEditCode(event, data) {
+ const { code, instruction, language } = data;
+ const requestId = crypto.randomUUID();
+
+ try {
+ const assistantInfo = selectCodingAssistant();
+ const llm = await createCodingLlm(assistantInfo, true);
+
+ const prompt = `Edit the following ${language || ""} code according to this instruction: ${instruction}
+
+Original code:
+\`\`\`${language || ""}
+${code}
+\`\`\`
+
+Provide ONLY the modified code without additional explanations.`;
+
+ llm.user(prompt);
+ const resp = await llm.chat({ stream: true });
+
+ let fullContent = "";
+
+ if (resp && typeof resp === "object" && "stream" in resp && typeof resp.complete === "function") {
+ for await (const chunk of resp.stream) {
+ if (!chunk) continue;
+ const { type, content: piece } = chunk;
+
+ if (type === "content" && piece && typeof piece === "string") {
+ fullContent += piece;
+ event.sender.send(AiCodingAgentChannels.on_off.STREAM_DELTA, {
+ requestId,
+ type: "content",
+ content: piece,
+ });
+ }
+ }
+
+ await resp.complete();
+ event.sender.send(AiCodingAgentChannels.on_off.STREAM_DONE, { requestId, fullContent });
+ return { success: true, requestId, content: fullContent };
+ }
+
+ return { success: false, error: "Invalid response from LLM" };
+ } catch (error) {
+ event.sender.send(AiCodingAgentChannels.on_off.STREAM_ERROR, {
+ requestId,
+ error: error.message,
+ });
+ return { success: false, error: error.message };
+ }
+}
+
+// Handle code explanation
+async function handleExplainCode(event, data) {
+ const { code, language } = data;
+ const requestId = crypto.randomUUID();
+
+ try {
+ const assistantInfo = selectCodingAssistant();
+ const llm = await createCodingLlm(assistantInfo, true);
+
+ const prompt = `Explain the following ${language || ""} code:
+
+\`\`\`${language || ""}
+${code}
+\`\`\`
+
+Provide a clear, concise explanation of what this code does, how it works, and any notable patterns or practices used.`;
+
+ llm.user(prompt);
+ const resp = await llm.chat({ stream: true });
+
+ let fullContent = "";
+
+ if (resp && typeof resp === "object" && "stream" in resp && typeof resp.complete === "function") {
+ for await (const chunk of resp.stream) {
+ if (!chunk) continue;
+ const { type, content: piece } = chunk;
+
+ if (type === "content" && piece && typeof piece === "string") {
+ fullContent += piece;
+ event.sender.send(AiCodingAgentChannels.on_off.STREAM_DELTA, {
+ requestId,
+ type: "content",
+ content: piece,
+ });
+ }
+ }
+
+ await resp.complete();
+ event.sender.send(AiCodingAgentChannels.on_off.STREAM_DONE, { requestId, fullContent });
+ return { success: true, requestId, content: fullContent };
+ }
+
+ return { success: false, error: "Invalid response from LLM" };
+ } catch (error) {
+ event.sender.send(AiCodingAgentChannels.on_off.STREAM_ERROR, {
+ requestId,
+ error: error.message,
+ });
+ return { success: false, error: error.message };
+ }
+}
+
+// Handle code fixing
+async function handleFixCode(event, data) {
+ const { code, error, language } = data;
+ const requestId = crypto.randomUUID();
+
+ try {
+ const assistantInfo = selectCodingAssistant();
+ const llm = await createCodingLlm(assistantInfo, true);
+
+ const prompt = `Fix the following ${language || ""} code that has this error: ${error}
+
+Code with error:
+\`\`\`${language || ""}
+${code}
+\`\`\`
+
+Provide the fixed code and a brief explanation of what was wrong and how you fixed it.`;
+
+ llm.user(prompt);
+ const resp = await llm.chat({ stream: true });
+
+ let fullContent = "";
+
+ if (resp && typeof resp === "object" && "stream" in resp && typeof resp.complete === "function") {
+ for await (const chunk of resp.stream) {
+ if (!chunk) continue;
+ const { type, content: piece } = chunk;
+
+ if (type === "content" && piece && typeof piece === "string") {
+ fullContent += piece;
+ event.sender.send(AiCodingAgentChannels.on_off.STREAM_DELTA, {
+ requestId,
+ type: "content",
+ content: piece,
+ });
+ }
+ }
+
+ await resp.complete();
+ event.sender.send(AiCodingAgentChannels.on_off.STREAM_DONE, { requestId, fullContent });
+ return { success: true, requestId, content: fullContent };
+ }
+
+ return { success: false, error: "Invalid response from LLM" };
+ } catch (error) {
+ event.sender.send(AiCodingAgentChannels.on_off.STREAM_ERROR, {
+ requestId,
+ error: error.message,
+ });
+ return { success: false, error: error.message };
+ }
+}
+
+export function registerAiCodingAgentHandlers() {
+ ipcMain.handle(AiCodingAgentChannels.GENERATE_CODE, handleGenerateCode);
+ ipcMain.handle(AiCodingAgentChannels.EDIT_CODE, handleEditCode);
+ ipcMain.handle(AiCodingAgentChannels.EXPLAIN_CODE, handleExplainCode);
+ ipcMain.handle(AiCodingAgentChannels.FIX_CODE, handleFixCode);
+}
diff --git a/src/ipc/channels.js b/src/ipc/channels.js
index 9dc3f8c..20e9f42 100644
--- a/src/ipc/channels.js
+++ b/src/ipc/channels.js
@@ -60,6 +60,18 @@ export const AiChatChannels = withPrefix('ai-chat', {
}
})
+export const AiCodingAgentChannels = withPrefix('ai-coding-agent', {
+ GENERATE_CODE: 'generate-code',
+ EDIT_CODE: 'edit-code',
+ EXPLAIN_CODE: 'explain-code',
+ FIX_CODE: 'fix-code',
+ on_off: {
+ STREAM_DELTA: 'stream-delta',
+ STREAM_DONE: 'stream-done',
+ STREAM_ERROR: 'stream-error',
+ }
+})
+
export const SystemChannels = withPrefix('system', {
OPEN_EXTERNAL_LINK: 'open-external-link',
GET_PLUGIN_METRIC: 'get-plugin-metric',
diff --git a/src/main.js b/src/main.js
index 601de5f..042203e 100644
--- a/src/main.js
+++ b/src/main.js
@@ -36,6 +36,7 @@ import {initMetrics, logMetric, logStartupError, checkSlowStartupWarning} from "
import {ipcMain} from 'electron';
import {StartupChannels} from "./ipc/channels";
import {registerAiChatHandlers} from "./ipc/ai/ai_chat";
+import {registerAiCodingAgentHandlers} from "./ipc/ai_coding_agent";
// Debug logging to file (works even in packaged mode)
const debugLog = (msg) => {
@@ -650,6 +651,7 @@ app.whenReady().then(async () => {
registerSystemHandlers();
registerPluginHandlers();
registerAiChatHandlers();
+ registerAiCodingAgentHandlers();
const allRoots = settings.get('certificates.root') || [];
const rootCert = allRoots.find(cert =>
diff --git a/src/preload.js b/src/preload.js
index 2c29abf..4f25f57 100644
--- a/src/preload.js
+++ b/src/preload.js
@@ -1,5 +1,5 @@
import {contextBridge, ipcRenderer} from 'electron'
-import {NotificationChannels, PluginChannels, SettingsChannels, SystemChannels, StartupChannels, AiChatChannels} from "./ipc/channels";
+import {NotificationChannels, PluginChannels, SettingsChannels, SystemChannels, StartupChannels, AiChatChannels, AiCodingAgentChannels} from "./ipc/channels";
contextBridge.exposeInMainWorld('electron', {
versions: {
@@ -47,6 +47,22 @@ contextBridge.exposeInMainWorld('electron', {
compressionDone: (cb) => ipcRenderer.removeListener(AiChatChannels.on_off.COMPRESSION_DONE, cb),
}
},
+ aiCodingAgent: {
+ generateCode: (data) => ipcRenderer.invoke(AiCodingAgentChannels.GENERATE_CODE, data),
+ editCode: (data) => ipcRenderer.invoke(AiCodingAgentChannels.EDIT_CODE, data),
+ explainCode: (data) => ipcRenderer.invoke(AiCodingAgentChannels.EXPLAIN_CODE, data),
+ fixCode: (data) => ipcRenderer.invoke(AiCodingAgentChannels.FIX_CODE, data),
+ on: {
+ streamDelta: (callback) => ipcRenderer.on(AiCodingAgentChannels.on_off.STREAM_DELTA, (_, data) => callback(data)),
+ streamDone: (callback) => ipcRenderer.on(AiCodingAgentChannels.on_off.STREAM_DONE, (_, data) => callback(data)),
+ streamError: (callback) => ipcRenderer.on(AiCodingAgentChannels.on_off.STREAM_ERROR, (_, data) => callback(data)),
+ },
+ off: {
+ streamDelta: (callback) => ipcRenderer.removeListener(AiCodingAgentChannels.on_off.STREAM_DELTA, callback),
+ streamDone: (callback) => ipcRenderer.removeListener(AiCodingAgentChannels.on_off.STREAM_DONE, callback),
+ streamError: (callback) => ipcRenderer.removeListener(AiCodingAgentChannels.on_off.STREAM_ERROR, callback),
+ }
+ },
settings: {
certificates: {
getRoot: () => ipcRenderer.invoke(SettingsChannels.certificates.GET_ROOT),
diff --git a/test-results/.last-run.json b/test-results/.last-run.json
index cbcc1fb..d51d76e 100644
--- a/test-results/.last-run.json
+++ b/test-results/.last-run.json
@@ -1,4 +1,7 @@
{
- "status": "passed",
- "failedTests": []
+ "status": "failed",
+ "failedTests": [
+ "8f5573b7aed3a6d66fec-6558235bca5b319efc44",
+ "8f5573b7aed3a6d66fec-355aa4bafb3d477f0eab"
+ ]
}
\ No newline at end of file
diff --git a/tests/e2e/ai-coding-agent.spec.js b/tests/e2e/ai-coding-agent.spec.js
new file mode 100644
index 0000000..08abb99
--- /dev/null
+++ b/tests/e2e/ai-coding-agent.spec.js
@@ -0,0 +1,170 @@
+const { test, expect, _electron: electron } = require('@playwright/test');
+
+let electronApp;
+let mainWindow;
+let editorWindow;
+
+test.beforeAll(async () => {
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ electronApp = await electron.launch({ args: ['.'] });
+ mainWindow = await electronApp.firstWindow();
+
+ // Create a plugin and open the editor
+ await mainWindow.click('button:has-text("Plugins Activated")');
+ await mainWindow.click('text=Create plugin');
+ const randomName = 'test-ai-plugin-' + Math.random().toString(36).substring(2, 8);
+ await mainWindow.fill('#plugin-name', randomName);
+
+ const [newEditorWindow] = await Promise.all([
+ electronApp.waitForEvent('window'),
+ mainWindow.click('text=Open editor')
+ ]);
+ editorWindow = newEditorWindow;
+
+ // Wait for editor to be ready
+ await editorWindow.waitForTimeout(2000);
+}, 60000);
+
+test.afterAll(async () => {
+ for (const win of electronApp.windows()) {
+ try {
+ await win.close();
+ } catch (e) {
+ console.error('Error closing window:', e);
+ }
+ }
+ await electronApp.close();
+}, 60000);
+
+test.describe('AI Coding Agent Tab', () => {
+ test('should display AI Coding Agent tab in the bottom panel', async () => {
+ // Check if the AI Coding Agent tab exists
+ const aiAgentTab = editorWindow.locator('text=AI Coding Agent');
+ await expect(aiAgentTab).toBeVisible({ timeout: 10000 });
+ });
+
+ test('should switch to AI Coding Agent tab when clicked', async () => {
+ // Click on the AI Coding Agent tab
+ await editorWindow.click('text=AI Coding Agent');
+
+ // Wait for the panel to be visible
+ await editorWindow.waitForTimeout(500);
+
+ // Check if the AI Coding Agent panel header is visible
+ const panelHeader = editorWindow.locator('text=AI Coding Assistant');
+ await expect(panelHeader).toBeVisible({ timeout: 5000 });
+ });
+
+ test('should display action dropdown in AI Coding Agent panel', async () => {
+ // Ensure we're on the AI Coding Agent tab
+ await editorWindow.click('text=AI Coding Agent');
+ await editorWindow.waitForTimeout(500);
+
+ // Check if the action dropdown exists
+ const actionSelect = editorWindow.locator('#action-select');
+ await expect(actionSelect).toBeVisible({ timeout: 5000 });
+
+ // Verify default value is "generate"
+ const selectedValue = await actionSelect.inputValue();
+ expect(selectedValue).toBe('generate');
+ });
+
+ test('should display prompt textarea in AI Coding Agent panel', async () => {
+ // Ensure we're on the AI Coding Agent tab
+ await editorWindow.click('text=AI Coding Agent');
+ await editorWindow.waitForTimeout(500);
+
+ // Check if the prompt textarea exists
+ const promptInput = editorWindow.locator('#prompt-input');
+ await expect(promptInput).toBeVisible({ timeout: 5000 });
+ });
+
+ test('should display submit button in AI Coding Agent panel', async () => {
+ // Ensure we're on the AI Coding Agent tab
+ await editorWindow.click('text=AI Coding Agent');
+ await editorWindow.waitForTimeout(500);
+
+ // Check if the submit button exists
+ const submitButton = editorWindow.locator('button:has-text("Submit")');
+ await expect(submitButton).toBeVisible({ timeout: 5000 });
+
+ // Button should be disabled when prompt is empty
+ const isDisabled = await submitButton.isDisabled();
+ expect(isDisabled).toBe(true);
+ });
+
+ test('should enable submit button when prompt is filled', async () => {
+ // Ensure we're on the AI Coding Agent tab
+ await editorWindow.click('text=AI Coding Agent');
+ await editorWindow.waitForTimeout(500);
+
+ // Fill in the prompt
+ const promptInput = editorWindow.locator('#prompt-input');
+ await promptInput.fill('Create a function that adds two numbers');
+
+ // Check if submit button is now enabled
+ const submitButton = editorWindow.locator('button:has-text("Submit")');
+ await editorWindow.waitForTimeout(300);
+ const isDisabled = await submitButton.isDisabled();
+ expect(isDisabled).toBe(false);
+ });
+
+ test('should change action dropdown options', async () => {
+ // Ensure we're on the AI Coding Agent tab
+ await editorWindow.click('text=AI Coding Agent');
+ await editorWindow.waitForTimeout(500);
+
+ // Get the action dropdown
+ const actionSelect = editorWindow.locator('#action-select');
+
+ // Change to "Edit Code"
+ await actionSelect.selectOption('edit');
+ let selectedValue = await actionSelect.inputValue();
+ expect(selectedValue).toBe('edit');
+
+ // Change to "Explain Code"
+ await actionSelect.selectOption('explain');
+ selectedValue = await actionSelect.inputValue();
+ expect(selectedValue).toBe('explain');
+
+ // Change to "Fix Code"
+ await actionSelect.selectOption('fix');
+ selectedValue = await actionSelect.inputValue();
+ expect(selectedValue).toBe('fix');
+
+ // Change back to "Generate Code"
+ await actionSelect.selectOption('generate');
+ selectedValue = await actionSelect.inputValue();
+ expect(selectedValue).toBe('generate');
+ });
+
+ test('should display NonIdealState when no response', async () => {
+ // Ensure we're on the AI Coding Agent tab
+ await editorWindow.click('text=AI Coding Agent');
+ await editorWindow.waitForTimeout(500);
+
+ // Check if NonIdealState is displayed
+ const nonIdealState = editorWindow.locator('text=Select an action and provide a prompt');
+ await expect(nonIdealState).toBeVisible({ timeout: 5000 });
+ });
+
+ test('should switch between tabs (Problems, Output, AI Coding Agent)', async () => {
+ // Click on Problems tab
+ await editorWindow.click('text=Problems');
+ await editorWindow.waitForTimeout(300);
+ let activeTab = await editorWindow.locator('[role="tab"][aria-selected="true"]').textContent();
+ expect(activeTab).toContain('Problems');
+
+ // Click on Output tab
+ await editorWindow.click('[role="tab"]:has-text("Output")');
+ await editorWindow.waitForTimeout(300);
+ activeTab = await editorWindow.locator('[role="tab"][aria-selected="true"]').textContent();
+ expect(activeTab).toContain('Output');
+
+ // Click on AI Coding Agent tab
+ await editorWindow.click('text=AI Coding Agent');
+ await editorWindow.waitForTimeout(300);
+ activeTab = await editorWindow.locator('[role="tab"][aria-selected="true"]').textContent();
+ expect(activeTab).toContain('AI Coding Agent');
+ });
+});
From 59aa78643083b9940f3afe865c8b35040a7f69c7 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 8 Nov 2025 21:20:35 +0000
Subject: [PATCH 03/24] Add FDO SDK knowledge to AI Coding Agent and
comprehensive documentation
- Enhanced AI system prompt with FDO SDK knowledge
- Added detailed documentation in docs/AI_CODING_AGENT.md
- Updated e2e test with better error handling
- AI now understands FDO plugin structure, DOM classes, and SDK patterns
- Documentation includes usage examples, API reference, and troubleshooting
Co-authored-by: anikitenko <12380460+anikitenko@users.noreply.github.com>
---
docs/AI_CODING_AGENT.md | 403 ++++++++++++++++++++++++++++++
src/ipc/ai_coding_agent.js | 73 +++++-
test-results/.last-run.json | 3 +-
tests/e2e/ai-coding-agent.spec.js | 75 ++++--
4 files changed, 522 insertions(+), 32 deletions(-)
create mode 100644 docs/AI_CODING_AGENT.md
diff --git a/docs/AI_CODING_AGENT.md b/docs/AI_CODING_AGENT.md
new file mode 100644
index 0000000..4223678
--- /dev/null
+++ b/docs/AI_CODING_AGENT.md
@@ -0,0 +1,403 @@
+# AI Coding Agent Integration
+
+## Overview
+
+The AI Coding Agent is a new feature integrated into the FDO built-in code editor. It provides AI-powered coding assistance directly within the editor, leveraging the existing LLM infrastructure (`@themaximalist/llm.js`) and coding assistant configuration.
+
+The AI assistant is **FDO SDK-aware** and can help with plugin development using the `@anikitenko/fdo-sdk`.
+
+## Features
+
+The AI Coding Agent supports four main actions:
+
+1. **Generate Code** - Create new code based on natural language descriptions
+2. **Edit Code** - Modify selected code according to instructions
+3. **Explain Code** - Get explanations for selected code blocks
+4. **Fix Code** - Debug and fix code with error messages
+
+## FDO SDK Integration
+
+The AI Coding Agent has built-in knowledge of the FDO SDK, including:
+
+### Plugin Structure
+- Base class: `FDO_SDK`
+- Interface: `FDOInterface`
+- Required metadata: name, version, author, description, icon
+- Lifecycle hooks: `init()`, `render()`
+
+### DOM Element Classes
+- **DOMTable**: Tables with full structure support
+- **DOMMedia**: Images with accessibility
+- **DOMSemantic**: HTML5 semantic elements
+- **DOMNested**: Lists and containers
+- **DOMInput**: Form inputs and selects
+- **DOMText**: Text elements
+- **DOMButton**: Interactive buttons
+- **DOMLink**: Anchor elements
+- **DOMMisc**: Misc elements
+
+### Example SDK Usage
+
+The AI can help you generate plugins like this:
+
+```typescript
+import { FDO_SDK, FDOInterface, PluginMetadata } from "@anikitenko/fdo-sdk";
+
+export default class MyPlugin extends FDO_SDK implements FDOInterface {
+ private readonly _metadata: PluginMetadata = {
+ name: "My Plugin",
+ version: "1.0.0",
+ author: "Your Name",
+ description: "Plugin description",
+ icon: "COG"
+ };
+
+ get metadata(): PluginMetadata {
+ return this._metadata;
+ }
+
+ init(): void {
+ this.log("Plugin initialized!");
+ }
+
+ render(): string {
+ return "
Hello World
";
+ }
+}
+```
+
+## Architecture
+
+### IPC Channels (`src/ipc/channels.js`)
+- Added `AiCodingAgentChannels` with the following operations:
+ - `GENERATE_CODE`
+ - `EDIT_CODE`
+ - `EXPLAIN_CODE`
+ - `FIX_CODE`
+ - Streaming events: `STREAM_DELTA`, `STREAM_DONE`, `STREAM_ERROR`
+
+### Main Process Handler (`src/ipc/ai_coding_agent.js`)
+- Implements handlers for all four AI operations
+- Uses the existing coding assistant configuration from settings
+- Supports streaming responses for real-time feedback
+- Each operation creates a unique request ID for tracking
+- **System prompt includes FDO SDK knowledge**
+
+### Preload API (`src/preload.js`)
+- Exposes `window.electron.aiCodingAgent` with methods:
+ - `generateCode(data)` - Generate new code
+ - `editCode(data)` - Edit existing code
+ - `explainCode(data)` - Explain code functionality
+ - `fixCode(data)` - Fix code errors
+ - Event listeners for streaming updates
+
+### UI Component (`src/components/editor/AiCodingAgentPanel.jsx`)
+- React component integrated into the Editor page
+- Placed as a tab alongside "Problems" and "Output"
+- Features:
+ - Action selector dropdown
+ - Prompt/instruction textarea
+ - Real-time streaming response display
+ - Insert code into editor button
+ - Clear response button
+ - Error handling and display
+
+### Editor Integration (`src/components/editor/EditorPage.jsx` and `BuildOutputTerminalComponent.js`)
+- AI Coding Agent panel added as a new tab in the bottom panel
+- Receives `codeEditor` and `editorModelPath` props for Monaco integration
+- Can read selected code, current language, and file context
+- Can insert generated/edited code back into the editor
+
+## Configuration
+
+### Prerequisites
+
+1. **Add a Coding Assistant** in Settings:
+ - Go to Settings → AI Assistants
+ - Add a new assistant with purpose "Coding Assistant"
+ - Configure with your preferred provider (OpenAI, Anthropic)
+ - Provide API key and select a model (e.g., GPT-4, Claude)
+ - Mark as default
+
+## Usage
+
+### In the Editor
+
+1. Open the Plugin Editor (create a new plugin or edit existing)
+2. Click on the "AI Coding Agent" tab in the bottom panel (next to Problems and Output)
+3. Select an action from the dropdown
+4. Based on the action:
+ - **Generate Code**: Describe what code you want in the prompt field
+ - **Edit Code**: Select code in the editor, then describe the desired changes
+ - **Explain Code**: Select code in the editor, optionally add specific questions
+ - **Fix Code**: Select problematic code, describe the error in the prompt
+5. Click "Submit"
+6. Watch the streaming response appear in real-time
+7. Click "Insert into Editor" to apply the generated/edited code
+
+### Example Prompts for FDO Plugin Development
+
+**Generate a Plugin:**
+```
+Create an FDO plugin that displays system metrics (CPU, memory, disk) using the SDK
+```
+
+**Edit Code:**
+```
+Add error handling to this plugin and use the SDK's DOMTable class to display data
+```
+
+**Explain Code:**
+```
+Explain how this plugin uses the FDO_SDK base class and lifecycle hooks
+```
+
+**Fix Code:**
+```
+This plugin fails to render - TypeError: Cannot read property 'render' of undefined
+```
+
+### Keyboard Shortcuts
+
+Currently no keyboard shortcuts are assigned, but they can be added in the future for:
+- Opening AI Agent tab
+- Submitting prompts
+- Inserting code
+
+## Testing
+
+### Manual Testing
+
+1. **Test Generate FDO Plugin**:
+ - Open editor
+ - Go to AI Coding Agent tab
+ - Select "Generate Code"
+ - Enter: "Create a plugin that shows the current time using FDO SDK"
+ - Submit and verify response includes proper SDK usage
+ - Insert into editor
+
+2. **Test Edit Code**:
+ - Write a basic FDO plugin in the editor
+ - Select the code
+ - Go to AI Coding Agent tab
+ - Select "Edit Code"
+ - Enter: "Use DOMTable class to display data in a table"
+ - Submit and verify response
+
+3. **Test Explain Code**:
+ - Select a plugin code block
+ - Go to AI Coding Agent tab
+ - Select "Explain Code"
+ - Submit and verify explanation mentions SDK concepts
+
+4. **Test Fix Code**:
+ - Write plugin code with a deliberate error
+ - Select the code
+ - Go to AI Coding Agent tab
+ - Select "Fix Code"
+ - Describe the error
+ - Submit and verify fix
+
+### E2E Tests (`tests/e2e/ai-coding-agent.spec.js`)
+
+Automated tests verify:
+- AI Coding Agent tab is visible in the bottom panel
+- Tab switching works correctly
+- Panel components render (action dropdown, prompt textarea, submit button)
+- Submit button is disabled when prompt is empty
+- Submit button is enabled when prompt is filled
+- Action dropdown options can be changed
+- NonIdealState displays when no response
+
+**Note**: E2E tests require a display server (Xvfb) in headless environments. Run with:
+```bash
+xvfb-run npm run test:e2e
+```
+
+### Unit Tests
+
+Unit tests can be added for:
+- IPC handler logic
+- Response streaming
+- Code insertion logic
+- Error handling
+
+## Future Enhancements
+
+### FDO-Specific Features
+1. **Plugin Template Generation**: Quick scaffolding of new plugins
+2. **SDK Auto-Import**: Automatically import SDK classes when generating code
+3. **Plugin Validation**: Check if generated code follows FDO plugin patterns
+4. **Live Preview**: Preview generated plugin UI in real-time
+5. **SDK Documentation Lookup**: Quick access to SDK docs while coding
+
+### General Features
+6. **Context-Aware Suggestions**: Automatically include file dependencies and project structure
+7. **Inline Code Actions**: Add CodeLens or inline buttons for quick AI actions
+8. **Chat History**: Maintain conversation context across multiple requests
+9. **Custom Prompts**: Allow users to save and reuse common prompts
+10. **Multi-file Context**: Analyze and suggest changes across multiple files
+11. **Refactoring Assistant**: Automated refactoring suggestions
+12. **Code Review**: AI-powered code review comments
+13. **Documentation Generation**: Auto-generate JSDoc or other documentation
+14. **Test Generation**: Create unit tests for selected code
+15. **Performance Optimization**: Suggest performance improvements
+
+## Troubleshooting
+
+### "No AI Coding assistant found" Error
+- Ensure you have added a Coding Assistant in Settings
+- Verify the assistant is marked as default
+- Check that API key is valid
+
+### Streaming Not Working
+- Check browser console for errors
+- Verify IPC communication is working
+- Ensure model supports streaming
+
+### Insert Code Not Working
+- Verify code editor is properly mounted
+- Check that Monaco editor instance is available
+- Look for selection/range issues in console
+
+### Generated Code Doesn't Use SDK
+- The AI should automatically use SDK when generating FDO plugins
+- If not, explicitly mention "using FDO SDK" in your prompt
+- Try regenerating with more specific instructions
+
+## API Reference
+
+### window.electron.aiCodingAgent.generateCode(data)
+
+Generates new code based on a prompt.
+
+**Parameters:**
+- `data.prompt` (string): Description of what code to generate
+- `data.language` (string): Programming language (auto-detected from editor)
+- `data.context` (string): Current file content for context
+
+**Returns:** Promise resolving to `{ success, requestId, content }`
+
+**Example:**
+```javascript
+await window.electron.aiCodingAgent.generateCode({
+ prompt: "Create an FDO plugin that displays weather information",
+ language: "typescript",
+ context: "// Current file content..."
+});
+```
+
+### window.electron.aiCodingAgent.editCode(data)
+
+Edits existing code based on instructions.
+
+**Parameters:**
+- `data.code` (string): Code to edit
+- `data.instruction` (string): How to modify the code
+- `data.language` (string): Programming language
+
+**Returns:** Promise resolving to `{ success, requestId, content }`
+
+**Example:**
+```javascript
+await window.electron.aiCodingAgent.editCode({
+ code: selectedCode,
+ instruction: "Add error handling using try-catch",
+ language: "typescript"
+});
+```
+
+### window.electron.aiCodingAgent.explainCode(data)
+
+Explains what code does.
+
+**Parameters:**
+- `data.code` (string): Code to explain
+- `data.language` (string): Programming language
+
+**Returns:** Promise resolving to `{ success, requestId, content }`
+
+**Example:**
+```javascript
+await window.electron.aiCodingAgent.explainCode({
+ code: selectedCode,
+ language: "typescript"
+});
+```
+
+### window.electron.aiCodingAgent.fixCode(data)
+
+Fixes code with errors.
+
+**Parameters:**
+- `data.code` (string): Code with errors
+- `data.error` (string): Error description
+- `data.language` (string): Programming language
+
+**Returns:** Promise resolving to `{ success, requestId, content }`
+
+**Example:**
+```javascript
+await window.electron.aiCodingAgent.fixCode({
+ code: selectedCode,
+ error: "TypeError: Cannot read property 'render' of undefined",
+ language: "typescript"
+});
+```
+
+## Implementation Details
+
+### Streaming Response Handling
+
+The AI Coding Agent uses a streaming approach for better UX:
+
+1. User submits a request
+2. Backend creates a unique `requestId`
+3. Backend starts streaming LLM response
+4. Frontend receives `STREAM_DELTA` events with chunks
+5. Frontend appends chunks to display
+6. Backend sends `STREAM_DONE` when complete
+7. Frontend enables "Insert into Editor" button
+
+### Code Insertion
+
+When inserting code:
+1. Extracts code from markdown code blocks if present
+2. Uses current editor selection as insertion point
+3. Pushes edit operation to Monaco editor
+4. Maintains undo/redo history
+5. Returns focus to editor
+
+### Error Handling
+
+- Network errors displayed as tags with error messages
+- Invalid responses handled gracefully
+- API key errors caught and displayed
+- Streaming interruptions handled with error events
+
+### FDO SDK Knowledge Integration
+
+The AI system prompt includes:
+- FDO SDK class structure and patterns
+- DOM element generation capabilities
+- Plugin lifecycle and metadata requirements
+- Common FDO plugin patterns and best practices
+- Examples of proper SDK usage
+
+This ensures the AI provides context-aware suggestions that align with FDO development practices.
+
+## Contributing
+
+When adding new features to the AI Coding Agent, consider:
+
+1. **Updating the System Prompt**: Add relevant context about new SDK features
+2. **Testing with Real Scenarios**: Validate AI responses against actual plugin development
+3. **Error Handling**: Ensure graceful degradation when AI responses are invalid
+4. **Performance**: Monitor token usage and response times
+5. **User Feedback**: Collect feedback on AI response quality and accuracy
+
+## References
+
+- FDO SDK Repository: https://github.com/anikitenko/fdo-sdk
+- FDO Main Repository: https://github.com/anikitenko/fdo
+- LLM.js Documentation: https://github.com/themaximalist/llm.js
diff --git a/src/ipc/ai_coding_agent.js b/src/ipc/ai_coding_agent.js
index 2fe2c0d..8976a00 100644
--- a/src/ipc/ai_coding_agent.js
+++ b/src/ipc/ai_coding_agent.js
@@ -34,14 +34,73 @@ Your role is to help developers with:
- Code explanation and documentation
- Bug fixing and error resolution
-Guidelines:
+### FDO Plugin Development Context
+
+When working with FDO plugins, be aware of:
+
+**FDO SDK (@anikitenko/fdo-sdk)**
+- Plugins extend the FDO_SDK base class and implement FDOInterface
+- Required metadata: name, version, author, description, icon
+- Lifecycle hooks: init() for initialization, render() for UI rendering
+- Communication: IPC message-based communication with main application
+- Storage: Multiple backends (in-memory, JSON file-based)
+- Logging: Built-in this.log() method
+
+**DOM Element Generation**
+The SDK provides specialized classes for creating HTML elements:
+- DOMTable: Tables with thead, tbody, tfoot, tr, th, td, caption
+- DOMMedia: Images with accessibility support
+- DOMSemantic: article, section, nav, header, footer, aside, main
+- DOMNested: Ordered lists (ol), definition lists (dl, dt, dd)
+- DOMInput: Form inputs, select dropdowns with options
+- DOMText: Headings, paragraphs, spans
+- DOMButton: Buttons with event handlers
+- DOMLink: Anchor elements
+- DOMMisc: Horizontal rules and other elements
+
+All DOM classes support:
+- Custom CSS styling via goober CSS-in-JS
+- Custom classes and inline styles
+- HTML attributes
+- Event handlers
+- Accessibility attributes
+
+**Example Plugin Structure:**
+\`\`\`typescript
+import { FDO_SDK, FDOInterface, PluginMetadata } from "@anikitenko/fdo-sdk";
+
+export default class MyPlugin extends FDO_SDK implements FDOInterface {
+ private readonly _metadata: PluginMetadata = {
+ name: "My Plugin",
+ version: "1.0.0",
+ author: "Your Name",
+ description: "Plugin description",
+ icon: "COG"
+ };
+
+ get metadata(): PluginMetadata {
+ return this._metadata;
+ }
+
+ init(): void {
+ this.log("Plugin initialized!");
+ }
+
+ render(): string {
+ return "
Hello World
";
+ }
+}
+\`\`\`
+
+### Guidelines:
1. Provide clean, production-ready code that follows best practices
-2. When generating code, match the style and patterns of the surrounding code
-3. When editing code, make minimal changes to achieve the desired result
-4. When explaining code, be concise but thorough
-5. When fixing bugs, explain what was wrong and how you fixed it
-6. Always consider the context of the file being edited (language, framework, etc.)
-7. Format your responses appropriately:
+2. When generating FDO plugins, use the SDK's DOM classes for better type safety
+3. When generating code, match the style and patterns of the surrounding code
+4. When editing code, make minimal changes to achieve the desired result
+5. When explaining code, be concise but thorough
+6. When fixing bugs, explain what was wrong and how you fixed it
+7. Always consider the context of the file being edited (language, framework, etc.)
+8. Format your responses appropriately:
- For code generation/editing: return ONLY the code without explanations unless asked
- For explanations: provide clear, structured explanations
- For fixes: include both the fix and a brief explanation
diff --git a/test-results/.last-run.json b/test-results/.last-run.json
index d51d76e..71ce8e2 100644
--- a/test-results/.last-run.json
+++ b/test-results/.last-run.json
@@ -1,7 +1,6 @@
{
"status": "failed",
"failedTests": [
- "8f5573b7aed3a6d66fec-6558235bca5b319efc44",
- "8f5573b7aed3a6d66fec-355aa4bafb3d477f0eab"
+ "43be4d247bf8e9eb807b-fd5f4eb4a1794fe3a07f"
]
}
\ No newline at end of file
diff --git a/tests/e2e/ai-coding-agent.spec.js b/tests/e2e/ai-coding-agent.spec.js
index 08abb99..5d545ff 100644
--- a/tests/e2e/ai-coding-agent.spec.js
+++ b/tests/e2e/ai-coding-agent.spec.js
@@ -5,35 +5,64 @@ let mainWindow;
let editorWindow;
test.beforeAll(async () => {
- await new Promise(resolve => setTimeout(resolve, 1000));
- electronApp = await electron.launch({ args: ['.'] });
- mainWindow = await electronApp.firstWindow();
-
- // Create a plugin and open the editor
- await mainWindow.click('button:has-text("Plugins Activated")');
- await mainWindow.click('text=Create plugin');
- const randomName = 'test-ai-plugin-' + Math.random().toString(36).substring(2, 8);
- await mainWindow.fill('#plugin-name', randomName);
-
- const [newEditorWindow] = await Promise.all([
- electronApp.waitForEvent('window'),
- mainWindow.click('text=Open editor')
- ]);
- editorWindow = newEditorWindow;
-
- // Wait for editor to be ready
- await editorWindow.waitForTimeout(2000);
-}, 60000);
+ try {
+ electronApp = await electron.launch({ args: ['.'] });
+ mainWindow = await electronApp.firstWindow();
+
+ // Attach dialog handler to avoid hangs
+ const acceptAllDialogs = async (dialog) => {
+ try {
+ await dialog.accept();
+ } catch (_) {}
+ };
+ mainWindow.on('dialog', acceptAllDialogs);
+ electronApp.on('window', (page) => {
+ page.on('dialog', acceptAllDialogs);
+ });
+
+ // Wait for the main window to be ready
+ await mainWindow.waitForLoadState('domcontentloaded', { timeout: 30000 });
+
+ // Create a plugin and open the editor
+ await mainWindow.click('button:has-text("Plugins Activated")', { timeout: 10000 });
+ await mainWindow.click('text=Create plugin', { timeout: 5000 });
+ const randomName = 'test-ai-plugin-' + Math.random().toString(36).substring(2, 8);
+ await mainWindow.fill('#plugin-name', randomName, { timeout: 5000 });
+
+ const [newEditorWindow] = await Promise.all([
+ electronApp.waitForEvent('window'),
+ mainWindow.click('text=Open editor', { timeout: 5000 })
+ ]);
+ editorWindow = newEditorWindow;
+
+ // Set up dialog handler for editor window
+ editorWindow.on('dialog', acceptAllDialogs);
+
+ // Wait for editor to be ready
+ await editorWindow.waitForLoadState('domcontentloaded', { timeout: 30000 });
+ await editorWindow.waitForTimeout(2000);
+ } catch (error) {
+ console.error('Error in beforeAll:', error);
+ throw error;
+ }
+}, 90000);
test.afterAll(async () => {
- for (const win of electronApp.windows()) {
+ if (electronApp) {
try {
- await win.close();
+ const windows = electronApp.windows();
+ for (const win of windows) {
+ try {
+ await win.close();
+ } catch (e) {
+ console.error('Error closing window:', e);
+ }
+ }
+ await electronApp.close();
} catch (e) {
- console.error('Error closing window:', e);
+ console.error('Error in afterAll:', e);
}
}
- await electronApp.close();
}, 60000);
test.describe('AI Coding Agent Tab', () => {
From f299128406c26f796da04c20d32dec7cab38c1af Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 8 Nov 2025 21:22:04 +0000
Subject: [PATCH 04/24] Update README with AI Coding Agent feature
documentation
Co-authored-by: anikitenko <12380460+anikitenko@users.noreply.github.com>
---
README.md | 99 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 98 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 0fa43b7..937e49f 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,98 @@
-# fdo
+# FDO (FlexDevOps)
+
+FDO (Flex DevOps) is a modular, plugin-driven DevOps platform built with ElectronJS and React. It empowers developers and SREs to extend core functionality using secure, versioned plugins, offering tools for automation, deployment, monitoring, and workflow customization — all from a unified desktop environment.
+
+## Features
+
+- **Plugin System**: Modular architecture with secure, versioned plugins
+- **Code Editor**: Built-in Monaco-based editor for plugin development
+- **AI Coding Agent**: Intelligent coding assistant integrated into the editor (NEW!)
+- **Live UI**: Real-time plugin UI preview
+- **Certificate Management**: Built-in PKI for plugin signing and trust
+- **SDK**: Comprehensive SDK for building plugins with rich UI capabilities
+
+## AI Coding Agent
+
+The AI Coding Agent provides intelligent coding assistance directly in the FDO editor:
+
+- **Generate Code**: Create new code from natural language descriptions
+- **Edit Code**: Modify selected code with AI-powered suggestions
+- **Explain Code**: Get detailed explanations of code functionality
+- **Fix Code**: Debug and repair code with AI assistance
+
+The AI agent is **FDO SDK-aware** and understands plugin architecture, helping you build better plugins faster.
+
+[Learn more about the AI Coding Agent](docs/AI_CODING_AGENT.md)
+
+## Quick Start
+
+```bash
+# Install dependencies
+npm install
+
+# Development mode
+npm run dev
+
+# Build for production
+npm run build
+
+# Package application
+npm run package
+```
+
+## Plugin Development
+
+FDO uses the [@anikitenko/fdo-sdk](https://github.com/anikitenko/fdo-sdk) for plugin development.
+
+Example plugin:
+
+```typescript
+import { FDO_SDK, FDOInterface, PluginMetadata } from "@anikitenko/fdo-sdk";
+
+export default class MyPlugin extends FDO_SDK implements FDOInterface {
+ private readonly _metadata: PluginMetadata = {
+ name: "My Plugin",
+ version: "1.0.0",
+ author: "Your Name",
+ description: "Plugin description",
+ icon: "COG"
+ };
+
+ get metadata(): PluginMetadata {
+ return this._metadata;
+ }
+
+ init(): void {
+ this.log("Plugin initialized!");
+ }
+
+ render(): string {
+ return "
Hello World
";
+ }
+}
+```
+
+Use the AI Coding Agent in the editor to generate plugins automatically!
+
+## Testing
+
+```bash
+# Run unit tests
+npm run test:unit
+
+# Run e2e tests
+npm run test:e2e
+
+# Run all tests
+npm test
+```
+
+## Documentation
+
+- [AI Coding Agent Documentation](docs/AI_CODING_AGENT.md)
+- [FDO SDK Repository](https://github.com/anikitenko/fdo-sdk)
+
+## License
+
+MIT
+
From bdaa182476ac892370babd90227710e5ad64a973 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 8 Nov 2025 21:42:57 +0000
Subject: [PATCH 05/24] Add assistant selector, API key validation, and
auto-apply with snapshot creation
- Fixed NonIdealState cut-off by adjusting panel CSS (removed redundant padding, added flex properties)
- Added assistant selector dropdown to choose between configured coding assistants
- Added validation to check if coding assistants are available before allowing submissions
- Added auto-apply toggle with snapshot creation before applying changes
- Updated backend handlers to accept and use selected assistantId
- Enhanced UX with loading states and helpful error messages
Co-authored-by: anikitenko <12380460+anikitenko@users.noreply.github.com>
---
src/components/editor/AiCodingAgentPanel.jsx | 142 +++++++++++++++++-
.../editor/AiCodingAgentPanel.module.css | 4 +-
src/ipc/ai_coding_agent.js | 28 ++--
3 files changed, 160 insertions(+), 14 deletions(-)
diff --git a/src/components/editor/AiCodingAgentPanel.jsx b/src/components/editor/AiCodingAgentPanel.jsx
index 155164f..a9b95e0 100644
--- a/src/components/editor/AiCodingAgentPanel.jsx
+++ b/src/components/editor/AiCodingAgentPanel.jsx
@@ -8,9 +8,12 @@ import {
Tag,
Spinner,
NonIdealState,
+ Switch,
+ Callout,
} from "@blueprintjs/core";
import * as styles from "./AiCodingAgentPanel.module.css";
import Markdown from "markdown-to-jsx";
+import virtualFS from "./utils/VirtualFS";
const AI_ACTIONS = [
{ label: "Generate Code", value: "generate" },
@@ -26,8 +29,34 @@ export default function AiCodingAgentPanel({ codeEditor, editorModelPath }) {
const [response, setResponse] = useState("");
const [error, setError] = useState(null);
const [streamingRequestId, setStreamingRequestId] = useState(null);
+ const [autoApply, setAutoApply] = useState(false);
+ const [assistants, setAssistants] = useState([]);
+ const [selectedAssistant, setSelectedAssistant] = useState(null);
+ const [loadingAssistants, setLoadingAssistants] = useState(true);
const responseRef = useRef("");
+ // Load available coding assistants
+ useEffect(() => {
+ async function loadAssistants() {
+ try {
+ setLoadingAssistants(true);
+ const allAssistants = await window.electron.settings.ai.getAssistants();
+ const codingAssistants = allAssistants.filter(a => a.purpose === 'coding');
+ setAssistants(codingAssistants);
+
+ // Select default or first assistant
+ const defaultAssistant = codingAssistants.find(a => a.default);
+ setSelectedAssistant(defaultAssistant || codingAssistants[0] || null);
+ } catch (err) {
+ console.error('Failed to load assistants:', err);
+ setError('Failed to load AI assistants. Please check your settings.');
+ } finally {
+ setLoadingAssistants(false);
+ }
+ }
+ loadAssistants();
+ }, []);
+
useEffect(() => {
const handleStreamDelta = (data) => {
if (data.requestId === streamingRequestId && data.type === "content") {
@@ -40,6 +69,11 @@ export default function AiCodingAgentPanel({ codeEditor, editorModelPath }) {
if (data.requestId === streamingRequestId) {
setIsLoading(false);
setStreamingRequestId(null);
+
+ // Auto-apply if enabled
+ if (autoApply && responseRef.current) {
+ autoInsertCodeIntoEditor();
+ }
}
};
@@ -60,7 +94,7 @@ export default function AiCodingAgentPanel({ codeEditor, editorModelPath }) {
window.electron.aiCodingAgent.off.streamDone(handleStreamDone);
window.electron.aiCodingAgent.off.streamError(handleStreamError);
};
- }, [streamingRequestId]);
+ }, [streamingRequestId, autoApply]);
const getSelectedCode = () => {
if (!codeEditor) return "";
@@ -84,8 +118,38 @@ export default function AiCodingAgentPanel({ codeEditor, editorModelPath }) {
return model.getValue();
};
+ const createSnapshotBeforeApply = () => {
+ try {
+ const currentVersion = virtualFS.fs.version();
+ const tabs = virtualFS.tabs.get().filter((t) => t.id !== "Untitled").map((t) => ({id: t.id, active: t.active}));
+ const created = virtualFS.fs.create(currentVersion.version, tabs);
+ console.log(`Created snapshot ${created.version} before AI code application`);
+ return created;
+ } catch (err) {
+ console.error('Failed to create snapshot:', err);
+ return null;
+ }
+ };
+
+ const autoInsertCodeIntoEditor = () => {
+ // Create snapshot before applying changes
+ const snapshot = createSnapshotBeforeApply();
+ if (!snapshot && autoApply) {
+ setError('Failed to create snapshot before applying changes');
+ return;
+ }
+
+ insertCodeIntoEditor();
+ };
+
const handleSubmit = async () => {
if (!prompt.trim()) return;
+
+ // Validate assistant is selected
+ if (!selectedAssistant) {
+ setError("No coding assistant selected. Please select one from the dropdown or add one in Settings.");
+ return;
+ }
setIsLoading(true);
setError(null);
@@ -104,6 +168,7 @@ export default function AiCodingAgentPanel({ codeEditor, editorModelPath }) {
prompt,
language,
context,
+ assistantId: selectedAssistant.id,
});
break;
case "edit":
@@ -116,6 +181,7 @@ export default function AiCodingAgentPanel({ codeEditor, editorModelPath }) {
code: selectedCode,
instruction: prompt,
language,
+ assistantId: selectedAssistant.id,
});
break;
case "explain":
@@ -127,6 +193,7 @@ export default function AiCodingAgentPanel({ codeEditor, editorModelPath }) {
result = await window.electron.aiCodingAgent.explainCode({
code: selectedCode,
language,
+ assistantId: selectedAssistant.id,
});
break;
case "fix":
@@ -139,6 +206,7 @@ export default function AiCodingAgentPanel({ codeEditor, editorModelPath }) {
code: selectedCode,
error: prompt,
language,
+ assistantId: selectedAssistant.id,
});
break;
default:
@@ -184,6 +252,41 @@ export default function AiCodingAgentPanel({ codeEditor, editorModelPath }) {
setError(null);
};
+ // Show loading state while fetching assistants
+ if (loadingAssistants) {
+ return (
+
diff --git a/src/ipc/ai/tools/weather.js b/src/ipc/ai/tools/weather.js
index 79f5196..64376cf 100644
--- a/src/ipc/ai/tools/weather.js
+++ b/src/ipc/ai/tools/weather.js
@@ -15,10 +15,13 @@ export const getCurrentWeatherTool = {
const q = String(prompt).toLowerCase();
const kws = [
"weather", "forecast", "temperature", "wind", "humidity", "snow",
- "rain", "sunny", "cloud", "storm", "cold", "hot"
+ "rain", "sunny", "cloud", "storm", "cold", "hot",
+ "погода", "дощ", "сонце", "вітер", "температура", "сніг", "гроза", "вітрянно", "парасолю", "парасоля"
];
if (kws.some(k => q.includes(k))) return true;
- if (/in\s+[a-z\s,'-]+\b/.test(q)) return q.includes("weather") || q.includes("forecast");
+ if (/in\s+[a-z\s,'-]+\b/.test(q) || /\bв\s+[а-яіїєґ\s,'-]+\b/i.test(q)) {
+ return kws.some(k => q.includes(k));
+ }
return false;
},
@@ -28,7 +31,8 @@ export const getCurrentWeatherTool = {
if (!city) return { name: "get_current_weather", error: "City is required" };
const q = encodeURIComponent(city);
- const geores = await fetch(`https://geocoding-api.open-meteo.com/v1/search?name=${q}&count=1&language=en&format=json`);
+ const lang = /[а-яіїєґ]/i.test(city) ? "uk" : "en";
+ const geores = await fetch(`https://geocoding-api.open-meteo.com/v1/search?name=${q}&count=1&language=${lang}&format=json`);
const geo = await geores.json();
const first = geo?.results?.[0];
if (!first) return { name: "get_current_weather", error: `City not found: ${city}` };
diff --git a/src/ipc/ai_coding_agent.js b/src/ipc/ai_coding_agent.js
index 5a74373..4ee5683 100644
--- a/src/ipc/ai_coding_agent.js
+++ b/src/ipc/ai_coding_agent.js
@@ -137,14 +137,22 @@ async function handleGenerateCode(event, data) {
fullPrompt += `\n\nContext:\n${context}`;
}
- fullPrompt += `\n\nIMPORTANT: When providing the code to insert, wrap it with a SOLUTION marker like this:
+ fullPrompt += `
+\n\nIMPORTANT: When providing the code to insert, wrap it with a SOLUTION marker like this:
\`\`\`${language || 'code'}
-
+<-- leave one empty line here -->
+// SOLUTION READY TO APPLY
your actual code here
\`\`\`
-You may include other code blocks for examples or explanations, but ONLY the block marked with will be inserted into the editor.`;
+💡 You may include additional code blocks for examples, references, or explanations if helpful,
+but **ONLY the block marked with "// SOLUTION READY TO APPLY"** will be inserted into the editor.
+
+Make sure there is a blank line between the opening code fence and the SOLUTION marker.
+Do NOT literally include the text "<-- leave one empty line here -->" inside the code block.
+\n
+`;
llm.user(fullPrompt);
console.log('[AI Coding Agent Backend] Sending to LLM');
@@ -200,6 +208,7 @@ async function handleEditCode(event, data) {
Original code:
\`\`\`${language || ""}
+<-- leave one empty line here -->
${code}
\`\`\`
@@ -208,11 +217,18 @@ Provide the modified code followed by a brief explanation of what you changed an
IMPORTANT: When providing the modified code, wrap it with a SOLUTION marker like this:
\`\`\`${language || 'code'}
-
+<-- leave one empty line here -->
+// SOLUTION READY TO APPLY
modified code here
\`\`\`
-You may include other code blocks for examples, but ONLY the block marked with will be inserted into the editor.`;
+💡 You may include other code blocks for examples, references, or explanations if helpful,
+but **ONLY** the block marked with "// SOLUTION READY TO APPLY" will be inserted into the editor.
+
+Make sure there is a blank line between the opening code fence and the SOLUTION marker
+(or the first line of code in general).
+Do **NOT** literally include the text "<-- leave one empty line here -->" inside any code block.
+`;
llm.user(prompt);
const resp = await llm.chat({ stream: true });
@@ -316,6 +332,7 @@ async function handleFixCode(event, data) {
Code with error:
\`\`\`${language || ""}
+<-- leave one empty line here -->
${code}
\`\`\`
@@ -324,11 +341,18 @@ Provide the fixed code and a brief explanation of what was wrong and how you fix
IMPORTANT: When providing the fixed code, wrap it with a SOLUTION marker like this:
\`\`\`${language || 'code'}
-
+<-- leave one empty line here -->
+// SOLUTION READY TO APPLY
fixed code here
\`\`\`
-You may include other code blocks for examples, but ONLY the block marked with will be inserted into the editor.`;
+💡 You may include other code blocks for examples, references, or explanations if helpful,
+but **ONLY** the block marked with "// SOLUTION READY TO APPLY" will be inserted into the editor.
+
+Make sure there is a blank line between the opening code fence and the SOLUTION marker
+(or the first line of code in general).
+Do **NOT** literally include the text "<-- leave one empty line here -->" inside any code block.
+`;
llm.user(prompt);
const resp = await llm.chat({ stream: true });
@@ -391,19 +415,26 @@ async function handleSmartMode(event, data) {
fullPrompt += `Provide the appropriate response based on the request.
IMPORTANT: When providing code (for generation, editing, or fixing):
-- Wrap the ACTUAL CODE TO INSERT with a SOLUTION marker:
+
+- Wrap the **actual code to insert** with a SOLUTION marker, like this:
\`\`\`${language || 'code'}
-
+<-- leave one empty line here -->
+// SOLUTION READY TO APPLY
your code here
\`\`\`
-- You may include other code blocks for examples or explanations
-- ONLY the block marked with will be inserted into the editor
-- Clearly explain what you changed and why
-- List each modification with before/after comparison if helpful
+💡 You may include other code blocks for examples, references, or explanations if helpful,
+but **ONLY** the block marked with "// SOLUTION READY TO APPLY" will be inserted into the editor.
+
+- Make sure there is a blank line between the opening code fence and the SOLUTION marker
+ (or the first line of code in general).
+ Do **NOT** literally include the text "<-- leave one empty line here -->" inside any code block.
+- Clearly explain what you changed and why.
+- When applicable, list each modification with before/after comparison.
-Return the code or explanation without meta-commentary about which action you chose.`;
+Return the code or explanation directly — do **not** include meta-commentary about which action you chose.
+`;
llm.user(fullPrompt);
console.log('[AI Coding Agent Backend] Sending to LLM');
From 6e46cb14ced85b840e827e027cc36a67c1a0f61f Mon Sep 17 00:00:00 2001
From: anikitenko
Date: Sun, 9 Nov 2025 21:36:08 +0200
Subject: [PATCH 20/24] add syntax highlighting for AI code responses and
related styles
---
src/assets/css/hljs/xt256.min.css | 1 +
src/components/editor/AiCodingAgentPanel.jsx | 28 ++++++++++++++++++++
2 files changed, 29 insertions(+)
create mode 100644 src/assets/css/hljs/xt256.min.css
diff --git a/src/assets/css/hljs/xt256.min.css b/src/assets/css/hljs/xt256.min.css
new file mode 100644
index 0000000..ef34f0c
--- /dev/null
+++ b/src/assets/css/hljs/xt256.min.css
@@ -0,0 +1 @@
+pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{color:#eaeaea;background:#000}.hljs-subst{color:#eaeaea}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}.hljs-type{color:#eaeaea}.hljs-params{color:#da0000}.hljs-literal,.hljs-name,.hljs-number{color:red;font-weight:bolder}.hljs-comment{color:#969896}.hljs-quote,.hljs-selector-id{color:#0ff}.hljs-template-variable,.hljs-title,.hljs-variable{color:#0ff;font-weight:700}.hljs-keyword,.hljs-selector-class,.hljs-symbol{color:#fff000}.hljs-bullet,.hljs-string{color:#0f0}.hljs-section,.hljs-tag{color:#000fff}.hljs-selector-tag{color:#000fff;font-weight:700}.hljs-attribute,.hljs-built_in,.hljs-link,.hljs-regexp{color:#f0f}.hljs-meta{color:#fff;font-weight:bolder}
\ No newline at end of file
diff --git a/src/components/editor/AiCodingAgentPanel.jsx b/src/components/editor/AiCodingAgentPanel.jsx
index d6fde43..0cbe746 100644
--- a/src/components/editor/AiCodingAgentPanel.jsx
+++ b/src/components/editor/AiCodingAgentPanel.jsx
@@ -12,9 +12,15 @@ import {
TextArea,
} from "@blueprintjs/core";
import * as styles from "./AiCodingAgentPanel.module.css";
+import * as styles2 from "../ai-chat/MarkdownRenderer.module.scss";
import Markdown from "markdown-to-jsx";
import virtualFS from "./utils/VirtualFS";
+import hljs from "../../assets/js/hljs/highlight.min"
+import "../../assets/css/hljs/xt256.min.css"
+
+import classnames from "classnames";
+
const AI_ACTIONS = [
{ label: "Smart Mode (AI decides)", value: "smart" },
{ label: "Generate Code", value: "generate" },
@@ -683,6 +689,12 @@ export default function AiCodingAgentPanel({ codeEditor, response, setResponse }
@@ -699,3 +711,19 @@ export default function AiCodingAgentPanel({ codeEditor, response, setResponse }
);
}
+
+function SyntaxHighlightedCode(props) {
+ const ref = React.useRef(null);
+
+ React.useEffect(() => {
+ if (ref.current && props.className?.includes('lang-') && typeof hljs !== "undefined") {
+ hljs.highlightElement(ref.current);
+
+ // hljs won't reprocess the element unless this attribute is removed
+ ref.current.removeAttribute('data-highlighted');
+ }
+ }, [props.className, props.children]);
+
+ return ;
+}
+
From 2c781bfa0be64e10f91af283cfdca0c2d4eee624 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 9 Nov 2025 19:51:05 +0000
Subject: [PATCH 21/24] Add code refinement feature for iterative AI
improvements
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
NEW FEATURE - Response Refinement:
- "Refine Response" button appears next to "Insert Code" after AI responds
- Sparkles icon (lightbulb) for visual distinction
- Clicking enters refinement mode with clear UI feedback:
* Label changes to "Refinement instructions (optional)"
* Placeholder shows helpful refinement examples
* Submit button text changes to "Submit Refinement"
- Refinement prompt includes previous response as context
- AI receives full conversation history for intelligent improvements
- Mode automatically resets after submission
- Works with all action types and assistant settings
- Compatible with auto-apply and SOLUTION marker extraction
- Buttons disabled during loading to prevent double-submission
User workflow:
1. Generate/edit code → AI responds
2. Click "Refine Response" → UI updates for refinement mode
3. Enter refinement instructions → Submit
4. AI improves code with context → Mode resets
5. Can refine multiple times iteratively
All existing features preserved and working correctly
Co-authored-by: anikitenko <12380460+anikitenko@users.noreply.github.com>
---
src/components/editor/AiCodingAgentPanel.jsx | 51 +++++++++++++++++---
1 file changed, 44 insertions(+), 7 deletions(-)
diff --git a/src/components/editor/AiCodingAgentPanel.jsx b/src/components/editor/AiCodingAgentPanel.jsx
index 0cbe746..a0e928f 100644
--- a/src/components/editor/AiCodingAgentPanel.jsx
+++ b/src/components/editor/AiCodingAgentPanel.jsx
@@ -39,6 +39,7 @@ export default function AiCodingAgentPanel({ codeEditor, response, setResponse }
const [selectedAssistant, setSelectedAssistant] = useState(null);
const [loadingAssistants, setLoadingAssistants] = useState(true);
const [sdkTypes, setSdkTypes] = useState(null);
+ const [isRefining, setIsRefining] = useState(false);
const responseRef = useRef("");
const timeoutRef = useRef(null);
const streamingRequestIdRef = useRef(null);
@@ -281,6 +282,16 @@ export default function AiCodingAgentPanel({ codeEditor, response, setResponse }
insertCodeIntoEditor();
};
+ const handleRefine = () => {
+ if (!response) {
+ console.log('[AI Coding Agent] Cannot refine - no response');
+ return;
+ }
+ console.log('[AI Coding Agent] Entering refinement mode');
+ setIsRefining(true);
+ setPrompt(''); // Clear for refinement instructions
+ };
+
const handleSubmit = async () => {
if (!prompt.trim()) return;
@@ -290,11 +301,21 @@ export default function AiCodingAgentPanel({ codeEditor, response, setResponse }
return;
}
- console.log('[AI Coding Agent] Submit started', { action, prompt: prompt.substring(0, 50) });
+ // Build final prompt - if refining, include previous response as context
+ let finalPrompt = prompt;
+ if (isRefining && response) {
+ finalPrompt = `Previous response:\n${response}\n\nRefinement request:\n${prompt}`;
+ console.log('[AI Coding Agent] Submitting refinement with context');
+ }
+
+ console.log('[AI Coding Agent] Submit started', { action, prompt: finalPrompt.substring(0, 50), isRefining });
setIsLoading(true);
setError(null);
setResponse("");
responseRef.current = "";
+
+ // Reset refinement mode after submitting
+ setIsRefining(false);
// Clear any existing timeout
if (timeoutRef.current) {
@@ -336,7 +357,7 @@ export default function AiCodingAgentPanel({ codeEditor, response, setResponse }
case "smart":
result = await window.electron.aiCodingAgent.smartMode({
requestId,
- prompt,
+ prompt: finalPrompt,
code: selectedCode,
language,
context: enhancedContext,
@@ -346,7 +367,7 @@ export default function AiCodingAgentPanel({ codeEditor, response, setResponse }
case "generate":
result = await window.electron.aiCodingAgent.generateCode({
requestId,
- prompt,
+ prompt: finalPrompt,
language,
context: enhancedContext,
assistantId: selectedAssistant.id,
@@ -365,7 +386,7 @@ export default function AiCodingAgentPanel({ codeEditor, response, setResponse }
result = await window.electron.aiCodingAgent.editCode({
requestId,
code: selectedCode,
- instruction: prompt,
+ instruction: finalPrompt,
language,
assistantId: selectedAssistant.id,
});
@@ -590,7 +611,9 @@ export default function AiCodingAgentPanel({ codeEditor, response, setResponse }
setPrompt(e.target.value)}
placeholder={
- action === "smart"
+ isRefining
+ ? "e.g., Add error handling, or Make it more performant, or Support international domains"
+ : action === "smart"
? "e.g., Add error handling to this function, or Create a validation function, or Explain this algorithm"
: action === "generate"
? "e.g., Create a function that validates email addresses"
@@ -640,7 +665,7 @@ export default function AiCodingAgentPanel({ codeEditor, response, setResponse }