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 + 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/playwright.config.js b/playwright.config.js index 053f526..b7fa3e5 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -3,7 +3,7 @@ const config = { testDir: 'tests/e2e', workers: 1, // Run tests sequentially - testMatch: ['tests/e2e/snapshots.e2e.spec.js'], + testMatch: ['tests/e2e/snapshots.e2e.spec.js', 'tests/e2e/ai-coding-agent.spec.js'], }; module.exports = config; 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 new file mode 100644 index 0000000..e60470a --- /dev/null +++ b/src/components/editor/AiCodingAgentPanel.jsx @@ -0,0 +1,887 @@ +import React, {useEffect, useRef, useState} from "react"; +import { + Button, + Callout, + Card, + FormGroup, + HTMLSelect, + NonIdealState, + Spinner, + Switch, + Tag, + 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" }, + { label: "Edit Code", value: "edit" }, + { label: "Explain Code", value: "explain" }, + { label: "Fix Code", value: "fix" }, + { label: "Plan Code (Plugin Scaffold)", value: "plan" }, +]; + +export default function AiCodingAgentPanel({ codeEditor, response, setResponse }) { + const [action, setAction] = useState("smart"); + const [prompt, setPrompt] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [autoApply, setAutoApply] = useState(false); + const [assistants, setAssistants] = useState([]); + const [selectedAssistant, setSelectedAssistant] = useState(null); + const [loadingAssistants, setLoadingAssistants] = useState(true); + const [sdkTypes, setSdkTypes] = useState(null); + const [isRefining, setIsRefining] = useState(false); + const [uploadedImage, setUploadedImage] = useState(null); + const [imagePreview, setImagePreview] = useState(null); + const responseRef = useRef(""); + const timeoutRef = useRef(null); + const streamingRequestIdRef = useRef(null); + const autoApplyRef = useRef(autoApply); + const fileInputRef = useRef(null); + + // Load SDK types on mount + useEffect(() => { + async function loadSdkTypes() { + try { + const result = await window.electron.system.getFdoSdkTypes(); + if (result && result.success && result.files) { + setSdkTypes(result.files); + console.log('[AI Coding Agent] SDK types loaded', { filesCount: result.files.length }); + } else { + console.error('[AI Coding Agent] Failed to load SDK types', result ? result.error : 'Unknown error'); + } + } catch (err) { + console.error('[AI Coding Agent] Failed to load SDK types', err); + } + } + loadSdkTypes(); + }, []); + + // 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(); + }, []); + + // Keep autoApplyRef in sync with autoApply state + useEffect(() => { + autoApplyRef.current = autoApply; + }, [autoApply]); + + // Store handlers in refs to ensure proper cleanup and prevent duplicates + const handlersRef = useRef({ + delta: null, + done: null, + error: null + }); + + const listenersRegistered = useRef(false); + useEffect(() => { + if (listenersRegistered.current) return; // Prevent double-registration + listenersRegistered.current = true; + // Create handler functions + const handleStreamDelta = (data) => { + console.log('[AI Coding Agent] Stream delta received', { requestId: data.requestId, contentLength: data.content ? data.content.length : 0 }); + // Robust validation: only process if requestId matches AND content is valid + if (data.requestId === streamingRequestIdRef.current && + data.content && + typeof data.content === 'string' && + data.content.length > 0 && + /\S/.test(data.content)) { // Must contain at least one non-whitespace character + responseRef.current += data.content; + setResponse(responseRef.current); + console.log('[AI Coding Agent] Response updated', { totalLength: responseRef.current.length }); + } + }; + + const handleStreamDone = (data) => { + const match = !streamingRequestIdRef.current || data.requestId === streamingRequestIdRef.current; + if (!match) { + console.warn("[AI Coding Agent] Stream done mismatch", data.requestId); + return; + } + // ALWAYS clear timeout FIRST (critical to prevent timeout errors) + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + console.log('[AI Coding Agent] Timeout cleared'); + } + console.log('[AI Coding Agent] Stream done', { requestId: data.requestId, streamingRequestId: streamingRequestIdRef.current }); + + console.log('[AI Coding Agent] Completing stream'); + setResponse(data.fullContent); + setIsLoading(false); + + // Auto-apply if enabled - use ref to get latest value + if (autoApplyRef.current && responseRef.current) { + autoInsertCodeIntoEditor(); + } + }; + + const handleStreamError = (data) => { + console.error('[AI Coding Agent] Stream error', data); + if (data.requestId === streamingRequestIdRef.current) { + setError(data.error); + setIsLoading(false); + streamingRequestIdRef.current = null; + + // Clear timeout + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } + } + }; + + // Store handlers in ref for cleanup + handlersRef.current = { + delta: handleStreamDelta, + done: handleStreamDone, + error: handleStreamError + }; + + // Register event handlers + window.electron.aiCodingAgent.on.streamDelta(handleStreamDelta); + window.electron.aiCodingAgent.on.streamDone(handleStreamDone); + window.electron.aiCodingAgent.on.streamError(handleStreamError); + + console.log('[AI Coding Agent] Event handlers registered'); + + return () => { + // Use stored refs for cleanup to ensure we remove the exact same handlers + if (handlersRef.current.delta) { + window.electron.aiCodingAgent.off.streamDelta(handlersRef.current.delta); + } + if (handlersRef.current.done) { + window.electron.aiCodingAgent.off.streamDone(handlersRef.current.done); + } + if (handlersRef.current.error) { + window.electron.aiCodingAgent.off.streamError(handlersRef.current.error); + } + console.log('[AI Coding Agent] Event handlers cleaned up'); + }; + }, []); // Empty dependency array - only register once + + const getSelectedCode = () => { + if (!codeEditor) return ""; + const selection = codeEditor.getSelection(); + const model = codeEditor.getModel(); + if (!model) return ""; + return model.getValueInRange(selection); + }; + + const getLanguage = () => { + if (!codeEditor) return ""; + const model = codeEditor.getModel(); + if (!model) return ""; + return model.getLanguageId(); + }; + + const getContext = () => { + if (!codeEditor) return ""; + const model = codeEditor.getModel(); + if (!model) return ""; + return model.getValue(); + }; + + const getAllProjectFiles = () => { + try { + const models = virtualFS.listModels(); + return models.map(model => { + const uri = model.uri.toString(true).replace("file://", ""); + // Skip node_modules and dist + if (uri.includes("/node_modules/") || uri.includes("/dist/")) { + return null; + } + return { + path: uri, + content: model.getValue() + }; + }).filter(Boolean); + } catch (err) { + console.error('[AI Coding Agent] Error getting project files', err); + return []; + } + }; + + const buildProjectContext = (currentFileContext) => { + const projectFiles = getAllProjectFiles(); + let context = ''; + + // Add SDK types reference + if (sdkTypes && sdkTypes.length > 0) { + context += `FDO SDK Type Definitions (for reference):\n`; + sdkTypes.forEach(file => { + // Include full SDK types as they contain comprehensive documentation + context += `\nSDK File: ${file.name}\n\`\`\`typescript\n${file.content}\n\`\`\`\n`; + }); + context += `\n---\n\n`; + } + + if (currentFileContext) { + context += `Current file content:\n${currentFileContext}\n\n`; + } + + if (projectFiles.length > 0) { + context += `Project files (${projectFiles.length} files):\n`; + projectFiles.forEach(file => { + // Limit file content to first 500 chars to avoid token limits + const preview = file.content.length > 500 + ? file.content.substring(0, 500) + '...' + : file.content; + context += `\nFile: ${file.path}\n\`\`\`\n${preview}\n\`\`\`\n`; + }); + } + + return context; + }; + + 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 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 handleImageUpload = (event) => { + const file = event.target.files[0]; + if (!file) return; + + if (!file.type.startsWith('image/')) { + setError('Please upload an image file'); + return; + } + + const reader = new FileReader(); + reader.onload = (e) => { + const base64Image = e.target.result; + setUploadedImage(base64Image); + setImagePreview(URL.createObjectURL(file)); + console.log('[AI Coding Agent] Image uploaded', { size: file.size, type: file.type }); + }; + reader.onerror = () => { + setError('Failed to read image file'); + }; + reader.readAsDataURL(file); + }; + + const handleRemoveImage = () => { + setUploadedImage(null); + if (imagePreview) { + URL.revokeObjectURL(imagePreview); + } + setImagePreview(null); + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + console.log('[AI Coding Agent] Image removed'); + }; + + const handleCopyPlan = () => { + if (!response) return; + navigator.clipboard.writeText(response); + console.log('[AI Coding Agent] Plan copied to clipboard'); + }; + + const handleExecutePlan = async () => { + if (!response) return; + // TODO: Parse plan and generate files in VirtualFS + console.log('[AI Coding Agent] Executing plan...'); + setError('Plan execution coming soon!'); + }; + + 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; + } + + // 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) { + clearTimeout(timeoutRef.current); + } + + // Set a safety timeout to prevent hanging forever + timeoutRef.current = setTimeout(() => { + console.error('[AI Coding Agent] Request timeout after 60s'); + setError("Request timed out. The AI service may be unavailable. Please try again."); + setIsLoading(false); + timeoutRef.current = null; + }, 60000); // 60 second timeout + + try { + const selectedCode = getSelectedCode(); + const language = getLanguage(); + const currentFileContext = getContext(); + + // Build comprehensive project context for smart mode and generate + const enhancedContext = (action === "smart" || action === "generate") + ? buildProjectContext(currentFileContext) + : ""; + + console.log('[AI Coding Agent] Preparing request', { + action, + hasCode: !!selectedCode, + language, + contextLength: enhancedContext.length + }); + + // Generate requestId upfront so we can track streaming events + const requestId = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + streamingRequestIdRef.current = requestId; + console.log('[AI Coding Agent] Request ID set', requestId); + + let result; + switch (action) { + case "smart": + result = await window.electron.aiCodingAgent.smartMode({ + requestId, + prompt: finalPrompt, + code: selectedCode, + language, + context: enhancedContext, + assistantId: selectedAssistant.id, + }); + break; + case "generate": + result = await window.electron.aiCodingAgent.generateCode({ + requestId, + prompt: finalPrompt, + language, + context: enhancedContext, + assistantId: selectedAssistant.id, + }); + break; + case "edit": + if (!selectedCode) { + setError("Please select code to edit"); + setIsLoading(false); + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } + return; + } + result = await window.electron.aiCodingAgent.editCode({ + requestId, + code: selectedCode, + instruction: finalPrompt, + language, + assistantId: selectedAssistant.id, + }); + break; + case "explain": + if (!selectedCode) { + setError("Please select code to explain"); + setIsLoading(false); + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } + return; + } + result = await window.electron.aiCodingAgent.explainCode({ + requestId, + code: selectedCode, + language, + assistantId: selectedAssistant.id, + }); + break; + case "fix": + if (!selectedCode) { + setError("Please select code to fix"); + setIsLoading(false); + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } + return; + } + result = await window.electron.aiCodingAgent.fixCode({ + requestId, + code: selectedCode, + error: prompt, + language, + assistantId: selectedAssistant.id, + }); + break; + case "plan": + result = await window.electron.aiCodingAgent.planCode({ + requestId, + prompt: finalPrompt, + image: uploadedImage, // base64 image if uploaded + assistantId: selectedAssistant.id, + }); + break; + default: + break; + } + + console.log('[AI Coding Agent] IPC result received', result); + + if (result && result.success && result.requestId) { + console.log('[AI Coding Agent] Request successful, streaming complete', { requestId: result.requestId }); + // Request ID already set before IPC call, streaming events should have flowed + // Verify requestId matches + if (result.requestId !== requestId) { + console.warn('[AI Coding Agent] RequestId mismatch in result', { expected: requestId, received: result.requestId }); + } + + // Clear streamingRequestId now that IPC is complete + // This prevents any late/duplicate done events from matching + streamingRequestIdRef.current = null; + } else if (result && result.error) { + console.error('[AI Coding Agent] Error in result', result.error); + setError(result.error); + setIsLoading(false); + streamingRequestIdRef.current = null; + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } + } else { + console.error('[AI Coding Agent] Invalid result - missing requestId or success flag', result); + setError("Invalid response from AI service. Please try again."); + setIsLoading(false); + streamingRequestIdRef.current = null; + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } + } + } catch (err) { + console.error('[AI Coding Agent] Exception in handleSubmit', err); + setError(err.message || "An error occurred"); + setIsLoading(false); + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } + } + }; + + const insertCodeIntoEditor = () => { + if (!codeEditor || !response) { + console.log('[AI Coding Agent] Cannot insert - no editor or response'); + return; + } + + const selection = codeEditor.getSelection(); + const model = codeEditor.getModel(); + if (!model) { + console.log('[AI Coding Agent] Cannot insert - no model'); + return; + } + + // Priority 1: Look for SOLUTION-marked code block (// SOLUTION READY TO APPLY) + const solutionRegex = /```(?:\w+)?\s*\n(?:\s*\n)?\s*\/\/\s*SOLUTION(?:\s*READY\s*TO\s*APPLY)?\s*\n([\s\S]*?)```/g; + const solutionMatches = [...response.matchAll(solutionRegex)]; + + let codeToInsert; + if (solutionMatches.length > 0) { + // Use the SOLUTION-marked code block (the actual code to insert) + codeToInsert = solutionMatches[0][1].trim(); + console.log('[AI Coding Agent] Inserting SOLUTION-marked code block', { + solutionBlocksFound: solutionMatches.length, + codeLength: codeToInsert.length + }); + } else { + // Priority 2: Look for any code blocks + const anyCodeRegex = /```(?:\w+)?\n([\s\S]*?)```/g; + const anyMatches = [...response.matchAll(anyCodeRegex)]; + + if (anyMatches.length > 0) { + // Use the LAST code block (most likely the actual code, not an example) + codeToInsert = anyMatches[anyMatches.length - 1][1].trim(); + console.log('[AI Coding Agent] Inserting last code block', { + codeBlocksFound: anyMatches.length, + codeLength: codeToInsert.length + }); + } else { + // Priority 3: No code blocks - use full response + codeToInsert = response.trim(); + console.log('[AI Coding Agent] No code blocks found, inserting full response'); + } + } + + const edit = { + range: selection, + text: codeToInsert, + forceMoveMarkers: true, + }; + + model.pushEditOperations([], [edit], () => null); + codeEditor.focus(); + + console.log('[AI Coding Agent] Code inserted successfully'); + }; + + const clearResponse = () => { + setResponse(""); + responseRef.current = ""; + setError(null); + }; + + // Show loading state while fetching assistants + if (loadingAssistants) { + return ( +
+
+ } + title="Loading AI Assistants" + description="Please wait..." + /> +
+
+ ); + } + + // Show message if no assistants available + if (assistants.length === 0) { + return ( +
+
+ +

No AI coding assistants found.

+

Please add a coding assistant in Settings → AI Assistants.

+
+ } + /> +
+ + ); + } + + return ( +
+
+

AI Coding Agent

+ + Beta + +
+ +
+ + { + const assistant = assistants.find(a => a.id === e.target.value); + setSelectedAssistant(assistant); + }} + fill + > + {assistants.map(assistant => ( + + ))} + + + + + setAction(e.target.value)} + options={AI_ACTIONS} + fill + /> + + + {action === "plan" && ( + + +
+
+ )} +
+ + )} + + +