Jules was unable to complete the task in time. Please review the work done so far and provide feedback for Jules to continue.

This commit is contained in:
google-labs-jules[bot] 2025-06-07 17:06:00 +00:00
parent 891257c1e2
commit 755e86559c
7 changed files with 512 additions and 1 deletions

View File

@ -106,6 +106,20 @@ project, please check the [project management guide](./PROJECT.md) to get starte
- **Integration-ready Docker support** for a hassle-free setup.
- **Deploy** directly to **Netlify**
### AI Coding Assistant (Experimental)
Bolt.diy includes experimental AI-powered coding assistance:
* **AI Code Completion:** Suggestions appear automatically as you type or can be explicitly invoked (often Ctrl+Space or Alt+`). AI completions are integrated into the standard completion list.
* **AI Code Suggestions/Refactoring:**
* Trigger: `Cmd-Alt-r` (macOS) / `Ctrl-Alt-r` (Windows/Linux)
* Select a block of code or place your cursor to have the AI analyze it for potential improvements or refactorings.
* **AI Bug Fixing:**
* Trigger: `Cmd-Alt-f` (macOS) / `Ctrl-Alt-f` (Windows/Linux)
* Select code or use on the whole file to ask the AI to find and suggest fixes for bugs.
Suggestions and bug fixes from the AI assistant will appear as editor diagnostics (inline highlights, gutter icons). You can view them in detail and apply actions from the **Lint Panel** (toggle with `Cmd-Shift-m` / `Ctrl-Shift-m`).
## Setup
If you're new to installing software from GitHub, don't worry! If you encounter any issues, feel free to submit an "issue" using the provided links or improve this documentation by forking the repository, editing the instructions, and submitting a pull request. The following instruction will help you get the stable branch up and running on your local machine in no time.

View File

@ -1,6 +1,9 @@
import { acceptCompletion, autocompletion, closeBrackets } from '@codemirror/autocomplete';
import { aiCompletionSource } from './aiCompletions';
import { triggerAIRefactorCommand, triggerAIBugFixCommand } from './aiLintSource';
import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
import { bracketMatching, foldGutter, indentOnInput, indentUnit } from '@codemirror/language';
import { linter, lintGutter, lintKeymap } from '@codemirror/lint';
import { searchKeymap } from '@codemirror/search';
import { Compartment, EditorSelection, EditorState, StateEffect, StateField, type Extension } from '@codemirror/state';
import {
@ -361,7 +364,10 @@ function newEditorState(
...defaultKeymap,
...historyKeymap,
...searchKeymap,
...lintKeymap,
{ key: 'Tab', run: acceptCompletion },
{ key: 'Mod-Alt-r', run: triggerAIRefactorCommand },
{ key: 'Mod-Alt-f', run: triggerAIBugFixCommand },
{
key: 'Mod-s',
preventDefault: true,
@ -374,7 +380,11 @@ function newEditorState(
]),
indentUnit.of('\t'),
autocompletion({
closeOnBlur: false,
closeOnBlur: false, // Existing option
override: [aiCompletionSource], // Add our AI source
activateOnTyping: true, // Explicitly set or ensure this is the desired behavior
// for AI suggestions to trigger during typing.
// Our aiCompletionSource also has internal logic to decide when to fire.
}),
tooltips({
position: 'absolute',
@ -412,6 +422,10 @@ function newEditorState(
return icon;
},
}),
linter(() => [], {
delay: 750,
}),
lintGutter(),
...extensions,
],
});

View File

@ -0,0 +1,85 @@
// app/components/editor/codemirror/aiCompletions.ts
import { CompletionContext, CompletionResult, Completion } from '@codemirror/autocomplete';
import { language as languageFacet } from '@codemirror/language';
import { EditorState } from '@codemirror/state';
import type { AISuggestion, AISuggestionParams } from '~/lib/ai-assistant/types';
// Helper to get the current language using CodeMirror's language facet
function getActualCurrentLanguage(state: EditorState): string {
const langConfig = state.facet(languageFacet);
// The language facet might hold the Language instance directly, or an array of them.
// If it's an array, it's typically the first one that's active.
// The exact way to get the name might vary slightly based on CodeMirror version or specific language package structure.
if (Array.isArray(langConfig) && langConfig.length > 0) {
// @ts-expect-error - name might not be on Language type directly for all lang packages
return langConfig[0]?.name?.toLowerCase() || langConfig[0]?.constructor?.name?.toLowerCase() || 'plaintext';
} else if (langConfig && typeof langConfig === 'object') {
// @ts-expect-error - name might not be on Language type directly for all lang packages
return (langConfig as any).name?.toLowerCase() || (langConfig as any).constructor?.name?.toLowerCase() || 'plaintext';
}
return 'plaintext'; // Default fallback
}
export const aiCompletionSource = async (context: CompletionContext): Promise<CompletionResult | null> => {
// Determine the token or text before the cursor to decide if we should complete
// Example: complete if explicit, or if there's some text typed
const word = context.matchBefore(/\w*/); // Matches a word before the cursor
// Only trigger completions if explicitly requested, or if there's a word being typed,
// or if it's after a character that might solicit a completion (like '.')
// This logic can be refined.
const shouldTrigger = context.explicit || (word && word.from !== word.to) || context.state.doc.sliceString(context.pos - 1, context.pos) === '.';
if (!shouldTrigger) {
return null;
}
const from = word ? word.from : context.pos;
const codeBeforeCursor = context.state.doc.sliceString(0, context.pos);
const currentLanguage = getActualCurrentLanguage(context.state);
const params: AISuggestionParams = {
code: codeBeforeCursor, // Send code up to cursor
cursorPosition: context.pos,
language: currentLanguage,
task: 'complete',
// fileName: could be passed if available globally or via context
};
try {
const response = await fetch('/api/ai-assistant', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
});
if (!response.ok) {
console.error('AI Completion API error:', response.statusText);
return null;
}
const result = await response.json();
if (result.success && result.suggestions && result.suggestions.length > 0) {
const completions: Completion[] = result.suggestions.map((s: AISuggestion) => ({
label: s.code || '',
apply: s.code, // Can be a function for more complex application logic
type: s.type === 'completion' ? 'ai_completion' : s.type, // Custom type for styling
detail: s.title,
info: s.description, // `info` can render a DOM node or return a promise
boost: -1, // AI suggestions might be boosted or de-prioritized as needed
}));
return {
from: from, // Start of the text to be replaced by the completion
options: completions,
// validFor: /^\w*$/, // Example: allow further typing if it's still a word
};
}
} catch (error) {
console.error('Error fetching AI completions:', error);
return null;
}
return null;
};

View File

@ -0,0 +1,258 @@
// app/components/editor/codemirror/aiLintSource.ts
import { EditorView } from '@codemirror/view';
import { Diagnostic, setDiagnostics } from '@codemirror/lint';
import { EditorSelection, EditorState } from '@codemirror/state';
import { language as languageFacet } from '@codemirror/language';
import type { AISuggestion, AISuggestionParams } from '~/lib/ai-assistant/types';
// Helper to get the current language using CodeMirror's language facet
function getActualCurrentLanguage(state: EditorState): string {
const langConfig = state.facet(languageFacet);
if (Array.isArray(langConfig) && langConfig.length > 0) {
// @ts-expect-error - name might not be on Language type directly for all lang packages
return langConfig[0]?.name?.toLowerCase() || langConfig[0]?.constructor?.name?.toLowerCase() || 'plaintext';
} else if (langConfig && typeof langConfig === 'object') {
// @ts-expect-error - name might not be on Language type directly for all lang packages
return (langConfig as any).name?.toLowerCase() || (langConfig as any).constructor?.name?.toLowerCase() || 'plaintext';
}
return 'plaintext'; // Default fallback
}
// This function will be called by a command to fetch and return diagnostics
export async function fetchAIRefactorSuggestions(view: EditorView): Promise<readonly Diagnostic[]> {
const { state } = view;
const diagnostics: Diagnostic[] = [];
const currentLanguage = getActualCurrentLanguage(state);
// For suggestions, we usually operate on the current selection, or the whole document if no selection
let codeToAnalyze = '';
let selectionRange: { from: number; to: number } | undefined = undefined;
const mainSelection = state.selection.main;
if (!mainSelection.empty) {
codeToAnalyze = state.doc.sliceString(mainSelection.from, mainSelection.to);
selectionRange = { from: mainSelection.from, to: mainSelection.to };
} else {
// If no selection, consider sending the whole document or a relevant block
// For simplicity now, let's assume whole document if no selection,
// or this could be a user option / smarter context gathering later.
codeToAnalyze = state.doc.toString();
selectionRange = { from: 0, to: state.doc.length };
}
if (!codeToAnalyze.trim()) {
return []; // No code to analyze
}
const params: AISuggestionParams = {
code: codeToAnalyze,
selection: selectionRange, // Send the selection range if analysis is on selection
language: currentLanguage,
task: 'suggest_refactor',
// fileName: could be passed
};
try {
const response = await fetch('/api/ai-assistant', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
});
if (!response.ok) {
console.error('AI Refactor API error:', response.statusText);
return [];
}
const result = await response.json();
if (result.success && result.suggestions) {
result.suggestions.forEach((s: AISuggestion) => {
// Adjust 'from' and 'to' if the suggestion is relative to a snippet
// For now, assume 's.from' and 's.to' are document-level if selectionRange was for the whole doc,
// or relative to the start of the selection if a sub-part of selection is suggested.
// This needs careful handling based on how AI returns ranges.
// If AI returns ranges relative to the *snippet* sent, and we sent a selection,
// then s.from and s.to need to be offset by selectionRange.from.
let diagnosticFrom = s.from ?? mainSelection.from;
let diagnosticTo = s.to ?? mainSelection.to;
if (selectionRange && !mainSelection.empty && s.from != null && s.to != null) {
diagnosticFrom = selectionRange.from + s.from;
diagnosticTo = selectionRange.from + s.to;
}
diagnostics.push({
from: diagnosticFrom,
to: diagnosticTo,
severity: 'hint', // 'info' or 'hint' for suggestions
message: s.title || s.description || 'AI Suggestion',
source: 'AI Assistant',
actions: s.code // Only add action if there's code to apply
? [
{
name: `Apply: ${s.title || 'Apply suggestion'}`,
apply: (v: EditorView, fromApply: number, toApply: number) => {
// The 'from' and 'to' for apply are the diagnostic's range
v.dispatch({
changes: { from: fromApply, to: toApply, insert: s.code },
selection: EditorSelection.cursor(fromApply + (s.code?.length || 0)),
scrollIntoView: true,
});
},
},
]
: [],
});
});
}
} catch (error) {
console.error('Error fetching AI refactor suggestions:', error);
}
return diagnostics;
}
export const triggerAIRefactorCommand = (view: EditorView): boolean => {
const loadingDiagnostic: Diagnostic = {
from: view.state.selection.main.from,
to: view.state.selection.main.to,
severity: 'info',
message: 'AI Assistant: Analyzing for refactorings...',
source: 'AI Assistant',
};
view.dispatch(setDiagnostics(view.state, [loadingDiagnostic]));
fetchAIRefactorSuggestions(view)
.then(diagnostics => {
// Dispatch a transaction to update the diagnostics in the lint state field
// Ensure the lint extension is configured to pick these up.
// The setDiagnostics effect comes from @codemirror/lint
view.dispatch(setDiagnostics(view.state, diagnostics));
if (diagnostics.length > 0) {
// Optionally, open the lint panel if you have one and it's not open
// openLintPanel(view); // This command would need to be imported or available
console.log('AI Refactor suggestions loaded.');
} else {
console.log('No AI Refactor suggestions found.');
// Clear previous AI suggestions if any
view.dispatch(setDiagnostics(view.state, []));
}
})
.catch(error => {
console.error("Error triggering AI Refactor:", error);
// Clear diagnostics on error too
view.dispatch(setDiagnostics(view.state, []));
});
return true; // Command successfully initiated
};
export async function fetchAIBugFixSuggestions(view: EditorView): Promise<readonly Diagnostic[]> {
const { state } = view;
const diagnostics: Diagnostic[] = [];
const currentLanguage = getActualCurrentLanguage(state); // Use updated function
let codeToAnalyze = '';
let selectionRange: { from: number; to: number } | undefined = undefined;
const mainSelection = state.selection.main;
if (!mainSelection.empty) {
codeToAnalyze = state.doc.sliceString(mainSelection.from, mainSelection.to);
selectionRange = { from: mainSelection.from, to: mainSelection.to };
} else {
codeToAnalyze = state.doc.toString();
selectionRange = { from: 0, to: state.doc.length };
}
if (!codeToAnalyze.trim()) {
return [];
}
const params: AISuggestionParams = {
code: codeToAnalyze,
selection: selectionRange,
language: currentLanguage,
task: 'fix_bug',
// fileName: could be passed
};
try {
const response = await fetch('/api/ai-assistant', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
});
if (!response.ok) {
console.error('AI Bug Fix API error:', response.statusText);
return [];
}
const result = await response.json();
if (result.success && result.suggestions) {
result.suggestions.forEach((s: AISuggestion) => {
let diagnosticFrom = s.from ?? mainSelection.from;
let diagnosticTo = s.to ?? mainSelection.to;
if (selectionRange && !mainSelection.empty && s.from != null && s.to != null) {
diagnosticFrom = selectionRange.from + s.from;
diagnosticTo = selectionRange.from + s.to;
}
diagnostics.push({
from: diagnosticFrom,
to: diagnosticTo,
severity: 'warning', // Severity for bug fixes
message: s.title || s.description || 'AI Bug Fix Suggestion',
source: 'AI Assistant (Bug Fix)',
actions: s.code
? [
{
name: `Apply Fix: ${s.title || 'Accept fix'}`,
apply: (v: EditorView, fromApply: number, toApply: number) => {
v.dispatch({
changes: { from: fromApply, to: toApply, insert: s.code },
selection: EditorSelection.cursor(fromApply + (s.code?.length || 0)),
scrollIntoView: true,
});
},
},
]
: [],
});
});
}
} catch (error) {
console.error('Error fetching AI bug fix suggestions:', error);
}
return diagnostics;
}
export const triggerAIBugFixCommand = (view: EditorView): boolean => {
const loadingDiagnostic: Diagnostic = {
from: view.state.selection.main.from,
to: view.state.selection.main.to,
severity: 'info',
message: 'AI Assistant: Scanning for bugs...',
source: 'AI Assistant',
};
view.dispatch(setDiagnostics(view.state, [loadingDiagnostic]));
fetchAIBugFixSuggestions(view)
.then(diagnostics => {
view.dispatch(setDiagnostics(view.state, diagnostics));
if (diagnostics.length > 0) {
console.log('AI Bug Fix suggestions loaded.');
} else {
console.log('No AI Bug Fix suggestions found.');
// Optionally clear previous AI bug fix diagnostics if desired
// view.dispatch(setDiagnostics(view.state, [])); // Or filter to keep other types
}
})
.catch(error => {
console.error("Error triggering AI Bug Fix:", error);
view.dispatch(setDiagnostics(view.state, [])); // Clear on error
});
return true;
};

View File

@ -0,0 +1,89 @@
// app/lib/ai-assistant/aiAssistantService.server.ts
import type { AISuggestionParams, AISuggestionResponse } from './types';
// Placeholder for actual LLM call utilities, to be imported later
// import { getLlmCompletion } from '~/lib/.server/llm';
export async function getAISuggestions(
params: AISuggestionParams
): Promise<AISuggestionResponse> {
console.log('AI Assistant Service called with params:', params);
// TODO: Select LLM provider and model
// TODO: Construct prompt based on params.task
// TODO: Call LLM API
// TODO: Process LLM response
// Placeholder response for now
if (params.task === 'complete') {
// Simulate a simple completion
if (params.code.endsWith('.')) {
return {
success: true,
suggestions: [
{
id: 'compl-1',
type: 'completion',
code: 'log("hello world");',
description: 'console.log example',
},
{
id: 'compl-2',
type: 'completion',
code: 'dir(document);',
description: 'console.dir example',
}
],
};
}
} else if (params.task === 'suggest_refactor') {
return {
success: true,
suggestions: [
{
id: 'refactor-1',
type: 'refactor',
title: 'Use const instead of let',
description: 'If the variable is not reassigned, use const.',
code: 'const myVar = 10;', // Example suggested code
from: 0, // Example range
to: 10, // Example range
}
]
}
} else if (params.task === 'fix_bug') {
// Simulate a bug fix suggestion
if (params.code.includes('myVar = 10;')) { // Example condition
return {
success: true,
suggestions: [
{
id: 'fix-1',
type: 'fix',
title: 'Potential null access',
description: 'Variable `myVar` might be null here, causing a runtime error. Consider adding a check.',
code: 'if (myVar != null) {\n console.log(myVar);\n} else {\n console.log("myVar is null");\n}', // Example fixed code
from: params.code.indexOf('myVar = 10;'), // Placeholder, ideally AI gives specific range
to: params.code.indexOf('myVar = 10;') + 'myVar = 10;'.length, // Placeholder
}
]
};
}
return { // Default if no specific mock bug found
success: true,
suggestions: [{
id: 'fix-generic',
type: 'fix',
title: 'Generic Fix Example',
description: 'This is a generic bug fix suggestion.',
code: '// Fixed code example\n' + params.code.replace(/let/g, 'const'), // Simple replacement example
from: 0,
to: params.code.length,
}]
};
}
return {
success: false,
error: 'Task type not yet implemented or no suggestion found.',
};
}

View File

@ -0,0 +1,28 @@
// app/lib/ai-assistant/types.ts
export interface AISuggestionParams {
code: string;
cursorPosition?: number;
selection?: { from: number; to: number };
language: string; // e.g., 'javascript', 'python', 'typescript'
task: 'complete' | 'suggest_refactor' | 'fix_bug' | 'explain_code';
// Consider adding filename if available, can be useful context for LLM
fileName?: string;
}
export interface AISuggestion {
id: string; // Unique ID for the suggestion
type: 'completion' | 'refactor' | 'fix' | 'explanation';
title?: string;
description?: string;
code?: string; // The suggested code or completion
// For fixes/refactors, a diff might be useful in the future
// diff?: string;
from?: number; // Start of range to replace (if applicable)
to?: number; // End of range to replace (if applicable)
}
export interface AISuggestionResponse {
success: boolean;
suggestions?: AISuggestion[];
error?: string;
}

View File

@ -0,0 +1,23 @@
// app/routes/api.ai-assistant.ts
import { json, ActionFunctionArgs } from '@remix-run/node'; // or cloudflare/workers
import { getAISuggestions } from '~/lib/ai-assistant/aiAssistantService.server';
import type { AISuggestionParams } from '~/lib/ai-assistant/types';
export async function action({ request }: ActionFunctionArgs) {
if (request.method !== 'POST') {
return json({ success: false, error: 'Invalid request method' }, { status: 405 });
}
try {
const params = (await request.json()) as AISuggestionParams;
if (!params.code || !params.language || !params.task) {
return json({ success: false, error: 'Missing required parameters' }, { status: 400 });
}
const result = await getAISuggestions(params);
return json(result);
} catch (error: any) {
console.error('AI Assistant API Error:', error);
return json({ success: false, error: error.message || 'An unexpected error occurred' }, { status: 500 });
}
}