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 "
No AI coding assistants found.
+Please add a coding assistant in Settings → AI Assistants.
++ The AI is analyzing your prompt and generating a response. This may take a few moments. +
+ +;
+}
+
diff --git a/src/components/editor/AiCodingAgentPanel.module.css b/src/components/editor/AiCodingAgentPanel.module.css
new file mode 100644
index 0000000..e992443
--- /dev/null
+++ b/src/components/editor/AiCodingAgentPanel.module.css
@@ -0,0 +1,87 @@
+.ai-coding-agent-panel {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ background-color: transparent;
+ padding: 8px;
+}
+
+.panel-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 12px;
+ flex-shrink: 0;
+}
+
+.panel-header h3 {
+ margin: 0;
+ font-size: 16px;
+ font-weight: 600;
+}
+
+.panel-content {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ flex: 1 1 auto;
+ min-height: 0;
+ overflow-y: auto;
+}
+
+.action-buttons {
+ display: flex;
+ gap: 8px;
+ flex-wrap: wrap;
+}
+
+.error-message {
+ margin-top: 8px;
+}
+
+.loading-indicator {
+ margin-top: 12px;
+ flex-shrink: 0;
+}
+
+.response-container {
+ margin-top: 12px;
+}
+
+.response-container h4 {
+ margin: 0 0 8px 0;
+ font-size: 13px;
+ font-weight: 600;
+}
+
+.response-card {
+ padding: 12px;
+ background-color: var(--bp6-dark-gray-5);
+ border-radius: 4px;
+ max-height: 300px;
+ overflow-y: auto;
+}
+
+.response-card pre {
+ background-color: var(--bp6-dark-gray-2);
+ padding: 8px;
+ border-radius: 4px;
+ overflow-x: auto;
+}
+
+.response-card code {
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace;
+ font-size: 12px;
+}
+
+.response-card p {
+ margin: 6px 0;
+ font-size: 13px;
+}
+
+.response-card ul,
+.response-card ol {
+ margin: 6px 0;
+ padding-left: 20px;
+ font-size: 13px;
+}
diff --git a/src/components/editor/BuildOutputTerminalComponent.js b/src/components/editor/BuildOutputTerminalComponent.js
index 9063f84..63486b5 100644
--- a/src/components/editor/BuildOutputTerminalComponent.js
+++ b/src/components/editor/BuildOutputTerminalComponent.js
@@ -5,12 +5,14 @@ import {PropTypes} from 'prop-types';
import * as styles from "./EditorPage.module.css";
import {AppToaster} from "../AppToaster.jsx";
import {v4 as uuidv4} from 'uuid';
+import AiCodingAgentPanel from "./AiCodingAgentPanel.jsx";
-const BuildOutputTerminalComponent = ({selectedTabId, setSelectedTabId}) => {
- const [markers, setMarkers] = useState(virtualFS.tabs.listMarkers())
- const [buildOutputStatus, setBuildOutputStatus] = useState(virtualFS.build.status())
- const [buildOutput, setBuildOutput] = useState([])
- const [buildOutputIntent, setBuildOutputIntent] = useState("primary")
+const BuildOutputTerminalComponent = ({selectedTabId, setSelectedTabId, codeEditor}) => {
+ const [markers, setMarkers] = useState(virtualFS.tabs.listMarkers());
+ const [buildOutputStatus, setBuildOutputStatus] = useState(virtualFS.build.status());
+ const [buildOutput, setBuildOutput] = useState([]);
+ const [buildOutputIntent, setBuildOutputIntent] = useState("primary");
+ const [codingAiResponse, setCodingAiResponse] = useState("");
const totalMarkers = markers.reduce((acc, marker) => {
return acc + marker.markers.length
}, 0)
@@ -70,17 +72,25 @@ const BuildOutputTerminalComponent = ({selectedTabId, setSelectedTabId}) => {
}
}/>
+
{selectedTabId === "problems" && ()}
{selectedTabId === "output" && ()}
+ {selectedTabId === "ai-agent" && (
+
+
+
+ )}
)
}
BuildOutputTerminalComponent.propTypes = {
selectedTabId: PropTypes.string.isRequired,
- setSelectedTabId: PropTypes.func.isRequired
+ setSelectedTabId: PropTypes.func.isRequired,
+ codeEditor: PropTypes.object,
}
const ProblemsPanel = ({markers}) => {
diff --git a/src/components/editor/EditorPage.jsx b/src/components/editor/EditorPage.jsx
index 8ab0f5a..e87c6cd 100644
--- a/src/components/editor/EditorPage.jsx
+++ b/src/components/editor/EditorPage.jsx
@@ -314,8 +314,12 @@ export const EditorPage = () => {
className={styles["gutter-row-editor-terminal"]} {...getInnerCodeGutterProps('row', 1)}>
-
+
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
new file mode 100644
index 0000000..c9ba2fb
--- /dev/null
+++ b/src/ipc/ai_coding_agent.js
@@ -0,0 +1,483 @@
+import {ipcMain} from "electron";
+import {AiCodingAgentChannels} from "./channels.js";
+import LLM from "@themaximalist/llm.js";
+import {settings} from "../utils/store.js";
+
+// Select a coding assistant from settings
+function selectCodingAssistant(assistantId) {
+ const list = settings.get("ai.coding", []) || [];
+
+ // If assistantId is provided, find that specific assistant
+ if (assistantId) {
+ const assistant = list.find(a => a.id === assistantId);
+ if (assistant) return assistant;
+ }
+
+ // Otherwise, fall back to default or first
+ const assistantInfo = list.find(a => a.default) || list[0];
+ if (!assistantInfo) {
+ throw new Error("No AI Coding assistant found. Please add one in Settings → AI Assistants.");
+ }
+ 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
+
+### 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 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
+
+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 { requestId, prompt, language, context, assistantId } = data;
+
+ console.log('[AI Coding Agent Backend] Generate code request', { requestId, language, promptLength: prompt?.length, assistantId });
+
+ try {
+ const assistantInfo = selectCodingAssistant(assistantId);
+ console.log('[AI Coding Agent Backend] Assistant selected', { name: assistantInfo.name, provider: assistantInfo.provider, model: assistantInfo.model });
+
+ 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}`;
+ }
+
+ 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 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');
+ const resp = await llm.chat({ stream: true });
+
+ let fullContent = "";
+
+ if (resp && typeof resp === "object" && "stream" in resp && typeof resp.complete === "function") {
+ console.log('[AI Coding Agent Backend] Streaming started');
+ 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();
+ console.log('[AI Coding Agent Backend] Streaming complete', { requestId, contentLength: fullContent.length });
+ event.sender.send(AiCodingAgentChannels.on_off.STREAM_DONE, { requestId, fullContent });
+ return { success: true, requestId, content: fullContent };
+ }
+
+ console.error('[AI Coding Agent Backend] Invalid LLM response');
+ return { success: false, error: "Invalid response from LLM" };
+ } catch (error) {
+ console.error('[AI Coding Agent Backend] Error in handleGenerateCode', 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 { requestId, code, instruction, language, assistantId } = data;
+
+ console.log('[AI Coding Agent Backend] Edit code request', { requestId, language, instructionLength: instruction?.length, assistantId });
+
+ try {
+ const assistantInfo = selectCodingAssistant(assistantId);
+ const llm = await createCodingLlm(assistantInfo, true);
+
+ const prompt = `Edit the following ${language || ""} code according to this instruction: ${instruction}
+
+Original code:
+\`\`\`${language || ""}
+<-- leave one empty line here -->
+${code}
+\`\`\`
+
+Provide the modified code followed by a brief explanation of what you changed and why.
+
+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, 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 });
+
+ 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 { requestId, code, language, assistantId } = data;
+
+ console.log('[AI Coding Agent Backend] Explain code request', { requestId, language, codeLength: code?.length, assistantId });
+
+ try {
+ const assistantInfo = selectCodingAssistant(assistantId);
+ 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 { requestId, code, error, language, assistantId } = data;
+
+ console.log('[AI Coding Agent Backend] Fix code request', { requestId, language, codeLength: code?.length, assistantId });
+
+ try {
+ const assistantInfo = selectCodingAssistant(assistantId);
+ const llm = await createCodingLlm(assistantInfo, true);
+
+ const prompt = `Fix the following ${language || ""} code that has this error: ${error}
+
+Code with error:
+\`\`\`${language || ""}
+<-- leave one empty line here -->
+${code}
+\`\`\`
+
+Provide the fixed code and a brief explanation of what was wrong and how you fixed it.
+
+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, 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 });
+
+ 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 smart mode - AI determines the action
+async function handleSmartMode(event, data) {
+ const { requestId, prompt, code, language, context, assistantId } = data;
+
+ console.log('[AI Coding Agent Backend] Smart mode request', { requestId, language, promptLength: prompt?.length, hasCode: !!code, hasContext: !!context });
+
+ try {
+ const assistantInfo = selectCodingAssistant(assistantId);
+ const llm = await createCodingLlm(assistantInfo, true);
+
+ // Build a clear prompt for the AI
+ let fullPrompt = `User's request: ${prompt}\n\n`;
+
+ // If code is selected, show it prominently
+ if (code) {
+ fullPrompt += `Selected code in ${language || 'current file'}:\n\`\`\`${language || ''}\n${code}\n\`\`\`\n\n`;
+ }
+
+ // Add context (SDK types, project files, current file) if available
+ if (context) {
+ fullPrompt += `Additional context:\n${context}\n\n`;
+ }
+
+ 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, like this:
+
+\`\`\`${language || 'code'}
+<-- leave one empty line here -->
+// SOLUTION READY TO APPLY
+your code here
+\`\`\`
+
+💡 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 directly — do **not** include meta-commentary about which action you chose.
+`;
+
+ llm.user(fullPrompt);
+ console.log('[AI Coding Agent Backend] Sending to LLM');
+ const resp = await llm.chat({ stream: true });
+
+ let fullContent = "";
+
+ if (resp && typeof resp === "object" && "stream" in resp && typeof resp.complete === "function") {
+ console.log('[AI Coding Agent Backend] Streaming started');
+ 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();
+ console.log('[AI Coding Agent Backend] Streaming complete', { requestId, contentLength: fullContent.length });
+ 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) {
+ console.error('[AI Coding Agent Backend] Error in handleSmartMode', 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);
+ ipcMain.handle(AiCodingAgentChannels.SMART_MODE, handleSmartMode);
+}
diff --git a/src/ipc/channels.js b/src/ipc/channels.js
index 9dc3f8c..02c7e06 100644
--- a/src/ipc/channels.js
+++ b/src/ipc/channels.js
@@ -60,6 +60,20 @@ 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',
+ SMART_MODE: 'smart-mode',
+ PLAN_CODE: 'plan-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..98b8ea1 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,24 @@ 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),
+ smartMode: (data) => ipcRenderer.invoke(AiCodingAgentChannels.SMART_MODE, data),
+ planCode: (data) => ipcRenderer.invoke(AiCodingAgentChannels.PLAN_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..71ce8e2 100644
--- a/test-results/.last-run.json
+++ b/test-results/.last-run.json
@@ -1,4 +1,6 @@
{
- "status": "passed",
- "failedTests": []
+ "status": "failed",
+ "failedTests": [
+ "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
new file mode 100644
index 0000000..5d545ff
--- /dev/null
+++ b/tests/e2e/ai-coding-agent.spec.js
@@ -0,0 +1,199 @@
+const { test, expect, _electron: electron } = require('@playwright/test');
+
+let electronApp;
+let mainWindow;
+let editorWindow;
+
+test.beforeAll(async () => {
+ 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 () => {
+ if (electronApp) {
+ try {
+ 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 in afterAll:', e);
+ }
+ }
+}, 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');
+ });
+});