feat: quick link navigation

This commit is contained in:
Carl-Robert Linnupuu 2025-10-02 00:13:49 +01:00
parent eb6cd0ffe2
commit 6c3f19b131
31 changed files with 587 additions and 159 deletions

View file

@ -4,11 +4,14 @@ import static ee.carlrobert.codegpt.ui.UIUtil.createScrollPaneWithSmartScroller;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.application.ReadAction;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.SelectionModel;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.ui.JBColor;
import com.intellij.util.concurrency.AppExecutorUtil;
import com.intellij.util.ui.JBUI;
import ee.carlrobert.codegpt.CodeGPTKeys;
import ee.carlrobert.codegpt.ReferencedFile;
@ -178,6 +181,7 @@ public class ChatToolWindowTabPanel implements Disposable {
.referencedFiles(getReferencedFiles(selectedTags))
.history(getHistory(getSelectedTags()))
.psiStructure(psiStructure)
.project(project)
.chatMode(userInputPanel.getChatMode());
findTagOfType(selectedTags, PersonaTagDetails.class)
@ -286,8 +290,15 @@ public class ChatToolWindowTabPanel implements Disposable {
public void includeFiles(List<VirtualFile> referencedFiles) {
userInputPanel.includeFiles(referencedFiles);
totalTokensPanel.updateReferencedFilesTokens(
referencedFiles.stream().map(it -> ReferencedFile.from(it).fileContent()).toList());
ReadAction.nonBlocking(() ->
referencedFiles.stream()
.map(it -> ReferencedFile.from(it).fileContent())
.toList()
)
.inSmartMode(project)
.expireWith(project)
.finishOnUiThread(ModalityState.any(), totalTokensPanel::updateReferencedFilesTokens)
.submit(AppExecutorUtil.getAppExecutorService());
}
private boolean hasReferencedFilePaths(Message message) {
@ -498,6 +509,7 @@ public class ChatToolWindowTabPanel implements Disposable {
userMessagePanel.addReloadAction(() -> reloadMessage(
ChatCompletionParameters.builder(conversation, message)
.conversationType(ConversationType.DEFAULT)
.project(project)
.chatMode(userInputPanel.getChatMode())
.build(),
userMessagePanel));

View file

@ -13,17 +13,21 @@ import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.DefaultActionGroup;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.application.ReadAction;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.options.ShowSettingsUtil;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.VerticalFlowLayout;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.ui.AnimatedIcon;
import com.intellij.ui.PopupHandler;
import com.intellij.ui.components.JBLabel;
import com.intellij.util.concurrency.AppExecutorUtil;
import com.intellij.util.ui.JBUI;
import com.intellij.util.ui.components.BorderLayoutPanel;
import ee.carlrobert.codegpt.CodeGPTBundle;
@ -58,6 +62,7 @@ import ee.carlrobert.codegpt.toolwindow.ui.WebpageList;
import ee.carlrobert.codegpt.ui.ThoughtProcessPanel;
import ee.carlrobert.codegpt.ui.UIUtil;
import ee.carlrobert.codegpt.util.EditorUtil;
import ee.carlrobert.codegpt.util.MarkdownUtil;
import java.awt.BorderLayout;
import java.util.Objects;
import java.util.stream.Stream;

View file

@ -3,16 +3,20 @@ package ee.carlrobert.codegpt.ui;
import static javax.swing.event.HyperlinkEvent.EventType.ACTIVATED;
import com.intellij.ide.BrowserUtil;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.roots.ui.componentsList.components.ScrollablePanel;
import com.intellij.openapi.ui.panel.ComponentPanelBuilder;
import com.intellij.ui.JBColor;
import com.intellij.ui.ScrollPaneFactory;
import com.intellij.ui.components.JBRadioButton;
import com.intellij.ui.components.JBTextArea;
import com.intellij.util.ui.HTMLEditorKitBuilder;
import com.intellij.util.ui.JBUI;
import com.intellij.util.ui.UI;
import ee.carlrobert.codegpt.CodeGPTBundle;
import ee.carlrobert.codegpt.toolwindow.chat.ui.SmartScroller;
import ee.carlrobert.codegpt.util.PsiLinkNavigator;
import java.awt.BorderLayout;
import java.awt.CardLayout;
import java.awt.Component;
@ -43,6 +47,8 @@ import javax.swing.text.DefaultCaret;
public class UIUtil {
private static final Logger LOG = Logger.getInstance(UIUtil.class);
public static JTextPane createTextPane(String text) {
return createTextPane(text, true);
}
@ -54,6 +60,7 @@ public class UIUtil {
public static JTextPane createTextPane(String text, boolean opaque, HyperlinkListener listener) {
var textPane = new JTextPane();
textPane.putClientProperty(JTextPane.HONOR_DISPLAY_PROPERTIES, true);
textPane.setEditorKit(HTMLEditorKitBuilder.simple());
textPane.addHyperlinkListener(listener);
textPane.setContentType("text/html");
textPane.setEditable(false);
@ -103,12 +110,23 @@ public class UIUtil {
}
public static void handleHyperlinkClicked(HyperlinkEvent event) {
if (!ACTIVATED.equals(event.getEventType())) {
return;
}
String desc = event.getDescription();
if (desc != null && PsiLinkNavigator.isValidNavigationLink(desc)) {
ApplicationManager.getApplication()
.executeOnPooledThread(() -> PsiLinkNavigator.handle(desc));
return;
}
var url = event.getURL();
if (ACTIVATED.equals(event.getEventType()) && url != null) {
if (url != null) {
try {
BrowserUtil.browse(url.toURI());
} catch (URISyntaxException e) {
throw new RuntimeException(e);
LOG.warn("Failed to browse URL: " + url, e);
}
}
}
@ -119,7 +137,6 @@ public class UIUtil {
textArea.getActionMap().put("text-submit", onSubmit);
}
public static JPanel createRadioButtonsPanel(List<JBRadioButton> radioButtons) {
var buttonGroup = new ButtonGroup();
var radioPanel = new JPanel();

View file

@ -116,19 +116,20 @@ class CodeCompletionEventListener(
}
if (firstLineSent.get() && firstLine != null) {
val remainingContent = finalResult.removePrefix(firstLine!!).toString()
val first = firstLine ?: return
val remainingContent = finalResult.removePrefix(first).toString()
if (remainingContent.trim().isEmpty()) {
return
}
val parsedContent = parseOutput(firstLine + remainingContent)
val parsedContent = parseOutput(first + remainingContent)
if (parsedContent.isNotEmpty()) {
cache?.setCache(prefix, suffix, parsedContent)
CodeGPTKeys.REMAINING_CODE_COMPLETION.set(
editor,
PartialCodeCompletionResponse.newBuilder()
.setPartialCompletion(parsedContent.removePrefix(firstLine ?: ""))
.setPartialCompletion(parsedContent.removePrefix(first))
.build()
)
}
@ -200,4 +201,4 @@ class CodeCompletionEventListener(
.parse(prefix, suffix, input)
.trimEnd()
}
}
}

View file

@ -1,5 +1,6 @@
package ee.carlrobert.codegpt.completions
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import ee.carlrobert.codegpt.ReferencedFile
import ee.carlrobert.codegpt.conversations.Conversation
@ -26,6 +27,7 @@ class ChatCompletionParameters private constructor(
var referencedFiles: List<ReferencedFile>?,
var personaDetails: PersonaDetails?,
var psiStructure: Set<ClassStructure>?,
var project: Project?,
var chatMode: ChatMode = ChatMode.ASK,
var featureType: FeatureType = FeatureType.CHAT
) : CompletionParameters {
@ -39,6 +41,7 @@ class ChatCompletionParameters private constructor(
referencedFiles(this@ChatCompletionParameters.referencedFiles)
personaDetails(this@ChatCompletionParameters.personaDetails)
psiStructure(this@ChatCompletionParameters.psiStructure)
project(this@ChatCompletionParameters.project)
chatMode(this@ChatCompletionParameters.chatMode)
featureType(this@ChatCompletionParameters.featureType)
}
@ -54,6 +57,7 @@ class ChatCompletionParameters private constructor(
private var personaDetails: PersonaDetails? = null
private var psiStructure: Set<ClassStructure>? = null
private var gitDiff: String = ""
private var project: Project? = null
private var chatMode: ChatMode = ChatMode.ASK
private var featureType: FeatureType = FeatureType.CHAT
@ -83,6 +87,8 @@ class ChatCompletionParameters private constructor(
fun psiStructure(psiStructure: Set<ClassStructure>?) = apply { this.psiStructure = psiStructure }
fun project(project: Project?) = apply { this.project = project }
fun chatMode(chatMode: ChatMode) = apply { this.chatMode = chatMode }
fun featureType(featureType: FeatureType) = apply { this.featureType = featureType }
@ -99,6 +105,7 @@ class ChatCompletionParameters private constructor(
referencedFiles,
personaDetails,
psiStructure,
project,
chatMode,
featureType
)

View file

@ -43,7 +43,7 @@ object CompletionRequestUtil {
fun getPromptWithContext(
referencedFiles: List<ReferencedFile>,
userPrompt: String?,
psiStructure: Set<ClassStructure>?
psiStructure: Set<ClassStructure>?,
): String {
val includedFilesSettings = service<IncludedFilesSettings>().state
val repeatableContext = includedFilesSettings.repeatableContext
@ -77,7 +77,10 @@ object CompletionRequestUtil {
}
return includedFilesSettings.promptTemplate
.replace("{REPEATABLE_CONTEXT}", fileContext + structureContext.orEmpty())
.replace("{REPEATABLE_CONTEXT}", buildString {
append(fileContext)
append(structureContext.orEmpty())
})
.replace("{QUESTION}", userPrompt!!)
}
}
}

View file

@ -4,7 +4,6 @@ import com.intellij.openapi.components.service
import ee.carlrobert.codegpt.completions.BaseRequestFactory
import ee.carlrobert.codegpt.completions.ChatCompletionParameters
import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings
import ee.carlrobert.codegpt.settings.models.ModelSettings
import ee.carlrobert.codegpt.settings.prompts.FilteredPromptsService
import ee.carlrobert.codegpt.settings.prompts.PromptsSettings
import ee.carlrobert.codegpt.settings.service.FeatureType
@ -22,7 +21,8 @@ class ClaudeRequestFactory : BaseRequestFactory() {
val selectedPersona = service<PromptsSettings>().state.personas.selectedPersona
if (!selectedPersona.disabled) {
system = service<FilteredPromptsService>().getFilteredPersonaPrompt(params.chatMode)
val base = service<FilteredPromptsService>().getFilteredPersonaPrompt(params.chatMode)
system = service<FilteredPromptsService>().applyClickableLinks(base)
}
messages = params.conversation.messages

View file

@ -10,10 +10,8 @@ import ee.carlrobert.codegpt.conversations.ConversationsState
import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings
import ee.carlrobert.codegpt.settings.prompts.FilteredPromptsService
import ee.carlrobert.codegpt.settings.prompts.PromptsSettings
import ee.carlrobert.codegpt.settings.service.google.GoogleSettings
import ee.carlrobert.codegpt.settings.service.FeatureType
import ee.carlrobert.codegpt.settings.service.ModelSelectionService
import ee.carlrobert.codegpt.settings.models.ModelSettings
import ee.carlrobert.codegpt.util.file.FileUtil
import ee.carlrobert.llm.client.google.completion.GoogleCompletionContent
import ee.carlrobert.llm.client.google.completion.GoogleCompletionRequest
@ -130,15 +128,16 @@ class GoogleRequestFactory : BaseRequestFactory() {
messages.add(GoogleCompletionContent("model", listOf(prevMessage.response)))
}
if (params.imageDetails != null) {
val imageDetails = params.imageDetails
if (imageDetails != null) {
messages.add(
GoogleCompletionContent(
listOf(
GoogleContentPart(
null,
GoogleContentPart.Blob(
params.imageDetails!!.mediaType,
params.imageDetails!!.data
imageDetails.mediaType,
imageDetails.data
)
),
GoogleContentPart(message.prompt)
@ -192,16 +191,17 @@ class GoogleRequestFactory : BaseRequestFactory() {
return when (params.conversationType) {
ConversationType.DEFAULT -> {
val selectedPersona = service<PromptsSettings>().state.personas.selectedPersona
return if (!selectedPersona.disabled) {
service<FilteredPromptsService>().getFilteredPersonaPrompt(params.chatMode)
} else {
null
}
if (!selectedPersona.disabled) {
val base = service<FilteredPromptsService>().getFilteredPersonaPrompt(params.chatMode)
service<FilteredPromptsService>().applyClickableLinks(base)
} else null
}
ConversationType.FIX_COMPILE_ERRORS -> service<PromptsSettings>().state.coreActions.fixCompileErrors.instructions
ConversationType.FIX_COMPILE_ERRORS -> service<FilteredPromptsService>()
.applyClickableLinks(service<PromptsSettings>().state.coreActions.fixCompileErrors.instructions.orEmpty())
ConversationType.REVIEW_CHANGES -> service<PromptsSettings>().state.coreActions.reviewChanges.instructions
ConversationType.REVIEW_CHANGES -> service<FilteredPromptsService>()
.applyClickableLinks(service<PromptsSettings>().state.coreActions.reviewChanges.instructions.orEmpty())
else -> null
}

View file

@ -13,14 +13,13 @@ import ee.carlrobert.codegpt.settings.prompts.PromptsSettings
import ee.carlrobert.codegpt.settings.prompts.addProjectPath
import ee.carlrobert.codegpt.settings.service.FeatureType
import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings
import ee.carlrobert.codegpt.conversations.message.Message
import ee.carlrobert.llm.client.llama.completion.LlamaCompletionRequest
class LlamaRequestFactory : BaseRequestFactory() {
override fun createChatRequest(params: ChatCompletionParameters): LlamaCompletionRequest {
val promptTemplate = getPromptTemplate()
val systemPrompt =
var systemPrompt =
if (params.conversationType == ConversationType.FIX_COMPILE_ERRORS) {
service<PromptsSettings>().state.coreActions.fixCompileErrors.instructions
} else {
@ -30,6 +29,7 @@ class LlamaRequestFactory : BaseRequestFactory() {
).addProjectPath()
}
}
systemPrompt = systemPrompt?.let { service<FilteredPromptsService>().applyClickableLinks(it) }
val prompt = promptTemplate.buildPrompt(
systemPrompt,

View file

@ -253,9 +253,12 @@ class OpenAIRequestFactory : BaseRequestFactory() {
val selectedPersona = service<PromptsSettings>().state.personas.selectedPersona
if (callParameters.conversationType == ConversationType.DEFAULT && !selectedPersona.disabled) {
val sessionPersonaDetails = callParameters.personaDetails
val instructions = sessionPersonaDetails?.instructions?.addProjectPath()
?: service<FilteredPromptsService>().getFilteredPersonaPrompt(callParameters.chatMode)
val baseInstructions = sessionPersonaDetails?.instructions?.addProjectPath()
?: service<FilteredPromptsService>()
.getFilteredPersonaPrompt(callParameters.chatMode)
.addProjectPath()
val instructions = service<FilteredPromptsService>()
.applyClickableLinks(baseInstructions)
val history = if (conversationsHistory.isNullOrEmpty()) {
""
} else {
@ -265,29 +268,22 @@ class OpenAIRequestFactory : BaseRequestFactory() {
}
if (instructions.isNotEmpty()) {
val content = if (history.isBlank()) instructions else instructions.trimEnd() + "\n" + history
messages.add(
OpenAIChatCompletionStandardMessage(
role,
instructions + "\n" + history
content
)
)
}
}
if (callParameters.conversationType == ConversationType.REVIEW_CHANGES) {
messages.add(
OpenAIChatCompletionStandardMessage(
role,
service<PromptsSettings>().state.coreActions.reviewChanges.instructions
)
)
val base = service<PromptsSettings>().state.coreActions.reviewChanges.instructions
messages.add(OpenAIChatCompletionStandardMessage(role, base))
}
if (callParameters.conversationType == ConversationType.FIX_COMPILE_ERRORS) {
messages.add(
OpenAIChatCompletionStandardMessage(
role,
service<PromptsSettings>().state.coreActions.fixCompileErrors.instructions
)
)
val base = service<PromptsSettings>().state.coreActions.fixCompileErrors.instructions
messages.add(OpenAIChatCompletionStandardMessage(role, base))
}
for (prevMessage in callParameters.conversation.messages) {
@ -333,15 +329,16 @@ class OpenAIRequestFactory : BaseRequestFactory() {
)
}
if (callParameters.imageDetails != null) {
val imageDetails = callParameters.imageDetails
if (imageDetails != null) {
messages.add(
OpenAIChatCompletionDetailedMessage(
"user",
listOf(
OpenAIMessageImageURLContent(
OpenAIImageUrl(
callParameters.imageDetails!!.mediaType,
callParameters.imageDetails!!.data
imageDetails.mediaType,
imageDetails.data
)
),
OpenAIMessageTextContent(message.prompt)
@ -355,7 +352,7 @@ class OpenAIRequestFactory : BaseRequestFactory() {
CompletionRequestUtil.getPromptWithContext(
referencedFiles,
message.prompt,
psiStructure
psiStructure,
)
}
messages.add(OpenAIChatCompletionStandardMessage("user", prompt))

View file

@ -66,12 +66,12 @@ class InlineEditInlay(private var editor: Editor) : Disposable {
private var isUpdatingInlaySize = false
private var resizeTimer: Timer? = null
private val project = editor.project!!
private val project = requireNotNull(editor.project) { "Editor project is null" }
private var inlayDisposable: Disposable? = null
private val psiStructureRepository = PsiStructureRepository(
this,
editor.project!!,
project,
tagManager,
PsiStructureProvider(),
CoroutineDispatchers()
@ -85,7 +85,7 @@ class InlineEditInlay(private var editor: Editor) : Disposable {
)
private val userInputPanel = UserInputPanel(
project = editor.project!!,
project = project,
totalTokensPanel = dummyTokensPanel,
parentDisposable = this,
featureType = FeatureType.INLINE_EDIT,
@ -497,7 +497,7 @@ class InlineEditInlay(private var editor: Editor) : Disposable {
try {
val refs = collectSelectedReferencedFiles()
val diff = try {
GitUtil.getCurrentChanges(editor.project!!)
GitUtil.getCurrentChanges(project)
} catch (_: Exception) {
null
}

View file

@ -241,7 +241,7 @@ class KotlinFileAnalyzer(
val parameters = constructor.valueParameters.map { parameter ->
val type = parameter.typeReference?.text ?: "TypeUnknown"
val resolvedType = resolveType(type)
ParameterInfo(parameter.name!!, resolvedType, getModifiers(parameter))
ParameterInfo(parameter.name.orEmpty(), resolvedType, getModifiers(parameter))
}
val modifierList = getModifiers(constructor)
return ConstructorStructure(parameters, modifierList)
@ -260,10 +260,10 @@ class KotlinFileAnalyzer(
val parameters = function.valueParameters.map { parameter ->
val type = parameter.typeReference?.text ?: "TypeUnknown"
val resolvedType = resolveType(type)
ParameterInfo(parameter.name!!, resolvedType, getModifiers(parameter))
ParameterInfo(parameter.name.orEmpty(), resolvedType, getModifiers(parameter))
}
val modifierList = getModifiers(function)
return MethodStructure(function.name!!, resolvedReturnType, parameters, modifierList)
return MethodStructure(function.name.orEmpty(), resolvedReturnType, parameters, modifierList)
}
private fun resolveType(shortType: String): ClassName {
@ -327,4 +327,4 @@ class KotlinFileAnalyzer(
psiFileQueue.put(psiFile, ktFile.name)
}
}
}
}

View file

@ -4,8 +4,6 @@ import com.intellij.openapi.components.service
import com.intellij.openapi.ui.DialogPanel
import com.intellij.ui.PortField
import com.intellij.ui.components.JBCheckBox
import com.intellij.ui.components.JBTextField
import com.intellij.ui.components.fields.IntegerField
import com.intellij.ui.dsl.builder.panel
import com.intellij.util.ui.JBUI
import ee.carlrobert.codegpt.CodeGPTBundle
@ -26,8 +24,17 @@ class ChatCompletionConfigurationForm {
number = service<ConfigurationSettings>().state.chatCompletionSettings.psiStructureAnalyzeDepth
}
private val clickableLinksCheckBox = JBCheckBox(
CodeGPTBundle.get("configurationConfigurable.section.chatCompletion.clickableLinks.title"),
service<ConfigurationSettings>().state.chatCompletionSettings.clickableLinksEnabled
)
fun createPanel(): DialogPanel {
return panel {
row {
cell(clickableLinksCheckBox)
.comment(CodeGPTBundle.get("configurationConfigurable.section.chatCompletion.clickableLinks.description"))
}
row {
cell(editorContextTagCheckBox)
.comment(CodeGPTBundle.get("configurationConfigurable.section.chatCompletion.editorContextTag.description"))
@ -50,6 +57,7 @@ class ChatCompletionConfigurationForm {
editorContextTagCheckBox.isSelected = prevState.editorContextTagEnabled
psiStructureCheckBox.isSelected = prevState.psiStructureEnabled
psiStructureAnalyzeDepthField.number = prevState.psiStructureAnalyzeDepth
clickableLinksCheckBox.isSelected = prevState.clickableLinksEnabled
}
fun getFormState(): ChatCompletionSettingsState {
@ -57,6 +65,7 @@ class ChatCompletionConfigurationForm {
this.editorContextTagEnabled = editorContextTagCheckBox.isSelected
this.psiStructureEnabled = psiStructureCheckBox.isSelected
this.psiStructureAnalyzeDepth = psiStructureAnalyzeDepthField.number
this.clickableLinksEnabled = clickableLinksCheckBox.isSelected
}
}
}
}

View file

@ -49,6 +49,7 @@ class ChatCompletionSettingsState : BaseState() {
var editorContextTagEnabled by property(true)
var psiStructureEnabled by property(true)
var psiStructureAnalyzeDepth by property(3)
var clickableLinksEnabled by property(true)
}
class CodeCompletionSettingsState : BaseState() {
@ -57,4 +58,4 @@ class CodeCompletionSettingsState : BaseState() {
var collectDependencyStructure by property(true)
var contextAwareEnabled by property(false)
var psiStructureAnalyzeDepth by property(2)
}
}

View file

@ -4,6 +4,7 @@ import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.vfs.VirtualFile
import ee.carlrobert.codegpt.settings.configuration.ChatMode
import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings
import ee.carlrobert.codegpt.util.file.FileUtil.getResourceContent
/**
@ -25,17 +26,15 @@ class FilteredPromptsService {
fun getFilteredPersonaPrompt(chatMode: ChatMode): String {
val selectedPersona = service<PromptsSettings>().state.personas.selectedPersona
return when (chatMode) {
ChatMode.EDIT -> DEFAULT_PERSONA_EDIT_MODE_PROMPT
ChatMode.ASK -> {
if (isDefaultPersona(selectedPersona)) {
PersonasState.DEFAULT_PERSONA_PROMPT
} else {
val originalPrompt = getOriginalPersonaPrompt()
filterOutSearchReplaceInstructions(originalPrompt)
}
val originalPrompt = getOriginalPersonaPrompt()
if (isDefaultPersona(selectedPersona)) originalPrompt
else filterOutSearchReplaceInstructions(originalPrompt)
}
ChatMode.AGENT -> PersonasState.DEFAULT_PERSONA_PROMPT
}
}
@ -47,6 +46,14 @@ class FilteredPromptsService {
ChatMode.AGENT -> getSimpleEditCodePrompt()
}
fun applyClickableLinks(prompt: String): String {
val enabled =
service<ConfigurationSettings>().state.chatCompletionSettings.clickableLinksEnabled
if (!enabled) return prompt
return prompt + "\n" + PSI_LINKS_GUIDELINES
}
private fun isDefaultPersona(persona: PersonaPromptDetailsState) =
persona.id == DEFAULT_PERSONA_ID
@ -60,7 +67,7 @@ class FilteredPromptsService {
private fun getOriginalPersonaPrompt(): String {
val selectedPersona = service<PromptsSettings>().state.personas.selectedPersona
return selectedPersona.instructions ?: when {
isDefaultPersona(selectedPersona) -> PersonasState.DEFAULT_PERSONA_PROMPT
else -> ""
@ -101,6 +108,9 @@ class FilteredPromptsService {
val DEFAULT_PERSONA_EDIT_MODE_PROMPT = getResourceContent(EDIT_MODE_PROMPT_RESOURCE)
private val PSI_LINKS_GUIDELINES =
getResourceContent("/prompts/persona/psi-navigation-guidelines.txt")
private val SEARCH_REPLACE_BLOCKS_REGEX = Regex(
"When generating SEARCH/REPLACE blocks:.*?Keep SEARCH blocks concise while including necessary surrounding lines\\.",
RegexOption.DOT_MATCHES_ALL
@ -144,4 +154,4 @@ class FilteredPromptsService {
CALCULATOR_EXAMPLE_REGEX to CALCULATOR_EXAMPLE_REPLACEMENT
)
}
}
}

View file

@ -113,13 +113,8 @@ class OllamaSettingsForm {
.addComponentFillVertically(JPanel(), 0)
.panel
fun getModel(featureType: FeatureType): String? {
return if (modelComboBoxes[featureType]!!.isEnabled) {
modelComboBoxes[featureType]!!.item
} else {
null
}
}
fun getModel(featureType: FeatureType): String? =
modelComboBoxes[featureType]?.let { combo -> if (combo.isEnabled) combo.item else null }
fun getApiKey(): String? = String(apiKeyField.password).ifEmpty { null }
@ -137,11 +132,11 @@ class OllamaSettingsForm {
fun applyChanges() {
service<OllamaSettings>().state.run {
host = hostField.text
if (modelComboBoxes[FeatureType.CHAT]!!.isEnabled)
model = modelComboBoxes[FeatureType.CHAT]!!.item
if (modelComboBoxes[FeatureType.CHAT]?.isEnabled == true)
model = modelComboBoxes[FeatureType.CHAT]?.item
codeCompletionsEnabled = codeCompletionConfigurationForm.isCodeCompletionsEnabled
fimTemplate = codeCompletionConfigurationForm.fimTemplate!!
fimOverride = codeCompletionConfigurationForm.fimOverride ?: false
fimTemplate = codeCompletionConfigurationForm.fimTemplate ?: fimTemplate
fimOverride = codeCompletionConfigurationForm.fimOverride == true
}
setCredential(OllamaApikey, getApiKey())
}

View file

@ -52,7 +52,7 @@ class ErrorPopoverHandler(
private fun showErrorPopoverWithHover() {
if (errorContent == null) return
if (errorPopup != null && errorPopup!!.isVisible) return
if (errorPopup?.isVisible == true) return
val documentationHint = DocumentationHintEditorPane(
project,
@ -96,4 +96,4 @@ class ErrorPopoverHandler(
errorPopup = popup
popup.showUnderneathOf(errorLabel)
}
}
}

View file

@ -198,9 +198,9 @@ class ChatHistoryToolWindow(private val project: Project) : BorderLayoutPanel()
lastFilteredConversations = null
projectFiltered
} else if (searchText == lastSearchText && lastFilteredConversations != null) {
lastFilteredConversations!!.filter { conversation ->
lastFilteredConversations?.filter { conversation ->
projectFiltered.contains(conversation)
}
}.orEmpty()
} else {
val startList = getOptimizedSearchStartList(searchText).filter { conversation ->
projectFiltered.contains(conversation)
@ -215,7 +215,7 @@ class ChatHistoryToolWindow(private val project: Project) : BorderLayoutPanel()
private fun getOptimizedSearchStartList(searchText: String): List<Conversation> {
return if (lastSearchText.isNotEmpty() && searchText.startsWith(lastSearchText) && lastFilteredConversations != null) {
lastFilteredConversations!!
lastFilteredConversations.orEmpty()
} else {
allConversations
}
@ -601,4 +601,4 @@ class ChatHistoryToolWindow(private val project: Project) : BorderLayoutPanel()
refresh()
}
}
}
}

View file

@ -5,6 +5,8 @@ import com.intellij.openapi.Disposable
import com.intellij.openapi.actionSystem.ActionPlaces
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.application.ReadAction
import com.intellij.openapi.application.invokeLater
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.components.service
@ -20,6 +22,7 @@ import com.intellij.ui.dsl.builder.AlignX
import com.intellij.ui.dsl.builder.RightGap
import com.intellij.ui.dsl.builder.panel
import com.intellij.util.IconUtil
import com.intellij.util.concurrency.AppExecutorUtil
import com.intellij.util.ui.AsyncProcessIcon
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.components.BorderLayoutPanel
@ -105,9 +108,16 @@ class UserInputPanel @JvmOverloads constructor(
onSubmit = ::handleSubmit,
onFilesDropped = { files ->
includeFiles(files.toMutableList())
totalTokensPanel.updateReferencedFilesTokens(files.map {
ReferencedFile.from(it).fileContent()
})
ReadAction
.nonBlocking<List<String>> {
files.map { ReferencedFile.from(it).fileContent() }
}
.inSmartMode(project)
.expireWith(project)
.finishOnUiThread(ModalityState.any()) { contents ->
totalTokensPanel.updateReferencedFilesTokens(contents)
}
.submit(AppExecutorUtil.getAppExecutorService())
},
featureType = featureType
)
@ -192,9 +202,16 @@ class UserInputPanel @JvmOverloads constructor(
}
FileDragAndDrop.install(this) { files ->
includeFiles(files.toMutableList())
totalTokensPanel.updateReferencedFilesTokens(
files.map { ReferencedFile.from(it).fileContent() }
)
ReadAction
.nonBlocking<List<String>> {
files.map { ReferencedFile.from(it).fileContent() }
}
.inSmartMode(project)
.expireWith(project)
.finishOnUiThread(ModalityState.any()) { contents ->
totalTokensPanel.updateReferencedFilesTokens(contents)
}
.submit(AppExecutorUtil.getAppExecutorService())
}
}

View file

@ -7,40 +7,42 @@ import ee.carlrobert.codegpt.toolwindow.chat.ResponseNodeRenderer
import java.util.regex.Pattern
object MarkdownUtil {
/**
* Splits a given string into a list of strings where each element is either a code block
* surrounded by triple backticks or a non-code block text.
*
* @param inputMarkdown The input markdown formatted string to be split.
* @return A list of strings where each element is a code block or a non-code block text from the
* input string.
*/
@JvmStatic
fun splitCodeBlocks(inputMarkdown: String): List<String> {
val result: MutableList<String> = ArrayList()
val pattern = Pattern.compile(
"""(?m)^```[a-zA-Z0-9]*\r?\n.*?\r?\n```""",
Pattern.DOTALL
)
val matcher = pattern.matcher(inputMarkdown)
var start = 0
while (matcher.find()) {
result.add(inputMarkdown.substring(start, matcher.start()))
result.add(matcher.group())
start = matcher.end()
}
result.add(inputMarkdown.substring(start))
return result.stream().filter(String::isNotBlank).toList()
}
@JvmStatic
fun convertMdToHtml(message: String): String {
val options = MutableDataSet()
options.set(HtmlRenderer.SOFT_BREAK, "<br/>")
val document = Parser.builder(options).build().parse(message)
return HtmlRenderer.builder(options)
.nodeRendererFactory(ResponseNodeRenderer.Factory())
.build()
.render(document)
}
/**
* Splits a given string into a list of strings where each element is either a code block
* surrounded by triple backticks or a non-code block text.
*
* @param inputMarkdown The input markdown formatted string to be split.
* @return A list of strings where each element is a code block or a non-code block text from the
* input string.
*/
@JvmStatic
fun splitCodeBlocks(inputMarkdown: String): List<String> {
val result: MutableList<String> = ArrayList()
val pattern = Pattern.compile(
"""(?m)^```[a-zA-Z0-9]*\r?\n.*?\r?\n```""",
Pattern.DOTALL
)
val matcher = pattern.matcher(inputMarkdown)
var start = 0
while (matcher.find()) {
result.add(inputMarkdown.substring(start, matcher.start()))
result.add(matcher.group())
start = matcher.end()
}
result.add(inputMarkdown.substring(start))
return result.stream().filter(String::isNotBlank).toList()
}
@JvmStatic
fun convertMdToHtml(message: String): String {
val options = MutableDataSet()
options.set(HtmlRenderer.SOFT_BREAK, "<br/>")
val document = Parser.builder(options).build().parse(message)
return HtmlRenderer.builder(options)
.nodeRendererFactory(ResponseNodeRenderer.Factory())
.build()
.render(document)
}
}

View file

@ -0,0 +1,250 @@
package ee.carlrobert.codegpt.util
import com.intellij.ide.util.gotoByName.GotoClassModel2
import com.intellij.ide.util.gotoByName.GotoFileModel
import com.intellij.ide.util.gotoByName.GotoSymbolModel2
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.application.ReadAction
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.project.Project
import com.intellij.pom.Navigatable
import com.intellij.psi.JavaPsiFacade
import com.intellij.psi.PsiClass
import com.intellij.psi.PsiManager
import com.intellij.psi.PsiNamedElement
import com.intellij.psi.search.FilenameIndex
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.util.concurrency.AppExecutorUtil
import java.net.URLDecoder
import java.nio.charset.StandardCharsets
object PsiLinkNavigator {
private val logger = thisLogger()
private const val PSI_ELEMENT_PREFIX = "psi_element://"
private const val FILE_PREFIX = "file://"
private val SUPPORTED_PROTOCOLS = listOf(PSI_ELEMENT_PREFIX, FILE_PREFIX)
@JvmStatic
fun handle(description: String): Boolean {
if (!isValidNavigationLink(description)) {
return false
}
val protocol = extractProtocol(description)
val target = extractTarget(description)
return try {
ReadAction.nonBlocking<Navigatable?> {
val resolver = NavigationResolverFactory.create(protocol)
resolver.resolve(target)
}
.finishOnUiThread(ModalityState.nonModal()) { navigatable ->
navigatable?.let { navigate(it) }
}
.submit(AppExecutorUtil.getAppExecutorService())
true
} catch (t: Throwable) {
logger.warn("Failed to schedule navigation for: $target", t)
false
}
}
@JvmStatic
fun isValidNavigationLink(description: String?): Boolean {
if (description.isNullOrBlank()) return false
return SUPPORTED_PROTOCOLS.any { description.startsWith(it) }
}
private fun extractProtocol(description: String): String {
return SUPPORTED_PROTOCOLS.first { description.startsWith(it) }
}
private fun extractTarget(description: String): String {
val protocol = extractProtocol(description)
val raw = description.removePrefix(protocol)
return decode(raw)
}
private fun decode(value: String): String = try {
URLDecoder.decode(value, StandardCharsets.UTF_8)
} catch (e: Exception) {
logger.error("Failed to decode URL: $value", e)
value
}
private fun navigate(target: Navigatable) {
if (target.canNavigate()) {
target.navigate(true)
logger.info("Successfully navigated to: $target")
} else {
logger.info("Target cannot navigate: $target")
}
}
}
abstract class NavigationResolver {
abstract fun resolve(target: String): Navigatable?
protected val logger = thisLogger()
protected fun getProject(): Project? = ApplicationUtil.findCurrentProject()
}
class PsiElementResolver : NavigationResolver() {
override fun resolve(target: String): Navigatable? {
val project = getProject() ?: return null
logger.info("Resolving PSI element: $target")
findByJavaFQN(project, target)?.let { return it }
findByMemberSeparation(project, target)?.let { return it }
return searchInModels(project, target)
}
private fun findByJavaFQN(project: Project, target: String): Navigatable? {
try {
val memberSeparatorIndex = target.indexOf('#')
val className = if (memberSeparatorIndex > 0) {
target.substring(0, memberSeparatorIndex)
} else {
target
}
val javaPsiFacade = JavaPsiFacade.getInstance(project)
val projectScope = GlobalSearchScope.projectScope(project)
val allScope = GlobalSearchScope.allScope(project)
val psiClass = javaPsiFacade.findClass(className, projectScope)
?: javaPsiFacade.findClass(className, allScope)
if (psiClass != null) {
val memberName = if (memberSeparatorIndex > 0) {
target.substring(memberSeparatorIndex + 1)
} else {
null
}
if (memberName != null) {
findMemberInClass(psiClass, memberName)?.let { return it }
}
return psiClass
}
if (className.contains('$')) {
val innerClassFQN = className.replace('$', '.')
val innerClass = javaPsiFacade.findClass(innerClassFQN, projectScope)
?: javaPsiFacade.findClass(innerClassFQN, allScope)
if (innerClass != null) {
return innerClass
}
}
return null
} catch (t: Throwable) {
logger.warn("Failed to resolve Java FQN: $target", t)
return null
}
}
private fun findByMemberSeparation(project: Project, target: String): Navigatable? {
val memberSeparatorIndex = target.indexOf('#')
if (memberSeparatorIndex <= 0) return null
val owner = target.substring(0, memberSeparatorIndex)
val member = target.substring(memberSeparatorIndex + 1)
searchInModels(project, owner)?.let { ownerElement ->
if (ownerElement is PsiClass) {
findMemberInClass(ownerElement, member)?.let { return it }
}
}
return null
}
private fun findMemberInClass(psiClass: PsiClass, memberName: String): Navigatable? {
psiClass.findMethodsByName(memberName, false).firstOrNull()?.let { return it }
psiClass.findFieldByName(memberName, false)?.let { return it }
psiClass.findInnerClassByName(memberName, false)?.let { return it }
return null
}
private fun searchInModels(project: Project, searchTerm: String): Navigatable? {
try {
val classModel = GotoClassModel2(project)
classModel.getElementsByName(searchTerm, true, searchTerm)
.filterIsInstance<Navigatable>()
.firstOrNull { it.canNavigate() }
?.let { return it }
val symbolModel = GotoSymbolModel2(project, project)
symbolModel.getElementsByName(searchTerm, true, searchTerm)
.filterIsInstance<Navigatable>()
.firstOrNull { it.canNavigate() }
?.let { return it }
} catch (e: Exception) {
logger.warn("Search failed for term: $searchTerm", e)
}
return null
}
}
class FileResolver : NavigationResolver() {
override fun resolve(target: String): Navigatable? {
val project = getProject() ?: return null
logger.info("Resolving file: $target")
val memberSeparatorIndex = target.indexOf('#')
val filePath = if (memberSeparatorIndex > 0) {
target.substring(0, memberSeparatorIndex)
} else {
target
}
val fileName = filePath.substringAfterLast('/')
val matchingVirtualFile =
FilenameIndex.getVirtualFilesByName(fileName, GlobalSearchScope.projectScope(project))
.firstOrNull { vf ->
vf.path.endsWith(filePath) || vf.name == fileName
}
if (matchingVirtualFile != null) {
val psiFile = PsiManager.getInstance(project).findFile(matchingVirtualFile)
if (psiFile != null) {
if (memberSeparatorIndex > 0) {
val memberName = target.substring(memberSeparatorIndex + 1)
val memberElement =
PsiTreeUtil.findChildrenOfType(psiFile, PsiNamedElement::class.java)
.firstOrNull { it.name == memberName }
return (memberElement as? Navigatable) ?: psiFile
}
return psiFile
}
}
return try {
val fileModel = GotoFileModel(project)
fileModel.getElementsByName(fileName, true, fileName)
.filterIsInstance<Navigatable>()
.firstOrNull { it.canNavigate() }
} catch (t: Throwable) {
logger.warn("File search failed for: $target", t)
null
}
}
}
object NavigationResolverFactory {
fun create(protocol: String): NavigationResolver {
return when (protocol) {
"psi_element://" -> PsiElementResolver()
"file://" -> FileResolver()
else -> throw IllegalArgumentException("Unsupported protocol: $protocol")
}
}
}

View file

@ -7,8 +7,6 @@
<depends optional="true" config-file="plugin-kotlin.xml">org.jetbrains.kotlin</depends>
<depends optional="true" config-file="plugin-java.xml">com.intellij.modules.java</depends>
<depends optional="true" config-file="plugin-python.xml">com.intellij.modules.python</depends>
<!-- TODO-->
<!-- <depends optional="true" config-file="plugin-js.xml">JavaScript</depends>-->
<!-- <depends optional="true" config-file="plugin-go.xml">org.jetbrains.plugins.go</depends>-->
<!-- <depends optional="true" config-file="plugin-ruby.xml">com.intellij.modules.ruby</depends>-->
<!-- <depends optional="true" config-file="plugin-php.xml">com.jetbrains.php</depends>-->
@ -199,15 +197,15 @@
<separator/>
</group>
<!--
Removed add-to-group into deprecated/removed VCS groups for 2025.1+/Remote Dev.
The action remains available via "Find Action" to avoid startup errors in Thin Client.
-->
<group id="CodeGPT.VcsLogContextMenu">
<separator/>
<action
id="CodeGPT.ExplainGitCommitAction"
class="ee.carlrobert.codegpt.actions.ExplainGitCommitAction"/>
<add-to-group
group-id="Vcs.Log.ContextMenu"
relative-to-action="Vcs.Log.CompareRevisions"
anchor="after"/>
<separator/>
</group>
@ -336,7 +334,7 @@
text="ProxyAI"
popup="true"
icon="ee.carlrobert.codegpt.Icons.DefaultSmall">
<add-to-group group-id="Vcs.MessageActionGroup" anchor="first"/>
<!-- Removed add-to-group to Vcs.MessageActionGroup to prevent startup errors in 251.* -->
<action
id="CodeGPT.GenerateGitCommitMessage"
text="Generate Commit Message"

View file

@ -166,6 +166,8 @@ configurationConfigurable.section.chatCompletion.psiStructure.title=Enable depen
configurationConfigurable.section.chatCompletion.psiStructure.analyzeDepth.title=Code analyze depth:
configurationConfigurable.section.chatCompletion.psiStructure.analyzeDepth.comment=The parameter limits the depth of the PSI structure traversal. Currently, it is implemented only for the Kotlin language.
configurationConfigurable.section.chatCompletion.psiStructure.description=If enabled, the class structure that is present in the imports of the attached files will be added in the context of the dialog. A structure refers to the source code in files that include constructors, fields, and methods, with all modifiers, arguments, and return types, but without an implementation. The implementation of dependencies is intentionally excluded in order to find a balance between a high-quality chat context and saving tokens.
configurationConfigurable.section.chatCompletion.clickableLinks.title=Show clickable links for classes and methods
configurationConfigurable.section.chatCompletion.clickableLinks.description=If enabled, code references in answers become clickable so you can jump to them in your IDE.
settingsConfigurable.service.llama.predefinedModel.comment=Download and use vetted models from HuggingFace.
settingsConfigurable.service.llama.customModel.comment=Use your own GGUF model file from a local path on your computer.
settingsConfigurable.service.custom.openai.testConnection.label=Test Connection

View file

@ -166,6 +166,8 @@ configurationConfigurable.section.chatCompletion.psiStructure.title=\u542F\u7528
configurationConfigurable.section.chatCompletion.psiStructure.analyzeDepth.title=\u4EE3\u7801\u5206\u6790\u6DF1\u5EA6:
configurationConfigurable.section.chatCompletion.psiStructure.analyzeDepth.comment=\u8BE5\u53C2\u6570\u9650\u5236PSI\u7ED3\u6784\u904D\u5386\u7684\u6DF1\u5EA6\u3002\u76EE\u524D\u4EC5\u9488\u5BF9Kotlin\u8BED\u8A00\u5B9E\u73B0\u3002
configurationConfigurable.section.chatCompletion.psiStructure.description=\u5982\u679C\u542F\u7528\uFF0C\u9644\u52A0\u6587\u4EF6\u5BFC\u5165\u4E2D\u5B58\u5728\u7684\u7C7B\u7ED3\u6784\u5C06\u88AB\u6DFB\u52A0\u5230\u5BF9\u8BDD\u7684\u4E0A\u4E0B\u6587\u4E2D\u3002\u7ED3\u6784\u6307\u7684\u662F\u6587\u4EF6\u4E2D\u5305\u542B\u6784\u9020\u51FD\u6570\u3001\u5B57\u6BB5\u548C\u65B9\u6CD5\u7684\u6E90\u4EE3\u7801\uFF0C\u5305\u62EC\u6240\u6709\u4FEE\u9970\u7B26\u3001\u53C2\u6570\u548C\u8FD4\u56DE\u7C7B\u578B\uFF0C\u4F46\u4E0D\u5305\u62EC\u5B9E\u73B0\u3002\u4E3A\u4E86\u5728\u9AD8\u8D28\u91CF\u804A\u5929\u4E0A\u4E0B\u6587\u548C\u8282\u7701\u6807\u8BB0\u4E4B\u95F4\u627E\u5230\u5E73\u8861\uFF0C\u6545\u610F\u6392\u9664\u4E86\u4F9D\u8D56\u7684\u5B9E\u73B0\u3002
configurationConfigurable.section.chatCompletion.clickableLinks.title=Show clickable links for classes and methods
configurationConfigurable.section.chatCompletion.clickableLinks.description=If enabled, code references in answers become clickable so you can jump to them in your IDE.
settingsConfigurable.service.llama.predefinedModel.comment=\u4ECEHuggingFace\u4E0B\u8F7D\u5E76\u4F7F\u7528\u7ECF\u8FC7\u5BA1\u67E5\u7684\u6A21\u578B\u3002
settingsConfigurable.service.llama.customModel.comment=\u4F7F\u7528\u60A8\u8BA1\u7B97\u673A\u4E0A\u672C\u5730\u8DEF\u5F84\u4E2D\u7684GGUF\u6A21\u578B\u6587\u4EF6\u3002
settingsConfigurable.service.custom.openai.testConnection.label=\u6D4B\u8BD5\u8FDE\u63A5
@ -416,4 +418,4 @@ conversation.status.count.plural={0}\u4E2A\u5BF9\u8BDD
conversation.status.sortedBy=\u6392\u5E8F\u65B9\u5F0F: {0}
conversation.deleteConfirmation.message=\u60A8\u786E\u5B9A\u8981\u5220\u9664\u6B64\u5BF9\u8BDD\u5417?
conversation.deleteConfirmation.title=\u5220\u9664\u5BF9\u8BDD
chat.message.welcome=\u55E8 <strong>{0}</strong>, \u6211\u662F ProxyAI\uFF01\u4F60\u53EF\u4EE5\u95EE\u6211\u4EFB\u4F55\u95EE\u9898\uFF0C\u4F46\u5927\u591A\u6570\u4EBA\u4F1A\u8BF7\u6C42\u6211\u63D0\u4F9B\u4EE3\u7801\u65B9\u9762\u7684\u5E2E\u52A9\u3002\u4EE5\u4E0B\u662F\u4E00\u4E9B\u4F60\u53EF\u4EE5\u5411\u6211\u54A8\u8BE2\u7684\u95EE\u9898\uFF1A
chat.message.welcome=\u55E8 <strong>{0}</strong>, \u6211\u662F ProxyAI\uFF01\u4F60\u53EF\u4EE5\u95EE\u6211\u4EFB\u4F55\u95EE\u9898\uFF0C\u4F46\u5927\u591A\u6570\u4EBA\u4F1A\u8BF7\u6C42\u6211\u63D0\u4F9B\u4EE3\u7801\u65B9\u9762\u7684\u5E2E\u52A9\u3002\u4EE5\u4E0B\u662F\u4E00\u4E9B\u4F60\u53EF\u4EE5\u5411\u6211\u54A8\u8BE2\u7684\u95EE\u9898\uFF1A

View file

@ -78,4 +78,4 @@ Formatting Guidelines:
6. Do not provide an implementation plan for pure explanations or general questions.
7. When refactoring an entire file, output multiple code blocks as needed, keeping changes concise unless a more extensive update is required.
7. When refactoring an entire file, output multiple code blocks as needed, keeping changes concise unless a more extensive update is required.

View file

@ -73,5 +73,3 @@ Formatting Guidelines:
5. Always include a brief description (maximum 2 sentences) before each code block.
6. Do not provide an implementation plan for pure explanations or general questions.
7. When refactoring an entire file, provide the complete updated file content in a single code block.

View file

@ -0,0 +1,69 @@
## JetBrains Navigation Links (MANDATORY)
**Link every concrete symbol** (class, method, field, constant, function) using these two protocols only:
### Navigation Protocols
**Java/Kotlin ONLY (.java, .kt files):**
- Classes: `psi_element://fully.qualified.ClassName` (MUST be fully qualified)
- Methods: `psi_element://fully.qualified.ClassName#methodName`
- Fields: `psi_element://fully.qualified.ClassName#fieldName`
- Constants: `psi_element://fully.qualified.ClassName#CONSTANT_NAME`
**All Other Languages (C/C++, JS, Python, etc.):**
- Functions: `file://src/path/file.ext#functionName`
- Constants/Variables: `file://src/path/file.ext#VARIABLE_NAME`
- Files: `file://src/path/file.ext`
**No Link Available:**
- Use backticks: `someSymbol` (when no file context or reference is possible)
### Critical Rules
1. **psi_element:// ONLY for Java/Kotlin**: Never use for other languages
2. **file:// for everything else**: C/C++, JavaScript, Python, Go, etc.
3. **Visible text = exact symbol name**: `[Repository]`, `[handleSubmit]`, `[API_KEY]`
4. **Use backticks when no context**: If you can't determine file location, use `backticks`
5. **Methods must include owner**: `fully.qualified.ClassName#methodName` or `file.ext#functionName`
6. **MANDATORY**: Always use fully qualified class names - never use short class names alone
### Examples
**Java/Kotlin:**
```
The [System](psi_element://java.lang.System) class has [out](psi_element://java.lang.System#out) field.
Use [UserRepository](psi_element://com.example.repository.UserRepository) with [findById](psi_element://com.example.repository.UserRepository#findById).
The [name](psi_element://com.example.model.User#name) field in [User](psi_element://com.example.model.User) class.
```
**C/C++:**
```
Call [logError](file://src/utils/logger.cpp#logError) function.
The [MAX_SIZE](file://include/constants.h#MAX_SIZE) constant is defined.
Implement [processData](file://src/processor.h#processData) in header.
```
**JavaScript:**
```
The [handleClick](file://src/components/Button.js#handleClick) event handler.
Export [API_BASE_URL](file://src/config.js#API_BASE_URL) from config.
```
**Python:**
```
Use [parse_json](file://utils/parser.py#parse_json) function.
The [DATABASE_URL](file://settings.py#DATABASE_URL) setting.
```
**No Context Available:**
```
Use the `printf` function for formatting.
The `malloc` function allocates memory.
Consider using `async/await` pattern.
```
**Files:**
```
Edit the [main.cpp](file://src/main.cpp) file.
Check [UserService.java](file://src/service/UserService.java).
```

View file

@ -7,6 +7,7 @@ import ee.carlrobert.codegpt.conversations.message.Message
import ee.carlrobert.codegpt.settings.prompts.PersonaPromptDetailsState
import ee.carlrobert.codegpt.settings.prompts.PromptsSettings
import ee.carlrobert.llm.client.openai.completion.OpenAIChatCompletionModel
import ee.carlrobert.codegpt.util.file.FileUtil.getResourceContent
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.groups.Tuple
import org.junit.jupiter.api.Assertions.assertThrows
@ -33,10 +34,12 @@ class CompletionRequestProviderTest : IntegrationTest() {
val request = OpenAIRequestFactory().createChatRequest(callParameters)
val guidelines = getResourceContent("/prompts/persona/psi-navigation-guidelines.txt")
val expectedSystem = "TEST_SYSTEM_PROMPT\n$guidelines"
assertThat(request.messages)
.extracting("role", "content")
.containsExactly(
Tuple.tuple("system", "TEST_SYSTEM_PROMPT\n"),
Tuple.tuple("system", expectedSystem),
Tuple.tuple("user", "TEST_PROMPT"),
Tuple.tuple("assistant", firstMessage.response),
Tuple.tuple("user", "TEST_PROMPT"),
@ -64,10 +67,12 @@ class CompletionRequestProviderTest : IntegrationTest() {
val request = OpenAIRequestFactory().createChatRequest(callParameters)
val guidelines = getResourceContent("/prompts/persona/psi-navigation-guidelines.txt")
val expectedSystem = "TEST_SYSTEM_PROMPT\n$guidelines"
assertThat(request.messages)
.extracting("role", "content")
.containsExactly(
Tuple.tuple("system", "TEST_SYSTEM_PROMPT\n"),
Tuple.tuple("system", expectedSystem),
Tuple.tuple("user", "FIRST_TEST_PROMPT"),
Tuple.tuple("assistant", firstMessage.response),
Tuple.tuple("user", "SECOND_TEST_PROMPT")

View file

@ -14,6 +14,7 @@ import ee.carlrobert.llm.client.util.JSONUtil.*
import org.apache.http.HttpHeaders
import org.assertj.core.api.Assertions.assertThat
import testsupport.IntegrationTest
import ee.carlrobert.codegpt.util.file.FileUtil.getResourceContent
class DefaultToolwindowChatCompletionRequestHandlerTest : IntegrationTest() {
@ -31,6 +32,8 @@ class DefaultToolwindowChatCompletionRequestHandlerTest : IntegrationTest() {
assertThat(request.uri.path).isEqualTo("/v1/chat/completions")
assertThat(request.method).isEqualTo("POST")
assertThat(request.headers[HttpHeaders.AUTHORIZATION]!![0]).isEqualTo("Bearer TEST_API_KEY")
val guidelines = getResourceContent("/prompts/persona/psi-navigation-guidelines.txt")
val expectedSystem = "TEST_SYSTEM_PROMPT\n$guidelines"
assertThat(request.body)
.extracting(
"model",
@ -39,7 +42,7 @@ class DefaultToolwindowChatCompletionRequestHandlerTest : IntegrationTest() {
.containsExactly(
"gpt-4o",
listOf(
mapOf("role" to "system", "content" to "TEST_SYSTEM_PROMPT\n"),
mapOf("role" to "system", "content" to expectedSystem),
mapOf("role" to "user", "content" to "TEST_PROMPT")
)
)
@ -75,6 +78,8 @@ class DefaultToolwindowChatCompletionRequestHandlerTest : IntegrationTest() {
conversation.addMessage(Message("Ping", "Pong"))
expectLlama(StreamHttpExchange { request: RequestEntity ->
assertThat(request.uri.path).isEqualTo("/completion")
val guidelines = getResourceContent("/prompts/persona/psi-navigation-guidelines.txt")
val expectedSystem = "TEST_SYSTEM_PROMPT\n$guidelines"
assertThat(request.body)
.extracting(
"prompt",
@ -83,7 +88,7 @@ class DefaultToolwindowChatCompletionRequestHandlerTest : IntegrationTest() {
)
.containsExactly(
LLAMA.buildPrompt(
"TEST_SYSTEM_PROMPT",
expectedSystem,
"TEST_PROMPT",
conversation.messages
),
@ -122,6 +127,8 @@ class DefaultToolwindowChatCompletionRequestHandlerTest : IntegrationTest() {
assertThat(request.uri.path).isEqualTo("/v1/chat/completions")
assertThat(request.method).isEqualTo("POST")
assertThat(request.headers[HttpHeaders.AUTHORIZATION]!![0]).isEqualTo("Bearer TEST_API_KEY")
val guidelines = getResourceContent("/prompts/persona/psi-navigation-guidelines.txt")
val expectedSystem = "TEST_SYSTEM_PROMPT\n$guidelines"
assertThat(request.body)
.extracting(
"model",
@ -130,7 +137,7 @@ class DefaultToolwindowChatCompletionRequestHandlerTest : IntegrationTest() {
.containsExactly(
HuggingFaceModel.LLAMA_3_8B_Q6_K.code,
listOf(
mapOf("role" to "system", "content" to "TEST_SYSTEM_PROMPT\n"),
mapOf("role" to "system", "content" to expectedSystem),
mapOf("role" to "user", "content" to "TEST_PROMPT")
)
)
@ -154,6 +161,8 @@ class DefaultToolwindowChatCompletionRequestHandlerTest : IntegrationTest() {
fun testGoogleChatCompletionCall() {
useGoogleService()
service<ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings>().state
.chatCompletionSettings.clickableLinksEnabled = true
val customPersona = PersonaPromptDetailsState().apply {
id = 999L
name = "Test Persona"
@ -166,13 +175,15 @@ class DefaultToolwindowChatCompletionRequestHandlerTest : IntegrationTest() {
assertThat(request.uri.path).isEqualTo("/v1/models/gemini-2.0-flash:streamGenerateContent")
assertThat(request.method).isEqualTo("POST")
assertThat(request.uri.query).isEqualTo("key=TEST_API_KEY&alt=sse")
val guidelines = getResourceContent("/prompts/persona/psi-navigation-guidelines.txt")
val expectedSystem = "TEST_SYSTEM_PROMPT\n$guidelines"
assertThat(request.body)
.extracting("contents", "systemInstruction")
.containsExactly(
listOf(
mapOf("parts" to listOf(mapOf("text" to "TEST_PROMPT")), "role" to "user"),
),
mapOf("parts" to listOf(mapOf("text" to "TEST_SYSTEM_PROMPT")))
mapOf("parts" to listOf(mapOf("text" to expectedSystem)))
)
listOf(
jsonMapResponse(
@ -212,6 +223,8 @@ class DefaultToolwindowChatCompletionRequestHandlerTest : IntegrationTest() {
assertThat(request.uri.path).isEqualTo("/v1/chat/completions")
assertThat(request.method).isEqualTo("POST")
assertThat(request.headers[HttpHeaders.AUTHORIZATION]!![0]).isEqualTo("Bearer TEST_API_KEY")
val guidelines = getResourceContent("/prompts/persona/psi-navigation-guidelines.txt")
val expectedSystem = "TEST_SYSTEM_PROMPT\n$guidelines"
assertThat(request.body)
.extracting(
"model",
@ -220,9 +233,9 @@ class DefaultToolwindowChatCompletionRequestHandlerTest : IntegrationTest() {
.containsExactly(
"gpt-5-mini",
listOf(
mapOf("role" to "user", "content" to "TEST_SYSTEM_PROMPT\n"),
mapOf("role" to "user", "content" to expectedSystem),
mapOf("role" to "user", "content" to "TEST_PROMPT")
)
)
)
listOf(
jsonMapResponse(

View file

@ -34,12 +34,13 @@ class OpenAIRequestFactoryIntegrationTest : IntegrationTest() {
val request = OpenAIRequestFactory().createChatRequest(callParameters)
val systemMessages = request.messages
.filterIsInstance<ee.carlrobert.llm.client.openai.completion.request.OpenAIChatCompletionStandardMessage>()
.filterIsInstance<OpenAIChatCompletionStandardMessage>()
.filter { it.role == "system" }
.map { it.content }
assertThat(systemMessages).isNotEmpty()
val systemContent = systemMessages.first()
assertThat(systemContent).isEqualTo(
val guidelinesEdit = getResourceContent("/prompts/persona/psi-navigation-guidelines.txt")
assertThat(systemContent).startsWith(
"You are an AI programming assistant integrated into a JetBrains IDE plugin. Your role is to answer coding questions, suggest new code, and perform refactoring or editing tasks. You have access to the following project information:\n" +
"\n" +
"Before we proceed with the main instructions, here is the content of relevant files in the project:\n" +
@ -122,6 +123,7 @@ class OpenAIRequestFactoryIntegrationTest : IntegrationTest() {
"\n" +
"7. When refactoring an entire file, output multiple code blocks as needed, keeping changes concise unless a more extensive update is required.\n"
)
assertThat(systemContent).endsWith(guidelinesEdit)
}
fun testDefaultPersonaIsFilteredInAskMode() {
@ -137,12 +139,13 @@ class OpenAIRequestFactoryIntegrationTest : IntegrationTest() {
val request = OpenAIRequestFactory().createChatRequest(callParameters)
val systemMessages = request.messages
.filterIsInstance<ee.carlrobert.llm.client.openai.completion.request.OpenAIChatCompletionStandardMessage>()
.filterIsInstance<OpenAIChatCompletionStandardMessage>()
.filter { it.role == "system" }
.map { it.content }
assertThat(systemMessages).isNotEmpty()
val systemContent = systemMessages.first()
assertThat(systemContent).isEqualTo(
val guidelinesAskDefault = getResourceContent("/prompts/persona/psi-navigation-guidelines.txt")
assertThat(systemContent).startsWith(
"You are an AI programming assistant integrated into a JetBrains IDE plugin. Your role is to answer coding questions, suggest new code, and perform refactoring or editing tasks. You have access to the following project information:\n" +
"\n" +
"Before we proceed with the main instructions, here is the content of relevant files in the project:\n" +
@ -217,10 +220,9 @@ class OpenAIRequestFactoryIntegrationTest : IntegrationTest() {
"\n" +
"5. Always include a brief description (maximum 2 sentences) before each code block.\n" +
"\n" +
"6. Do not provide an implementation plan for pure explanations or general questions.\n" +
"\n" +
"7. When refactoring an entire file, provide the complete updated file content in a single code block.\n"
"6. Do not provide an implementation plan for pure explanations or general questions.\n\n"
)
assertThat(systemContent).endsWith(guidelinesAskDefault)
}
fun testChatRequestUsesFilteredPersonaPromptInAskMode() {
@ -256,12 +258,13 @@ class OpenAIRequestFactoryIntegrationTest : IntegrationTest() {
val request = OpenAIRequestFactory().createChatRequest(callParameters)
val systemMessages = request.messages
.filterIsInstance<ee.carlrobert.llm.client.openai.completion.request.OpenAIChatCompletionStandardMessage>()
.filterIsInstance<OpenAIChatCompletionStandardMessage>()
.filter { it.role == "system" }
.map { it.content }
assertThat(systemMessages).isNotEmpty()
val systemContent = systemMessages.first()
assertThat(systemContent).isEqualTo(
val guidelinesAskCustom = getResourceContent("/prompts/persona/psi-navigation-guidelines.txt")
assertThat(systemContent).startsWith(
"You are a helpful assistant.\n" +
"For refactoring or editing an existing file, provide the complete modified code.\n" +
"When providing code modifications:\n" +
@ -279,6 +282,7 @@ class OpenAIRequestFactoryIntegrationTest : IntegrationTest() {
" }\n" +
" ```\n"
)
assertThat(systemContent).endsWith(guidelinesAskCustom)
}
fun testChatRequestKeepsOriginalPersonaPromptInEditMode() {
@ -304,7 +308,8 @@ class OpenAIRequestFactoryIntegrationTest : IntegrationTest() {
.map { it.content }
assertThat(systemMessages).isNotEmpty()
val systemContent = systemMessages.first()
assertThat(systemContent).isEqualTo(
val guidelinesEditCustom = getResourceContent("/prompts/persona/psi-navigation-guidelines.txt")
assertThat(systemContent).startsWith(
"You are an AI programming assistant integrated into a JetBrains IDE plugin. Your role is to answer coding questions, suggest new code, and perform refactoring or editing tasks. You have access to the following project information:\n" +
"\n" +
"Before we proceed with the main instructions, here is the content of relevant files in the project:\n" +
@ -387,6 +392,7 @@ class OpenAIRequestFactoryIntegrationTest : IntegrationTest() {
"\n" +
"7. When refactoring an entire file, output multiple code blocks as needed, keeping changes concise unless a more extensive update is required.\n"
)
assertThat(systemContent).endsWith(guidelinesEditCustom)
}
fun testInlineEditSingleRequestNoHistory() {

View file

@ -16,6 +16,7 @@ import ee.carlrobert.llm.client.http.RequestEntity
import ee.carlrobert.llm.client.http.exchange.StreamHttpExchange
import ee.carlrobert.llm.client.util.JSONUtil.*
import org.apache.http.HttpHeaders
import ee.carlrobert.codegpt.util.file.FileUtil.getResourceContent
import org.assertj.core.api.Assertions.assertThat
import testsupport.IntegrationTest
import java.io.IOException
@ -36,6 +37,8 @@ class ChatToolWindowTabPanelTest : IntegrationTest() {
assertThat(request.uri.path).isEqualTo("/v1/chat/completions")
assertThat(request.method).isEqualTo("POST")
assertThat(request.headers[HttpHeaders.AUTHORIZATION]!![0]).isEqualTo("Bearer TEST_API_KEY")
val guidelines = getResourceContent("/prompts/persona/psi-navigation-guidelines.txt")
val expectedSystem = "TEST_SYSTEM_PROMPT\n$guidelines"
assertThat(request.body)
.extracting(
"model",
@ -44,7 +47,7 @@ class ChatToolWindowTabPanelTest : IntegrationTest() {
.containsExactly(
"gpt-4o",
listOf(
mapOf("role" to "system", "content" to "TEST_SYSTEM_PROMPT\n"),
mapOf("role" to "system", "content" to expectedSystem),
mapOf("role" to "user", "content" to "Hello!")
)
)
@ -108,6 +111,8 @@ class ChatToolWindowTabPanelTest : IntegrationTest() {
assertThat(request.uri.path).isEqualTo("/v1/chat/completions")
assertThat(request.method).isEqualTo("POST")
assertThat(request.headers[HttpHeaders.AUTHORIZATION]!![0]).isEqualTo("Bearer TEST_API_KEY")
val guidelines = getResourceContent("/prompts/persona/psi-navigation-guidelines.txt")
val expectedSystem = "TEST_SYSTEM_PROMPT\n$guidelines"
assertThat(request.body)
.extracting(
"model",
@ -116,7 +121,7 @@ class ChatToolWindowTabPanelTest : IntegrationTest() {
.containsExactly(
"gpt-4o",
listOf(
mapOf("role" to "system", "content" to "TEST_SYSTEM_PROMPT\n"),
mapOf("role" to "system", "content" to expectedSystem),
mapOf(
"role" to "user",
"content" to """
@ -203,6 +208,8 @@ class ChatToolWindowTabPanelTest : IntegrationTest() {
assertThat(request.method).isEqualTo("POST")
assertThat(request.headers[HttpHeaders.AUTHORIZATION]!![0]).isEqualTo("Bearer TEST_API_KEY")
try {
val guidelines = getResourceContent("/prompts/persona/psi-navigation-guidelines.txt")
val expectedSystem = "TEST_SYSTEM_PROMPT\n$guidelines"
val testImageUrl = ("data:image/png;base64,"
+ Base64.getEncoder()
.encodeToString(Files.readAllBytes(Path.of(testImagePath))))
@ -211,7 +218,7 @@ class ChatToolWindowTabPanelTest : IntegrationTest() {
.containsExactly(
"gpt-4-vision-preview",
listOf(
mapOf("role" to "system", "content" to "TEST_SYSTEM_PROMPT\n"),
mapOf("role" to "system", "content" to expectedSystem),
mapOf(
"role" to "user", "content" to listOf(
mapOf(
@ -408,7 +415,9 @@ class ChatToolWindowTabPanelTest : IntegrationTest() {
)
.containsExactly(
LLAMA.buildPrompt(
"TEST_SYSTEM_PROMPT",
(getResourceContent("/prompts/persona/psi-navigation-guidelines.txt").let { g ->
"TEST_SYSTEM_PROMPT\n$g"
}),
"TEST_PROMPT",
conversation.messages
),