feat: improved popup suggestions and personas support (#638)

* feat: support personas

* fix: replace previous system prompts with personas

* feat: add persona toolbar label

* refactor: rename properties

* refactor: clean up

* fix: personas settings configurable state

* refactor: code cleanup

* feat: list item auto highlightning

* feat: replace personas toolbar label with action link

* refactor: code cleanup

* fix: manual items not being able to delete

* fix: personas settings configurable state

* refactor: clean up code

* fix: folder selection
This commit is contained in:
Carl-Robert 2024-07-25 23:50:31 +03:00 committed by Carl-Robert Linnupuu
parent 40fa27a8bd
commit 307c12e15d
23 changed files with 4266 additions and 369 deletions

View file

@ -21,6 +21,7 @@ import ee.carlrobert.codegpt.conversations.message.Message;
import ee.carlrobert.codegpt.credentials.CredentialsStore;
import ee.carlrobert.codegpt.settings.IncludedFilesSettings;
import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings;
import ee.carlrobert.codegpt.settings.persona.PersonaSettings;
import ee.carlrobert.codegpt.settings.service.anthropic.AnthropicSettings;
import ee.carlrobert.codegpt.settings.service.custom.CustomServiceChatCompletionSettingsState;
import ee.carlrobert.codegpt.settings.service.custom.CustomServiceSettings;
@ -71,9 +72,6 @@ import org.jetbrains.annotations.Nullable;
public class CompletionRequestProvider {
public static final String COMPLETION_SYSTEM_PROMPT =
getResourceContent("/prompts/default-completion.txt");
public static final String GENERATE_COMMIT_MESSAGE_SYSTEM_PROMPT =
getResourceContent("/prompts/generate-commit-message.txt");
@ -197,7 +195,7 @@ public class CompletionRequestProvider {
}
var systemPrompt = conversationType == FIX_COMPILE_ERRORS
? FIX_COMPILE_ERRORS_SYSTEM_PROMPT : ConfigurationSettings.getSystemPrompt();
? FIX_COMPILE_ERRORS_SYSTEM_PROMPT : PersonaSettings.getSystemPrompt();
var prompt = promptTemplate.buildPrompt(
systemPrompt,
@ -302,7 +300,7 @@ public class CompletionRequestProvider {
request.setModel(settings.getModel());
request.setMaxTokens(configuration.getMaxTokens());
request.setStream(true);
request.setSystem(ConfigurationSettings.getSystemPrompt());
request.setSystem(PersonaSettings.getSystemPrompt());
List<ClaudeCompletionMessage> messages = conversation.getMessages().stream()
.filter(prevMessage -> prevMessage.getResponse() != null
&& !prevMessage.getResponse().isEmpty())
@ -345,8 +343,8 @@ public class CompletionRequestProvider {
var message = callParameters.getMessage();
var messages = new ArrayList<OllamaChatCompletionMessage>();
if (callParameters.getConversationType() == ConversationType.DEFAULT) {
String systemPrompt = ConfigurationSettings.getCurrentState().getSystemPrompt();
messages.add(new OllamaChatCompletionMessage("system", systemPrompt, null));
messages.add(
new OllamaChatCompletionMessage("system", PersonaSettings.getSystemPrompt(), null));
}
if (callParameters.getConversationType() == ConversationType.FIX_COMPILE_ERRORS) {
messages.add(
@ -397,8 +395,8 @@ public class CompletionRequestProvider {
var message = callParameters.getMessage();
var messages = new ArrayList<OpenAIChatCompletionMessage>();
if (callParameters.getConversationType() == ConversationType.DEFAULT) {
String systemPrompt = ConfigurationSettings.getCurrentState().getSystemPrompt();
messages.add(new OpenAIChatCompletionStandardMessage("system", systemPrompt));
messages.add(
new OpenAIChatCompletionStandardMessage("system", PersonaSettings.getSystemPrompt()));
}
if (callParameters.getConversationType() == ConversationType.FIX_COMPILE_ERRORS) {
messages.add(
@ -474,8 +472,7 @@ public class CompletionRequestProvider {
// Gemini API does not support direct 'system' prompts:
// see https://www.reddit.com/r/Bard/comments/1b90i8o/does_gemini_have_a_system_prompt_option_while/
if (callParameters.getConversationType() == ConversationType.DEFAULT) {
String systemPrompt = ConfigurationSettings.getCurrentState().getSystemPrompt();
messages.add(new GoogleCompletionContent("user", List.of(systemPrompt)));
messages.add(new GoogleCompletionContent("user", List.of(PersonaSettings.getSystemPrompt())));
messages.add(new GoogleCompletionContent("model", List.of("Understood.")));
}
if (callParameters.getConversationType() == ConversationType.FIX_COMPILE_ERRORS) {

View file

@ -1,7 +1,6 @@
package ee.carlrobert.codegpt.settings.configuration;
import static ee.carlrobert.codegpt.actions.editor.EditorActionsUtil.DEFAULT_ACTIONS_ARRAY;
import static ee.carlrobert.codegpt.completions.CompletionRequestProvider.COMPLETION_SYSTEM_PROMPT;
import com.intellij.icons.AllIcons;
import com.intellij.icons.AllIcons.Nodes;
@ -49,7 +48,6 @@ public class ConfigurationComponent {
private final JBCheckBox autoFormattingCheckBox;
private final JBCheckBox autocompletionPostProcessingCheckBox;
private final JBCheckBox autocompletionContextAwareCheckBox;
private final JTextArea systemPromptTextArea;
private final JTextArea commitMessagePromptTextArea;
private final IntegerField maxTokensField;
private final JBTextField temperatureField;
@ -94,17 +92,6 @@ public class ConfigurationComponent {
maxTokensField.setColumns(12);
maxTokensField.setValue(configuration.getMaxTokens());
systemPromptTextArea = new JTextArea(3, 60);
if (configuration.getSystemPrompt().isBlank()) {
// for backward compatibility
systemPromptTextArea.setText(COMPLETION_SYSTEM_PROMPT);
} else {
systemPromptTextArea.setText(configuration.getSystemPrompt());
}
systemPromptTextArea.setLineWrap(true);
systemPromptTextArea.setWrapStyleWord(true);
systemPromptTextArea.setBorder(JBUI.Borders.empty(8, 4));
commitMessagePromptTextArea = new JTextArea(configuration.getCommitMessagePrompt(), 3, 60);
commitMessagePromptTextArea.setLineWrap(true);
commitMessagePromptTextArea.setWrapStyleWord(true);
@ -164,7 +151,6 @@ public class ConfigurationComponent {
state.setTableData(getTableData());
state.setMaxTokens(maxTokensField.getValue());
state.setTemperature(Double.parseDouble(temperatureField.getText()));
state.setSystemPrompt(systemPromptTextArea.getText());
state.setCommitMessagePrompt(commitMessagePromptTextArea.getText());
state.setCheckForPluginUpdates(checkForPluginUpdatesCheckBox.isSelected());
state.setCheckForNewScreenshots(checkForNewScreenshotsCheckBox.isSelected());
@ -181,7 +167,6 @@ public class ConfigurationComponent {
setTableData(configuration.getTableData());
maxTokensField.setValue(configuration.getMaxTokens());
temperatureField.setText(String.valueOf(configuration.getTemperature()));
systemPromptTextArea.setText(configuration.getSystemPrompt());
commitMessagePromptTextArea.setText(configuration.getCommitMessagePrompt());
checkForPluginUpdatesCheckBox.setSelected(configuration.isCheckForPluginUpdates());
checkForNewScreenshotsCheckBox.setSelected(configuration.isCheckForNewScreenshots());
@ -242,15 +227,6 @@ public class ConfigurationComponent {
private JPanel createAssistantConfigurationForm() {
var formBuilder = FormBuilder.createFormBuilder();
addAssistantFormLabeledComponent(
formBuilder,
"configurationConfigurable.section.assistant.systemPromptField.label",
"configurationConfigurable.section.assistant.systemPromptField.comment",
JBUI.Panels
.simplePanel(systemPromptTextArea)
.withBorder(JBUI.Borders.customLine(
JBUI.CurrentTheme.CustomFrameDecorations.separatorForeground())));
formBuilder.addVerticalGap(8);
addAssistantFormLabeledComponent(
formBuilder,
"configurationConfigurable.section.assistant.temperatureField.label",

View file

@ -31,8 +31,4 @@ public class ConfigurationSettings implements PersistentStateComponent<Configura
public static ConfigurationSettings getInstance() {
return ApplicationManager.getApplication().getService(ConfigurationSettings.class);
}
public static String getSystemPrompt() {
return getCurrentState().getSystemPrompt();
}
}

View file

@ -1,6 +1,5 @@
package ee.carlrobert.codegpt.settings.configuration;
import static ee.carlrobert.codegpt.completions.CompletionRequestProvider.COMPLETION_SYSTEM_PROMPT;
import static ee.carlrobert.codegpt.completions.CompletionRequestProvider.GENERATE_COMMIT_MESSAGE_SYSTEM_PROMPT;
import ee.carlrobert.codegpt.actions.editor.EditorActionsUtil;
@ -9,7 +8,6 @@ import java.util.Objects;
public class ConfigurationState {
private String systemPrompt = COMPLETION_SYSTEM_PROMPT;
private String commitMessagePrompt = GENERATE_COMMIT_MESSAGE_SYSTEM_PROMPT;
private int maxTokens = 2048;
private double temperature = 0.1;
@ -24,18 +22,10 @@ public class ConfigurationState {
private boolean autocompletionContextAwareEnabled = false;
private Map<String, String> tableData = EditorActionsUtil.DEFAULT_ACTIONS;
public String getSystemPrompt() {
return systemPrompt;
}
public String getCommitMessagePrompt() {
return commitMessagePrompt;
}
public void setSystemPrompt(String systemPrompt) {
this.systemPrompt = systemPrompt;
}
public void setCommitMessagePrompt(String commitMessagePrompt) {
this.commitMessagePrompt = commitMessagePrompt;
}
@ -155,14 +145,13 @@ public class ConfigurationState {
&& autoFormattingEnabled == that.autoFormattingEnabled
&& autocompletionPostProcessingEnabled == that.autocompletionPostProcessingEnabled
&& autocompletionContextAwareEnabled == that.autocompletionContextAwareEnabled
&& Objects.equals(systemPrompt, that.systemPrompt)
&& Objects.equals(commitMessagePrompt, that.commitMessagePrompt)
&& Objects.equals(tableData, that.tableData);
}
@Override
public int hashCode() {
return Objects.hash(systemPrompt, commitMessagePrompt, maxTokens, temperature,
return Objects.hash(commitMessagePrompt, maxTokens, temperature,
checkForPluginUpdates, checkForNewScreenshots, createNewChatOnEachAction,
ignoreGitCommitTokenLimit, methodNameGenerationEnabled, captureCompileErrors,
autoFormattingEnabled, autocompletionPostProcessingEnabled,

View file

@ -7,8 +7,13 @@ import com.intellij.ide.BrowserUtil;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.actionSystem.ActionManager;
import com.intellij.openapi.actionSystem.ActionToolbar;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.DefaultCompactActionGroup;
import com.intellij.openapi.actionSystem.Presentation;
import com.intellij.openapi.actionSystem.ex.CustomComponentAction;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.options.ShowSettingsUtil;
import com.intellij.openapi.project.DumbAwareAction;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.SimpleToolWindowPanel;
import com.intellij.openapi.util.Disposer;
@ -23,6 +28,8 @@ import ee.carlrobert.codegpt.actions.toolwindow.OpenInEditorAction;
import ee.carlrobert.codegpt.conversations.ConversationService;
import ee.carlrobert.codegpt.conversations.ConversationsState;
import ee.carlrobert.codegpt.settings.GeneralSettings;
import ee.carlrobert.codegpt.settings.persona.PersonaSettings;
import ee.carlrobert.codegpt.settings.persona.PersonasConfigurable;
import ee.carlrobert.codegpt.settings.service.ProviderChangeNotifier;
import ee.carlrobert.codegpt.settings.service.ServiceType;
import ee.carlrobert.codegpt.settings.service.codegpt.CodeGPTUserDetailsNotifier;
@ -35,6 +42,7 @@ import java.nio.file.Paths;
import java.util.List;
import java.util.stream.Collectors;
import javax.swing.BoxLayout;
import javax.swing.JComponent;
import javax.swing.JPanel;
import org.jetbrains.annotations.NotNull;
@ -165,6 +173,8 @@ public class ChatToolWindowPanel extends SimpleToolWindowPanel {
new ClearChatWindowAction(() -> tabbedPane.resetCurrentlyActiveTabPanel(project)));
actionGroup.addSeparator();
actionGroup.add(new OpenInEditorAction());
actionGroup.addSeparator();
actionGroup.add(new SelectedPersonaActionLink(project));
var toolbar = ActionManager.getInstance()
.createActionToolbar("NAVIGATION_BAR_TOOLBAR", actionGroup, true);
@ -186,4 +196,47 @@ public class ChatToolWindowPanel extends SimpleToolWindowPanel {
.syncPublisher(IncludeFilesInContextNotifier.FILES_INCLUDED_IN_CONTEXT_TOPIC)
.filesIncluded(emptyList());
}
private static class SelectedPersonaActionLink extends DumbAwareAction implements
CustomComponentAction {
private final Project project;
SelectedPersonaActionLink(Project project) {
this.project = project;
}
@Override
@NotNull
public JComponent createCustomComponent(
@NotNull Presentation presentation,
@NotNull String place) {
var link = new ActionLink(getSelectedPersonaName(), (e) -> {
ShowSettingsUtil.getInstance()
.showSettingsDialog(project, PersonasConfigurable.class);
});
link.setExternalLinkIcon();
link.setFont(JBUI.Fonts.smallFont());
link.setBorder(JBUI.Borders.empty(0, 4));
return link;
}
@Override
public void updateCustomComponent(
@NotNull JComponent component,
@NotNull Presentation presentation) {
((ActionLink) component).setText(getSelectedPersonaName());
}
@Override
public void actionPerformed(@NotNull AnActionEvent e) {
}
private String getSelectedPersonaName() {
return ApplicationManager.getApplication().getService(PersonaSettings.class)
.getState()
.getSelectedPersona()
.getName();
}
}
}

View file

@ -1,7 +1,7 @@
package ee.carlrobert.codegpt.toolwindow.chat.ui.textarea;
import ee.carlrobert.codegpt.EncodingManager;
import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings;
import ee.carlrobert.codegpt.settings.persona.PersonaSettings;
public class TotalTokensDetails {
@ -12,7 +12,7 @@ public class TotalTokensDetails {
private int referencedFilesTokens;
public TotalTokensDetails(EncodingManager encodingManager) {
systemPromptTokens = encodingManager.countTokens(ConfigurationSettings.getSystemPrompt());
systemPromptTokens = encodingManager.countTokens(PersonaSettings.getSystemPrompt());
}
public int getSystemPromptTokens() {

View file

@ -0,0 +1,44 @@
package ee.carlrobert.codegpt.settings.persona
import com.intellij.openapi.components.*
const val DEFAULT_PROMPT =
"You are an AI programming assistant.\nFollow the user's requirements carefully & to the letter.\nYour responses should be informative and logical.\nYou should always adhere to technical information.\nIf the user asks for code or technical questions, you must provide code suggestions and adhere to technical information.\nIf the question is related to a developer, you must respond with content related to a developer.\nFirst think step-by-step - describe your plan for what to build in pseudocode, written out in great detail.\nThen output the code in a single code block.\nMinimize any other prose.\nKeep your answers short and impersonal.\nUse Markdown formatting in your answers.\nMake sure to include the programming language name at the start of the Markdown code blocks.\nAvoid wrapping the whole response in triple backticks.\nThe user works in an IDE built by JetBrains which has a concept for editors with open files, integrated unit test support, and output pane that shows the output of running the code as well as an integrated terminal.\nYou can only give one reply for each conversation turn."
@Service
@State(
name = "CodeGPT_PersonaSettings",
storages = [Storage("CodeGPT_PersonaSettings.xml")]
)
class PersonaSettings :
SimplePersistentStateComponent<PersonaSettingsState>(PersonaSettingsState()) {
companion object {
@JvmStatic
fun getSystemPrompt(): String {
return service<PersonaSettings>().state.selectedPersona.instructions ?: ""
}
}
}
class PersonaSettingsState : BaseState() {
var selectedPersona by property(PersonaDetailsState())
var userCreatedPersonas by list<PersonaDetailsState>()
}
class PersonaDetailsState : BaseState() {
var id by property(1L)
var name by string("CodeGPT Default")
var instructions by string(DEFAULT_PROMPT)
}
@JvmRecord
data class PersonaDetails(val id: Long, val name: String, val instructions: String)
fun PersonaDetails.toPersonaDetailsState(): PersonaDetailsState {
val newState = PersonaDetailsState()
newState.id = id
newState.name = name
newState.instructions = instructions
return newState
}

View file

@ -0,0 +1,28 @@
package ee.carlrobert.codegpt.settings.persona
import com.intellij.openapi.options.Configurable
import javax.swing.JComponent
class PersonasConfigurable : Configurable {
private lateinit var component: PersonasSettingsForm
override fun getDisplayName(): String {
return "CodeGPT: Personas"
}
override fun createComponent(): JComponent {
component = PersonasSettingsForm()
return component.createPanel()
}
override fun isModified(): Boolean = component.isModified()
override fun apply() {
component.applyChanges()
}
override fun reset() {
component.resetChanges()
}
}

View file

@ -0,0 +1,288 @@
package ee.carlrobert.codegpt.settings.persona
import com.intellij.icons.AllIcons
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.components.service
import com.intellij.openapi.ui.DialogPanel
import com.intellij.ui.DocumentAdapter
import com.intellij.ui.ToolbarDecorator
import com.intellij.ui.components.JBScrollPane
import com.intellij.ui.components.JBTextArea
import com.intellij.ui.components.JBTextField
import com.intellij.ui.dsl.builder.Align
import com.intellij.ui.dsl.builder.LabelPosition
import com.intellij.ui.dsl.builder.panel
import com.intellij.ui.table.JBTable
import com.intellij.util.ui.JBUI
import ee.carlrobert.codegpt.util.ResourceUtil
import java.awt.Dimension
import javax.swing.UIManager
import javax.swing.event.DocumentEvent
import javax.swing.table.DefaultTableModel
import javax.swing.text.JTextComponent
class NonEditableTableModel(columnNames: Array<String>, rowCount: Int) :
DefaultTableModel(columnNames, rowCount) {
override fun isCellEditable(row: Int, column: Int) = false
}
class PersonasSettingsForm {
private val tableModel =
NonEditableTableModel(arrayOf("Id", "Name", "Instructions", "FromResource"), 0)
private val table = JBTable(tableModel).apply {
setupTableColumns()
selectionModel.addListSelectionListener { populateEditArea() }
}
private val nameField = JBTextField().apply {
addTextChangeListener { newText ->
updateTableModelIfRowSelected(1, newText)
}
}
private val instructionsTextArea = JBTextArea().apply {
lineWrap = true
wrapStyleWord = true
font = UIManager.getFont("TextField.font")
border = JBUI.Borders.empty(3, 6)
addTextChangeListener { newText ->
updateTableModelIfRowSelected(2, newText)
}
}
private val addedItems = mutableListOf<PersonaDetails>()
private val removedItemIds = mutableListOf<Long>()
init {
setupForm()
}
fun createPanel(): DialogPanel {
return panel {
row {
val toolbarDecorator = ToolbarDecorator.createDecorator(table)
.setAddAction { handleAddItem() }
.setRemoveAction { handleRemoveItem() }
.addExtraAction(object :
AnAction("Duplicate", "Duplicate persona", AllIcons.Actions.Copy) {
override fun actionPerformed(e: AnActionEvent) {
handleDuplicateItem()
}
})
.setRemoveActionUpdater {
val selectedRow = table.selectedRow
selectedRow != -1 && !(tableModel.getValueAt(selectedRow, 3) as Boolean)
}
.disableUpDownActions()
cell(toolbarDecorator.createPanel())
.align(Align.FILL)
.resizableColumn()
.applyToComponent {
preferredSize = Dimension(650, 250)
}
}
row {
cell(nameField)
.label("Name:", LabelPosition.TOP)
.align(Align.FILL)
}
row {
cell(JBScrollPane(instructionsTextArea).apply {
preferredSize = Dimension(650, 225)
})
.label("Instructions:", LabelPosition.TOP)
.align(Align.FILL)
.resizableColumn()
}
}
}
fun applyChanges() {
val persona = getSelectedPersona()
service<PersonaSettings>().state.run {
if (persona != null) {
selectedPersona = persona.toPersonaDetailsState()
}
userCreatedPersonas.removeIf { removedItemIds.contains(it.id) }
userCreatedPersonas.addAll(addedItems.map { it.toPersonaDetailsState() })
}
clear()
}
fun isModified(): Boolean {
service<PersonaSettings>().state.let {
val (id, name, description) = getSelectedPersona() ?: return false
return it.selectedPersona.id != id
|| it.selectedPersona.name != name
|| it.selectedPersona.instructions != description
|| removedItemIds.size > 0
|| addedItems.size > 0
}
}
fun resetChanges() {
clear()
tableModel.rowCount = 0
setupForm()
}
private fun populateEditArea() {
val selectedRow = table.selectedRow
if (selectedRow != -1) {
val userCreatedResource = !(tableModel.getValueAt(selectedRow, 3) as Boolean)
nameField.text = tableModel.getValueAt(selectedRow, 1) as String
nameField.isEnabled = userCreatedResource
instructionsTextArea.text = tableModel.getValueAt(selectedRow, 2) as String
instructionsTextArea.isEnabled = userCreatedResource
} else {
nameField.text = ""
instructionsTextArea.text = ""
}
}
private fun handleAddItem() {
addPersonaToTable(createNewPersona("New Persona", "New Prompt"))
}
private fun handleDuplicateItem() {
val selectedRow = table.selectedRow
val originalName = tableModel.getValueAt(selectedRow, 1) as String
val originalPrompt = tableModel.getValueAt(selectedRow, 2) as String
addPersonaToTable(createNewPersona("$originalName Copy", originalPrompt))
}
private fun createNewPersona(name: String, prompt: String): PersonaDetails {
val newId = findMaxId() + 1
return PersonaDetails(newId, name, prompt)
}
private fun addPersonaToTable(persona: PersonaDetails) {
addedItems.add(persona)
tableModel.addRow(arrayOf(persona.id, persona.name, persona.instructions, false))
selectLastRowAndUpdateUI()
}
private fun findMaxId(): Long {
return (0 until table.rowCount).maxOf {
tableModel.getValueAt(it, 0) as Long
}
}
private fun selectLastRowAndUpdateUI() {
val lastRow = table.rowCount - 1
table.setRowSelectionInterval(lastRow, lastRow)
populateEditArea()
scrollToLastRow()
nameField.requestFocus()
}
private fun handleRemoveItem() {
val selectedRow = table.selectedRow
if (selectedRow != -1 && !(tableModel.getValueAt(selectedRow, 3) as Boolean)) {
val id = tableModel.getValueAt(selectedRow, 0) as Long
if (addedItems.none { it.id == id }) {
removedItemIds.add(id)
}
addedItems.filter { it.id != id }
tableModel.removeRow(selectedRow)
populateEditArea()
val newSelectedRow = if (selectedRow > 0) selectedRow - 1 else 0
if (table.rowCount > 0) {
table.setRowSelectionInterval(newSelectedRow, newSelectedRow)
}
}
}
private fun setupForm() {
service<PersonaSettings>().state.run {
userCreatedPersonas.forEachIndexed { index, persona ->
tableModel.addPersonaRow(
PersonaDetails(
persona.id,
persona.name!!,
persona.instructions!!
),
selectedPersona.id,
index
)
}
ResourceUtil.getFilteredPersonaSuggestions().forEachIndexed { index, persona ->
tableModel.addPersonaRow(persona, selectedPersona.id, index, true)
}
}
}
private fun DefaultTableModel.addPersonaRow(
persona: PersonaDetails,
selectedPersonaId: Long,
rowIndex: Int,
fromResource: Boolean = false
) {
val (id, name, instructions) = persona
addRow(arrayOf(id, name, instructions, fromResource))
if (selectedPersonaId == id) {
table.setRowSelectionInterval(rowIndex, rowIndex)
}
}
private fun scrollToLastRow() {
table.scrollRectToVisible(table.getCellRect(table.rowCount - 1, 0, true))
}
private fun JBTable.setupTableColumns() {
columnModel.apply {
getColumn(0).apply {
minWidth = 0
maxWidth = 0
preferredWidth = 0
resizable = false
}
getColumn(1).preferredWidth = 60
getColumn(2).preferredWidth = 240
getColumn(3).apply {
minWidth = 0
maxWidth = 0
preferredWidth = 0
resizable = false
}
}
}
private fun JBTable.getSelectedPersona(): PersonaDetails? {
if (selectedRow == -1) {
return null
}
return PersonaDetails(
tableModel.getValueAt(selectedRow, 0) as Long,
tableModel.getValueAt(selectedRow, 1) as String,
tableModel.getValueAt(selectedRow, 2) as String
)
}
private fun JTextComponent.addTextChangeListener(listener: (String) -> Unit) {
document.addDocumentListener(object : DocumentAdapter() {
override fun textChanged(e: DocumentEvent) {
listener(e.document.getText(0, e.document.length))
}
})
}
private fun updateTableModelIfRowSelected(column: Int, newValue: Any) {
if (table.selectedRow != -1) {
tableModel.setValueAt(newValue, table.selectedRow, column)
}
}
private fun getSelectedPersona(): PersonaDetails? {
return table.getSelectedPersona()
}
private fun clear() {
addedItems.clear()
removedItemIds.clear()
}
}

View file

@ -4,6 +4,7 @@ import com.intellij.openapi.components.service
import com.intellij.openapi.editor.colors.EditorColorsManager
import com.intellij.openapi.editor.colors.EditorFontType
import com.intellij.openapi.editor.ex.util.EditorUtil
import com.intellij.openapi.util.TextRange
import com.intellij.openapi.util.registry.Registry
import com.intellij.ui.JBColor
import com.intellij.util.ui.JBFont
@ -43,8 +44,12 @@ class CustomTextPane(private val onSubmit: (String) -> Unit) : JTextPane() {
})
}
fun highlightText(text: String) {
val lastIndex = this.text.lastIndexOf('@')
fun appendHighlightedText(
text: String,
searchChar: Char = '@',
withWhitespace: Boolean = true
): TextRange? {
val lastIndex = this.text.lastIndexOf(searchChar)
if (lastIndex != -1) {
val styleContext = StyleContext.getDefaultStyleContext()
val fileNameStyle = styleContext.addStyle("smart-highlighter", null)
@ -71,12 +76,16 @@ class CustomTextPane(private val onSubmit: (String) -> Unit) : JTextPane() {
fileNameStyle,
true
)
document.insertString(
document.length,
" ",
styleContext.getStyle(StyleContext.DEFAULT_STYLE)
)
if (withWhitespace) {
document.insertString(
document.length,
" ",
styleContext.getStyle(StyleContext.DEFAULT_STYLE)
)
}
return TextRange(lastIndex, lastIndex + text.length)
}
return null
}
override fun paintComponent(g: Graphics) {
@ -90,7 +99,6 @@ class CustomTextPane(private val onSubmit: (String) -> Unit) : JTextPane() {
} else {
UIManager.getFont("TextField.font")
}
// Draw placeholder
g2d.drawString(
CodeGPTBundle.get("toolwindow.chat.textArea.emptyText"),
insets.left,

View file

@ -0,0 +1,109 @@
package ee.carlrobert.codegpt.ui.textarea
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.TextRange
import com.jetbrains.rd.util.AtomicReference
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.awt.event.KeyAdapter
import java.awt.event.KeyEvent
import javax.swing.text.StyleContext
import javax.swing.text.StyledDocument
class CustomTextPaneKeyAdapter(
private val project: Project,
private val textPane: CustomTextPane
) : KeyAdapter() {
private val suggestionsPopupManager = SuggestionsPopupManager(project, textPane)
private val popupOpenedAtRange: AtomicReference<TextRange?> = AtomicReference(null)
override fun keyReleased(e: KeyEvent) {
if (textPane.text.isEmpty()) {
// TODO: Remove only the files that were added via shortcuts
project.service<FileSearchService>().removeFilesFromSession()
suggestionsPopupManager.hidePopup()
return
}
if (e.keyCode == KeyEvent.VK_BACK_SPACE) {
if (popupOpenedAtRange.get() == TextRange(
textPane.caretPosition,
textPane.caretPosition + 1
)
) {
suggestionsPopupManager.hidePopup()
return
}
if (textPane.text.isNotEmpty() && textPane.text.last() == '@') {
suggestionsPopupManager.reset()
}
}
when (e.keyCode) {
KeyEvent.VK_UP, KeyEvent.VK_DOWN -> {
suggestionsPopupManager.requestFocus()
suggestionsPopupManager.selectNext()
e.consume()
}
else -> {
if (suggestionsPopupManager.isPopupVisible()) {
updateSuggestions()
}
}
}
}
override fun keyTyped(e: KeyEvent) {
val popupVisible = suggestionsPopupManager.isPopupVisible()
if (e.keyChar == '@' && !popupVisible) {
suggestionsPopupManager.showPopup(textPane)
popupOpenedAtRange.getAndSet(
TextRange(
textPane.caretPosition,
textPane.caretPosition + 1
)
)
return
} else if (e.keyChar == '\t') {
suggestionsPopupManager.requestFocus()
suggestionsPopupManager.selectNext()
return
} else if (popupVisible) {
updateSuggestions()
}
val doc = textPane.document as StyledDocument
if (textPane.caretPosition >= 0) {
doc.setCharacterAttributes(
textPane.caretPosition,
1,
StyleContext.getDefaultStyleContext().getStyle(StyleContext.DEFAULT_STYLE),
true
)
}
}
private fun updateSuggestions() {
CoroutineScope(Dispatchers.Default).launch {
runInEdt {
val lastAtIndex = textPane.text.lastIndexOf('@')
if (lastAtIndex != -1) {
val lastAtSearchIndex = textPane.text.lastIndexOf(':')
if (lastAtSearchIndex != -1) {
val searchText = textPane.text.substring(lastAtSearchIndex + 1)
if (searchText.isNotEmpty()) {
suggestionsPopupManager.updateSuggestions(searchText)
}
}
} else {
suggestionsPopupManager.hidePopup()
}
}
}
}
}

View file

@ -1,33 +1,38 @@
package ee.carlrobert.codegpt.ui.textarea
import com.intellij.icons.AllIcons
import com.intellij.openapi.fileTypes.FileTypeManager
import com.intellij.ui.JBColor
import com.intellij.ui.components.JBList
import com.intellij.ui.dsl.builder.AlignX
import com.intellij.ui.dsl.builder.panel
import com.intellij.util.ui.JBUI
import java.awt.Component
import java.awt.Dimension
import java.awt.KeyboardFocusManager
import java.awt.event.KeyAdapter
import java.awt.event.KeyEvent
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import javax.swing.*
import javax.swing.DefaultListModel
import javax.swing.ListSelectionModel
class SuggestionList(
listModel: DefaultListModel<SuggestionItem>,
private val textPane: CustomTextPane,
private val onSelected: (SuggestionItem) -> Unit
) : JBList<SuggestionItem>(listModel) {
init {
setupUI()
setupKeyboardFocusManager()
setupKeyListener()
setupMouseListener()
setupMouseMotionListener()
}
private fun setupUI() {
border = JBUI.Borders.empty()
preferredSize = Dimension(480, (30 * 6))
selectionMode = ListSelectionModel.SINGLE_SELECTION
cellRenderer = SuggestionsListCellRenderer()
cellRenderer = SuggestionListCellRenderer(textPane)
}
private fun setupKeyboardFocusManager() {
KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher { e ->
if (e.keyCode == KeyEvent.VK_TAB && e.id == KeyEvent.KEY_PRESSED && isFocusOwner) {
if (isTabKeyPressed(e) && isFocusOwner) {
selectNext()
e.consume()
true
@ -35,22 +40,33 @@ class SuggestionList(
false
}
}
}
private fun isTabKeyPressed(e: KeyEvent) =
e.keyCode == KeyEvent.VK_TAB && e.id == KeyEvent.KEY_PRESSED
private fun setupKeyListener() {
addKeyListener(object : KeyAdapter() {
override fun keyReleased(e: KeyEvent) {
when (e.keyCode) {
KeyEvent.VK_ENTER -> {
onSelected(listModel.get(selectedIndex))
e.consume()
}
if (e.keyCode == KeyEvent.VK_ENTER) {
handleEnterKey()
e.consume()
}
}
})
}
private fun handleEnterKey() {
val item = model.getElementAt(selectedIndex)
if (item is SuggestionItem.ActionItem && item.action.enabled || item !is SuggestionItem.ActionItem) {
onSelected(item)
}
}
private fun setupMouseListener() {
addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) {
val index = locationToIndex(e.point)
if (index >= 0) {
onSelected(listModel.getElementAt(index))
}
handleMouseClick(e)
}
override fun mouseExited(e: MouseEvent) {
@ -58,6 +74,20 @@ class SuggestionList(
repaint()
}
})
}
private fun handleMouseClick(e: MouseEvent) {
val index = locationToIndex(e.point)
if (index >= 0) {
val item = model.getElementAt(index)
if (item is SuggestionItem.ActionItem && item.action.enabled || item !is SuggestionItem.ActionItem) {
onSelected(item)
}
e.consume()
}
}
private fun setupMouseMotionListener() {
addMouseMotionListener(object : MouseAdapter() {
override fun mouseMoved(e: MouseEvent) {
val index = locationToIndex(e.point)
@ -74,105 +104,4 @@ class SuggestionList(
selectedIndex = newIndex
ensureIndexIsVisible(newIndex)
}
}
private class SuggestionsListCellRenderer : DefaultListCellRenderer() {
override fun getListCellRendererComponent(
list: JList<*>?,
value: Any?,
index: Int,
isSelected: Boolean,
cellHasFocus: Boolean
): Component = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus).apply {
setOpaque(false)
}.let { component ->
if (component is JLabel && value is SuggestionItem) {
renderSuggestionItem(component, value, list, index, isSelected, cellHasFocus)
} else {
component
}
}
private fun renderSuggestionItem(
component: JLabel,
value: SuggestionItem,
list: JList<*>?,
index: Int,
isSelected: Boolean,
cellHasFocus: Boolean
): JPanel = when (value) {
is SuggestionItem.FileItem -> renderFileItem(component, value)
is SuggestionItem.ActionItem -> renderActionItem(component, value)
}.apply {
setupPanelProperties(list, index, isSelected, cellHasFocus)
}
private fun renderFileItem(component: JLabel, value: SuggestionItem.FileItem): JPanel {
val file = value.file
component.apply {
text = file.name
icon = when {
file.isDirectory -> AllIcons.Nodes.Folder
else -> FileTypeManager.getInstance().getFileTypeByFileName(file.name).icon
}
iconTextGap = 4
}
return panel {
row {
cell(component)
text(truncatePath(480 - component.width - 28, file.path))
.align(AlignX.RIGHT)
.applyToComponent {
font = JBUI.Fonts.smallFont()
foreground = JBColor.gray
}
}
}
}
private fun renderActionItem(component: JLabel, value: SuggestionItem.ActionItem): JPanel {
component.apply {
text = value.action.displayName
icon = value.action.icon
iconTextGap = 4
}
return panel {
row {
cell(component)
}
}
}
private fun JPanel.setupPanelProperties(
list: JList<*>?,
index: Int,
isSelected: Boolean,
cellHasFocus: Boolean
) {
preferredSize = Dimension(preferredSize.width, 30)
border = JBUI.Borders.empty(0, 4, 0, 4)
val isHovered = list?.getClientProperty("hoveredIndex") == index
if (isHovered || isSelected || cellHasFocus) {
background = UIManager.getColor("List.selectionBackground")
foreground = UIManager.getColor("List.selectionForeground")
}
}
private fun truncatePath(maxWidth: Int, fullPath: String): String {
val fontMetrics = getFontMetrics(JBUI.Fonts.smallFont())
if (fontMetrics.stringWidth(fullPath) <= maxWidth) {
return fullPath
}
val ellipsis = "..."
var truncatedPath = fullPath
while (truncatedPath.isNotEmpty() && fontMetrics.stringWidth(ellipsis + truncatedPath) > maxWidth) {
truncatedPath = truncatedPath.substring(1)
}
return ellipsis + truncatedPath
}
}

View file

@ -0,0 +1,188 @@
package ee.carlrobert.codegpt.ui.textarea
import com.intellij.icons.AllIcons
import com.intellij.openapi.components.service
import com.intellij.openapi.fileTypes.FileTypeManager
import com.intellij.ui.ColorUtil
import com.intellij.ui.JBColor
import com.intellij.ui.dsl.builder.AlignX
import com.intellij.ui.dsl.builder.panel
import com.intellij.ui.dsl.gridLayout.UnscaledGaps
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.JBUI.CurrentTheme.GotItTooltip
import ee.carlrobert.codegpt.settings.persona.PersonaSettings
import java.awt.Component
import java.awt.Dimension
import javax.swing.*
class SuggestionListCellRenderer(
private val textPane: CustomTextPane
) : DefaultListCellRenderer() {
override fun getListCellRendererComponent(
list: JList<*>?,
value: Any?,
index: Int,
isSelected: Boolean,
cellHasFocus: Boolean
): Component =
super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus).apply {
setOpaque(false)
}.let { component ->
if (component is JLabel && value is SuggestionItem) {
renderSuggestionItem(component, value, list, index, isSelected, cellHasFocus)
} else {
component
}
}
private fun renderSuggestionItem(
component: JLabel,
value: SuggestionItem,
list: JList<*>?,
index: Int,
isSelected: Boolean,
cellHasFocus: Boolean
): JPanel = when (value) {
is SuggestionItem.FileItem -> renderFileItem(component, value)
is SuggestionItem.FolderItem -> renderFolderItem(component, value)
is SuggestionItem.ActionItem -> renderActionItem(component, value)
is SuggestionItem.PersonaItem -> renderPersonaItem(component, value)
}.apply {
setupPanelProperties(list, index, isSelected, cellHasFocus)
}
private fun renderFileItem(component: JLabel, item: SuggestionItem.FileItem): JPanel {
val icon = when {
item.file.isDirectory -> AllIcons.Nodes.Folder
else -> service<FileTypeManager>().getFileTypeByFileName(item.file.name).icon
}
return createDefaultPanel(component, icon, item.file.name, item.file.path)
}
private fun renderFolderItem(component: JLabel, item: SuggestionItem.FolderItem): JPanel {
return createDefaultPanel(
component,
AllIcons.Nodes.Folder,
item.folder.name,
item.folder.path
)
}
private fun renderActionItem(component: JLabel, item: SuggestionItem.ActionItem): JPanel {
val description = if (item.action == DefaultAction.PERSONAS)
service<PersonaSettings>().state.selectedPersona.name
else null
return createDefaultPanel(
component.apply {
disabledIcon = item.action.icon
isEnabled = item.action.enabled
},
item.action.icon,
item.action.displayName,
description
)
}
private fun renderPersonaItem(component: JLabel, item: SuggestionItem.PersonaItem): JPanel {
return createDefaultPanel(
component,
AllIcons.General.User,
item.personaDetails.name,
item.personaDetails.instructions,
)
}
private fun getSearchText(text: String): String? {
val lastAtIndex = text.lastIndexOf('@')
if (lastAtIndex == -1) return null
val lastColonIndex = text.lastIndexOf(':')
if (lastColonIndex == -1) return null
return text.substring(lastColonIndex + 1).takeIf { it.isNotEmpty() }
}
private fun generateHighlightedHtml(title: String, searchText: String): String {
val searchIndex = title.indexOf(searchText, ignoreCase = true)
if (searchIndex == -1) return title
val prefix = title.substring(0, searchIndex)
val highlight = title.substring(
searchIndex,
(searchIndex + searchText.length).coerceAtMost(title.length)
)
val suffix = title.substring((searchIndex + searchText.length).coerceAtMost(title.length))
val foregroundHex = ColorUtil.toHex(GotItTooltip.codeForeground(true))
val backgroundHex = ColorUtil.toHex(GotItTooltip.codeBackground(true))
return "<html>$prefix<span style=\"color: $foregroundHex;background-color: $backgroundHex;\">$highlight</span>$suffix</html>"
}
private fun createDefaultPanel(
label: JLabel,
labelIcon: Icon,
title: String,
description: String? = null
): JPanel {
val searchText = getSearchText(textPane.text)
label.apply {
icon = labelIcon
iconTextGap = 4
text = if (searchText != null) {
generateHighlightedHtml(title, searchText)
} else {
title
}
}
return panel {
row {
cell(label)
if (description != null) {
text(description.truncate(480 - label.width - 28, false))
.customize(UnscaledGaps(left = 8))
.align(AlignX.RIGHT)
.applyToComponent {
font = JBUI.Fonts.smallFont()
foreground = JBColor.gray
}
}
}
}
}
private fun JPanel.setupPanelProperties(
list: JList<*>?,
index: Int,
isSelected: Boolean,
cellHasFocus: Boolean
) {
preferredSize = Dimension(480, 30)
border = JBUI.Borders.empty(0, 4, 0, 4)
val isHovered = list?.getClientProperty("hoveredIndex") == index
if (isHovered || isSelected || cellHasFocus) {
background = UIManager.getColor("List.selectionBackground")
foreground = UIManager.getColor("List.selectionForeground")
}
}
private fun String.truncate(maxWidth: Int, fromEnd: Boolean = true): String {
val fontMetrics = getFontMetrics(JBUI.Fonts.smallFont())
if (fontMetrics.stringWidth(this) <= maxWidth) return this
val ellipsis = "..."
var truncated = this
while (fontMetrics.stringWidth(ellipsis + truncated) > maxWidth && truncated.isNotEmpty()) {
truncated = if (fromEnd) {
truncated.drop(1)
} else {
truncated.dropLast(1)
}
}
return ellipsis + truncated
}
}

View file

@ -0,0 +1,164 @@
package ee.carlrobert.codegpt.ui.textarea
import com.intellij.openapi.application.readAction
import com.intellij.openapi.components.service
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.ProjectFileIndex
import ee.carlrobert.codegpt.util.ResourceUtil
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.nio.file.Files
import java.nio.file.Path
import javax.swing.DefaultListModel
import kotlin.io.path.absolutePathString
import kotlin.io.path.isDirectory
import kotlin.io.path.name
interface SuggestionUpdateStrategy {
fun populateSuggestions(
project: Project,
listModel: DefaultListModel<SuggestionItem>,
)
fun updateSuggestions(
project: Project,
listModel: DefaultListModel<SuggestionItem>,
searchText: String,
)
}
class FileSuggestionActionStrategy : SuggestionUpdateStrategy {
override fun populateSuggestions(
project: Project,
listModel: DefaultListModel<SuggestionItem>,
) {
val projectFileIndex = project.service<ProjectFileIndex>()
CoroutineScope(Dispatchers.Default).launch {
val openFilePaths = project.service<FileEditorManager>().openFiles
.filter { readAction { projectFileIndex.isInContent(it) } }
.take(10)
.map { file -> file.path }
listModel.clear()
listModel.addAll(openFilePaths.map { SuggestionItem.FileItem(File(it)) })
}
}
override fun updateSuggestions(
project: Project,
listModel: DefaultListModel<SuggestionItem>,
searchText: String,
) {
val filePaths = project.service<FileSearchService>().searchFiles(searchText).take(10)
listModel.clear()
listModel.addAll(filePaths.map { SuggestionItem.FileItem(File(it)) })
}
}
class FolderSuggestionActionStrategy : SuggestionUpdateStrategy {
private val projectFoldersCache = mutableMapOf<Project, List<String>>()
override fun populateSuggestions(
project: Project,
listModel: DefaultListModel<SuggestionItem>
) {
CoroutineScope(Dispatchers.Default).launch {
val folderPaths = getProjectFolders(project)
.take(10)
.map { SuggestionItem.FolderItem(Path.of(it).toFile()) }
listModel.clear()
listModel.addAll(folderPaths)
}
}
override fun updateSuggestions(
project: Project,
listModel: DefaultListModel<SuggestionItem>,
searchText: String
) {
CoroutineScope(Dispatchers.Default).launch {
val filteredFolders = getProjectFolders(project)
.filter { it.contains(searchText, ignoreCase = true) }
.take(10)
.map { SuggestionItem.FolderItem(Path.of(it).toFile()) }
listModel.clear()
listModel.addAll(filteredFolders)
}
}
private suspend fun getProjectFolders(project: Project): List<String> {
return projectFoldersCache.getOrPut(project) {
findProjectFolders(project)
}
}
private suspend fun findProjectFolders(project: Project): List<String> {
val projectRoot = project.basePath?.let { Path.of(it) } ?: return emptyList()
return withContext(Dispatchers.IO) {
val uniqueFolders = mutableSetOf<String>()
Files.walk(projectRoot)
.filter { it.isDirectory() && !it.name.startsWith(".") }
.forEach { folder ->
val folderPath = folder.absolutePathString()
if (uniqueFolders.none { it.startsWith(folderPath) }) {
uniqueFolders.removeAll { it.startsWith(folderPath) }
uniqueFolders.add(folderPath)
}
}
uniqueFolders.toList()
}
}
}
class PersonaSuggestionActionStrategy : SuggestionUpdateStrategy {
override fun populateSuggestions(
project: Project,
listModel: DefaultListModel<SuggestionItem>,
) {
listModel.clear()
listModel.addAll(ResourceUtil.getFilteredPersonaSuggestions(null))
}
override fun updateSuggestions(
project: Project,
listModel: DefaultListModel<SuggestionItem>,
searchText: String,
) {
listModel.clear()
listModel.addAll(ResourceUtil.getFilteredPersonaSuggestions { it.name.contains(searchText, true) })
}
}
class CreatePersonaActionStrategy : SuggestionUpdateStrategy {
override fun populateSuggestions(
project: Project,
listModel: DefaultListModel<SuggestionItem>,
) {
}
override fun updateSuggestions(
project: Project,
listModel: DefaultListModel<SuggestionItem>,
searchText: String,
) {
}
}
class DefaultSuggestionActionStrategy : SuggestionUpdateStrategy {
override fun populateSuggestions(
project: Project,
listModel: DefaultListModel<SuggestionItem>,
) {
}
override fun updateSuggestions(
project: Project,
listModel: DefaultListModel<SuggestionItem>,
searchText: String,
) {
}
}

View file

@ -1,62 +1,96 @@
package ee.carlrobert.codegpt.ui.textarea
import com.intellij.icons.AllIcons
import com.intellij.openapi.application.readAction
import com.intellij.openapi.components.service
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.options.ShowSettingsUtil
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.ProjectFileIndex
import com.intellij.openapi.ui.popup.JBPopup
import com.intellij.openapi.ui.popup.JBPopupFactory
import com.intellij.openapi.vfs.VfsUtilCore
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.openapi.vfs.VirtualFileVisitor
import com.intellij.ui.components.JBScrollPane
import com.intellij.util.ui.JBUI
import com.intellij.vcsUtil.showAbove
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import ee.carlrobert.codegpt.settings.persona.PersonaDetails
import ee.carlrobert.codegpt.settings.persona.PersonaSettings
import ee.carlrobert.codegpt.settings.persona.PersonasConfigurable
import java.awt.Dimension
import java.awt.Point
import java.io.File
import java.nio.file.Paths
import javax.swing.DefaultListModel
import javax.swing.Icon
import javax.swing.JComponent
import javax.swing.ScrollPaneConstants
import javax.swing.event.ListDataEvent
import javax.swing.event.ListDataListener
enum class DefaultAction(val displayName: String, val icon: Icon) {
ATTACH_IMAGE("Attach image", AllIcons.FileTypes.Image),
SEARCH_WEB("Search web", AllIcons.General.Web),
enum class DefaultAction(
val displayName: String,
val code: String,
val icon: Icon,
val enabled: Boolean = true
) {
FILES("Files →", "file:", AllIcons.FileTypes.Any_type),
FOLDERS("Folders →", "folder:", AllIcons.Nodes.Folder),
PERSONAS("Personas →", "persona:", AllIcons.General.User),
DOCS("Docs (coming soon) →", "docs:", AllIcons.Toolwindows.Documentation, false),
SEARCH_WEB("Web (coming soon)", "", AllIcons.General.Web, false),
CREATE_NEW_PERSONA("Create new persona", "", AllIcons.General.Add),
}
sealed class SuggestionItem {
data class FileItem(val file: File) : SuggestionItem()
data class FolderItem(val folder: File) : SuggestionItem()
data class ActionItem(val action: DefaultAction) : SuggestionItem()
data class PersonaItem(val personaDetails: PersonaDetails) : SuggestionItem()
}
val DEFAULT_ACTIONS = mutableListOf(
SuggestionItem.ActionItem(DefaultAction.FILES),
SuggestionItem.ActionItem(DefaultAction.FOLDERS),
SuggestionItem.ActionItem(DefaultAction.PERSONAS),
SuggestionItem.ActionItem(DefaultAction.DOCS),
SuggestionItem.ActionItem(DefaultAction.SEARCH_WEB),
)
class SuggestionsPopupManager(
private val project: Project,
private val onSelected: (filePath: String) -> Unit
private val textPane: CustomTextPane,
) {
private var currentActionStrategy: SuggestionUpdateStrategy = DefaultSuggestionActionStrategy()
private val appliedActions: MutableList<SuggestionItem.ActionItem> = mutableListOf()
private var popup: JBPopup? = null
private val listModel = DefaultListModel<SuggestionItem>()
private val list = SuggestionList(listModel) {
if (it is SuggestionItem.FileItem) {
onSelected(it.file.path)
} else if (it is SuggestionItem.ActionItem) {
when (it.action) {
DefaultAction.ATTACH_IMAGE -> {} // todo
DefaultAction.SEARCH_WEB -> {} // todo
}
private var originalLocation: Point? = null
private val listModel = DefaultListModel<SuggestionItem>().apply {
addListDataListener(object : ListDataListener {
override fun intervalAdded(e: ListDataEvent) = adjustPopupSize()
override fun intervalRemoved(e: ListDataEvent) {}
override fun contentsChanged(e: ListDataEvent) {}
})
}
private val list = SuggestionList(listModel, textPane) {
when (it) {
is SuggestionItem.ActionItem -> handleActionSelection(it)
is SuggestionItem.FileItem -> handleFileSelection(it.file.path)
is SuggestionItem.FolderItem -> handleFolderSelection(it.folder.path)
is SuggestionItem.PersonaItem -> handlePersonaSelection(it.personaDetails)
}
}
private val scrollPane: JBScrollPane = JBScrollPane(list).apply {
border = JBUI.Borders.empty()
verticalScrollBarPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED
horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER
}
fun showPopup(component: JComponent) {
popup = createPopup(component)
popup?.showAbove(component)
val projectFileIndex = project.service<ProjectFileIndex>()
CoroutineScope(Dispatchers.Default).launch {
val openFilePaths = project.service<FileEditorManager>().openFiles
.filter { readAction { projectFileIndex.isInContent(it) } }
.take(6)
.map { it.path }
updateSuggestions(openFilePaths)
}
originalLocation = component.locationOnScreen
reset(true)
}
fun hidePopup() {
@ -67,11 +101,6 @@ class SuggestionsPopupManager(
return popup?.isVisible ?: false
}
fun updateSuggestions(filePaths: List<String>) {
listModel.clear()
listModel.addAll(filePaths.map { SuggestionItem.FileItem(File(it)) })
}
fun requestFocus() {
list.requestFocus()
}
@ -80,16 +109,119 @@ class SuggestionsPopupManager(
list.selectNext()
}
private fun createPopup(preferableFocusComponent: JComponent? = null): JBPopup =
fun updateSuggestions(searchText: String) {
currentActionStrategy.updateSuggestions(project, listModel, searchText)
}
fun reset(clearPrevious: Boolean = true) {
if (clearPrevious) {
listModel.clear()
}
listModel.addAll(DEFAULT_ACTIONS)
popup?.content?.revalidate()
popup?.content?.repaint()
}
private fun handleActionSelection(item: SuggestionItem.ActionItem) {
if (item.action == DefaultAction.CREATE_NEW_PERSONA) {
hidePopup()
service<ShowSettingsUtil>().showSettingsDialog(
project,
PersonasConfigurable::class.java
)
return
}
appliedActions.add(item)
currentActionStrategy = when (item.action) {
DefaultAction.FILES -> {
FileSuggestionActionStrategy()
}
DefaultAction.FOLDERS -> {
FolderSuggestionActionStrategy()
}
DefaultAction.PERSONAS -> {
PersonaSuggestionActionStrategy()
}
else -> {
DefaultSuggestionActionStrategy()
}
}
currentActionStrategy.populateSuggestions(project, listModel)
textPane.appendHighlightedText(item.action.code, withWhitespace = false)
textPane.requestFocus()
}
private fun handleFileSelection(filePath: String) {
val selectedFile = service<VirtualFileManager>().findFileByNioPath(Paths.get(filePath))
selectedFile?.let { file ->
textPane.appendHighlightedText(file.name, ':')
project.service<FileSearchService>().addFileToSession(file)
}
hidePopup()
}
private fun handleFolderSelection(folderPath: String) {
textPane.appendHighlightedText(folderPath, ':')
val folder = service<VirtualFileManager>().findFileByNioPath(Paths.get(folderPath))
if (folder != null) {
VfsUtilCore.visitChildrenRecursively(folder, object : VirtualFileVisitor<Any>() {
override fun visitFile(file: VirtualFile): Boolean {
if (!file.isDirectory) {
project.service<FileSearchService>().addFileToSession(file)
}
return true
}
})
}
hidePopup()
}
private fun handlePersonaSelection(personaDetails: PersonaDetails) {
service<PersonaSettings>().state.selectedPersona.apply {
id = personaDetails.id
name = personaDetails.name
instructions = personaDetails.instructions
}
textPane.appendHighlightedText(personaDetails.name, ':')
hidePopup()
}
private fun adjustPopupSize() {
val maxVisibleRows = 15
val newRowCount = minOf(listModel.size(), maxVisibleRows)
list.setVisibleRowCount(newRowCount)
list.revalidate()
list.repaint()
popup?.size = list.preferredSize
originalLocation?.let { original ->
val newY = original.y - list.preferredSize.height
popup?.setLocation(Point(original.x, maxOf(newY, 0)))
}
}
private fun createPopup(
preferableFocusComponent: JComponent? = null,
): JBPopup =
service<JBPopupFactory>()
.createComponentPopupBuilder(list, preferableFocusComponent)
.createComponentPopupBuilder(scrollPane, preferableFocusComponent)
.setMovable(true)
.setCancelOnClickOutside(true)
.setCancelOnClickOutside(false)
.setCancelOnWindowDeactivation(false)
.setRequestFocus(true)
.setMinSize(Dimension(480, 30))
.setCancelCallback {
listModel.removeAllElements()
originalLocation = null
currentActionStrategy = DefaultSuggestionActionStrategy()
true
}
.setResizable(true)
.createPopup()
}

View file

@ -7,7 +7,6 @@ import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.components.service
import com.intellij.openapi.observable.properties.AtomicBooleanProperty
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.ui.components.AnActionLink
import com.intellij.ui.dsl.builder.AlignX
import com.intellij.ui.dsl.builder.RightGap
@ -22,16 +21,8 @@ import ee.carlrobert.codegpt.settings.GeneralSettings
import ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowContentManager
import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.ModelComboBoxAction
import ee.carlrobert.codegpt.ui.IconActionButton
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.awt.*
import java.awt.event.KeyAdapter
import java.awt.event.KeyEvent
import java.nio.file.Paths
import javax.swing.JPanel
import javax.swing.text.StyleContext
import javax.swing.text.StyledDocument
class UserInputPanel(
private val project: Project,
@ -39,12 +30,9 @@ class UserInputPanel(
private val onStop: () -> Unit
) : JPanel(BorderLayout()) {
private val suggestionsPopupManager = SuggestionsPopupManager(project) {
handleFileSelection(it)
}
private val textPane = CustomTextPane { handleSubmit() }.apply {
addKeyListener(CustomTextPaneKeyAdapter())
}
private val textPane = CustomTextPane { handleSubmit() }
.apply { addKeyListener(CustomTextPaneKeyAdapter(project, this)) }
private val submitButton = IconActionButton(
object : AnAction(
CodeGPTBundle.get("smartTextPane.submitButton.title"),
@ -78,6 +66,46 @@ class UserInputPanel(
add(getFooter(), BorderLayout.SOUTH)
}
fun setSubmitEnabled(enabled: Boolean) {
submitButton.isEnabled = enabled
stopButton.isEnabled = !enabled
}
override fun requestFocus() {
textPane.requestFocus()
textPane.requestFocusInWindow()
}
override fun paintComponent(g: Graphics) {
val g2 = g.create() as Graphics2D
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
g2.color = background
g2.fillRoundRect(0, 0, width - 1, height - 1, 16, 16)
super.paintComponent(g)
g2.dispose()
}
override fun paintBorder(g: Graphics) {
val g2 = g.create() as Graphics2D
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
g2.color = JBUI.CurrentTheme.ActionButton.focusedBorder()
if (textPane.isFocusOwner) {
g2.stroke = BasicStroke(1.5F)
}
g2.drawRoundRect(0, 0, width - 1, height - 1, 16, 16)
g2.dispose()
}
override fun getInsets(): Insets = JBUI.insets(4)
private fun handleSubmit() {
val text = textPane.text.trim()
if (text.isNotEmpty()) {
onSubmit(text)
textPane.text = ""
}
}
private fun getFooter(): JPanel {
val attachImageLink = AnActionLink(CodeGPTBundle.get("shared.image"), AttachImageAction())
.apply {
@ -115,118 +143,4 @@ class UserInputPanel(
private fun isImageActionSupported(): Boolean {
return service<GeneralSettings>().state.selectedService.isImageActionSupported
}
fun setSubmitEnabled(enabled: Boolean) {
submitButton.isEnabled = enabled
stopButton.isEnabled = !enabled
}
override fun requestFocus() {
textPane.requestFocus()
textPane.requestFocusInWindow()
}
override fun paintComponent(g: Graphics) {
val g2 = g.create() as Graphics2D
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
g2.color = background
g2.fillRoundRect(0, 0, width - 1, height - 1, 16, 16)
super.paintComponent(g)
g2.dispose()
}
override fun paintBorder(g: Graphics) {
val g2 = g.create() as Graphics2D
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
g2.color = JBUI.CurrentTheme.ActionButton.focusedBorder()
if (textPane.isFocusOwner) {
g2.stroke = BasicStroke(1.5F)
}
g2.drawRoundRect(0, 0, width - 1, height - 1, 16, 16)
g2.dispose()
}
override fun getInsets(): Insets = JBUI.insets(4)
private fun updateSuggestions() {
CoroutineScope(Dispatchers.Default).launch {
val lastAtIndex = textPane.text.lastIndexOf('@')
if (lastAtIndex != -1) {
val searchText = textPane.text.substring(lastAtIndex + 1)
if (searchText.isNotEmpty()) {
val filePaths = project.service<FileSearchService>().searchFiles(searchText)
suggestionsPopupManager.updateSuggestions(filePaths)
}
} else {
suggestionsPopupManager.hidePopup()
}
}
}
private fun handleSubmit() {
val text = textPane.text.trim()
if (text.isNotEmpty()) {
onSubmit(text)
textPane.text = ""
}
}
private fun handleFileSelection(filePath: String) {
val selectedFile = service<VirtualFileManager>().findFileByNioPath(Paths.get(filePath))
selectedFile?.let { file ->
textPane.highlightText(file.name)
project.service<FileSearchService>().addFileToSession(file)
}
suggestionsPopupManager.hidePopup()
}
inner class CustomTextPaneKeyAdapter : KeyAdapter() {
private val defaultStyle =
StyleContext.getDefaultStyleContext().getStyle(StyleContext.DEFAULT_STYLE)
override fun keyReleased(e: KeyEvent) {
if (text.isEmpty()) {
project.service<FileSearchService>().removeFilesFromSession()
}
// todo
if (!text.contains('@')) {
suggestionsPopupManager.hidePopup()
return
}
when (e.keyCode) {
KeyEvent.VK_UP, KeyEvent.VK_DOWN -> {
suggestionsPopupManager.requestFocus()
suggestionsPopupManager.selectNext()
e.consume()
}
else -> {
if (suggestionsPopupManager.isPopupVisible()) {
updateSuggestions()
}
}
}
}
override fun keyTyped(e: KeyEvent) {
val popupVisible = suggestionsPopupManager.isPopupVisible()
if (e.keyChar == '@' && !popupVisible) {
suggestionsPopupManager.showPopup(textPane)
return
} else if (e.keyChar == '\t') {
suggestionsPopupManager.requestFocus()
suggestionsPopupManager.selectNext()
return
} else if (popupVisible) {
updateSuggestions()
}
val doc = textPane.document as StyledDocument
if (textPane.caretPosition >= 0) {
doc.setCharacterAttributes(textPane.caretPosition, 1, defaultStyle, true)
}
}
}
}

View file

@ -0,0 +1,29 @@
package ee.carlrobert.codegpt.util
import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.databind.ObjectMapper
import ee.carlrobert.codegpt.settings.persona.PersonaDetails
import ee.carlrobert.codegpt.ui.textarea.DefaultAction
import ee.carlrobert.codegpt.ui.textarea.SuggestionItem
import ee.carlrobert.codegpt.util.file.FileUtil.getResourceContent
object ResourceUtil {
fun getFilteredPersonaSuggestions(
filterPredicate: ((PersonaDetails) -> Boolean)? = null
): List<SuggestionItem> {
var personaDetails = getFilteredPersonaSuggestions()
if (filterPredicate != null) {
personaDetails = personaDetails.filter(filterPredicate).toMutableList()
}
return personaDetails
.map { SuggestionItem.PersonaItem(it) }
.take(10) + listOf(SuggestionItem.ActionItem(DefaultAction.CREATE_NEW_PERSONA))
}
fun getFilteredPersonaSuggestions(): MutableList<PersonaDetails> {
return ObjectMapper().readValue(
getResourceContent("/prompts.json"),
object : TypeReference<MutableList<PersonaDetails>>() {})
}
}

View file

@ -48,6 +48,8 @@
instance="ee.carlrobert.codegpt.settings.configuration.ConfigurationConfigurable"/>
<applicationConfigurable id="settings.codegpt.advanced" parentId="settings.codegpt" displayName="Advanced Settings"
instance="ee.carlrobert.codegpt.settings.advanced.AdvancedSettingsConfigurable"/>
<applicationConfigurable id="settings.codegpt.personas" parentId="settings.codegpt" displayName="Personas"
instance="ee.carlrobert.codegpt.settings.persona.PersonasConfigurable"/>
<applicationConfigurable
parentId="settings.codegpt"
instance="ee.carlrobert.codegpt.telemetry.ui.preferences.TelemetryConfigurable"

View file

@ -189,7 +189,7 @@ toolwindow.chat.youProCheckBox.text=Use GPT-4 model
toolwindow.chat.youProCheckBox.enable=Turn on for complex queries
toolwindow.chat.youProCheckBox.disable=Turn off for faster responses
toolwindow.chat.youProCheckBox.notAllowed=Enable by subscribing to YouPro plan
toolwindow.chat.textArea.emptyText=Ask anything... Use '@' to include files in the message
toolwindow.chat.textArea.emptyText=Ask anything... Use '@' to include additional context
service.codegpt.title=CodeGPT
service.openai.title=OpenAI
service.custom.openai.title=Custom OpenAI

File diff suppressed because it is too large Load diff

View file

@ -1,9 +1,10 @@
package ee.carlrobert.codegpt.completions
import ee.carlrobert.codegpt.completions.CompletionRequestProvider.COMPLETION_SYSTEM_PROMPT
import com.intellij.openapi.components.service
import ee.carlrobert.codegpt.conversations.ConversationService
import ee.carlrobert.codegpt.conversations.message.Message
import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings
import ee.carlrobert.codegpt.settings.persona.DEFAULT_PROMPT
import ee.carlrobert.codegpt.settings.persona.PersonaSettings
import ee.carlrobert.llm.client.openai.completion.OpenAIChatCompletionModel
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.groups.Tuple
@ -13,7 +14,7 @@ class CompletionRequestProviderTest : IntegrationTest() {
fun testChatCompletionRequestWithSystemPromptOverride() {
useOpenAIService()
ConfigurationSettings.getCurrentState().systemPrompt = "TEST_SYSTEM_PROMPT"
service<PersonaSettings>().state.selectedPersona.instructions = "TEST_SYSTEM_PROMPT"
val conversation = ConversationService.getInstance().startConversation()
val firstMessage = createDummyMessage(500)
val secondMessage = createDummyMessage(250)
@ -42,7 +43,7 @@ class CompletionRequestProviderTest : IntegrationTest() {
fun testChatCompletionRequestWithoutSystemPromptOverride() {
useOpenAIService()
ConfigurationSettings.getCurrentState().systemPrompt = COMPLETION_SYSTEM_PROMPT
service<PersonaSettings>().state.selectedPersona.instructions = DEFAULT_PROMPT
val conversation = ConversationService.getInstance().startConversation()
val firstMessage = createDummyMessage(500)
val secondMessage = createDummyMessage(250)
@ -61,7 +62,7 @@ class CompletionRequestProviderTest : IntegrationTest() {
assertThat(request.messages)
.extracting("role", "content")
.containsExactly(
Tuple.tuple("system", COMPLETION_SYSTEM_PROMPT),
Tuple.tuple("system", DEFAULT_PROMPT),
Tuple.tuple("user", "TEST_PROMPT"),
Tuple.tuple("assistant", firstMessage.response),
Tuple.tuple("user", "TEST_PROMPT"),
@ -71,7 +72,7 @@ class CompletionRequestProviderTest : IntegrationTest() {
fun testChatCompletionRequestRetry() {
useOpenAIService()
ConfigurationSettings.getCurrentState().systemPrompt = "TEST_SYSTEM_PROMPT"
service<PersonaSettings>().state.selectedPersona.instructions = "TEST_SYSTEM_PROMPT"
val conversation = ConversationService.getInstance().startConversation()
val firstMessage = createDummyMessage("FIRST_TEST_PROMPT", 500)
val secondMessage = createDummyMessage("SECOND_TEST_PROMPT", 250)
@ -97,8 +98,7 @@ class CompletionRequestProviderTest : IntegrationTest() {
}
fun testReducedChatCompletionRequest() {
useOpenAIService()
ConfigurationSettings.getCurrentState().systemPrompt = COMPLETION_SYSTEM_PROMPT
service<PersonaSettings>().state.selectedPersona.instructions = DEFAULT_PROMPT
val conversation = ConversationService.getInstance().startConversation()
conversation.addMessage(createDummyMessage(50))
conversation.addMessage(createDummyMessage(100))
@ -120,7 +120,7 @@ class CompletionRequestProviderTest : IntegrationTest() {
assertThat(request.messages)
.extracting("role", "content")
.containsExactly(
Tuple.tuple("system", COMPLETION_SYSTEM_PROMPT),
Tuple.tuple("system", DEFAULT_PROMPT),
Tuple.tuple("user", "TEST_PROMPT"),
Tuple.tuple("assistant", remainingMessage.response),
Tuple.tuple("user", "TEST_CHAT_COMPLETION_PROMPT"))

View file

@ -1,9 +1,11 @@
package ee.carlrobert.codegpt.completions
import com.intellij.openapi.components.service
import ee.carlrobert.codegpt.completions.llama.PromptTemplate.LLAMA
import ee.carlrobert.codegpt.conversations.ConversationService
import ee.carlrobert.codegpt.conversations.message.Message
import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings
import ee.carlrobert.codegpt.settings.persona.PersonaSettings
import ee.carlrobert.llm.client.http.RequestEntity
import ee.carlrobert.llm.client.http.exchange.NdJsonStreamHttpExchange
import ee.carlrobert.llm.client.http.exchange.StreamHttpExchange
@ -16,7 +18,7 @@ class DefaultCompletionRequestHandlerTest : IntegrationTest() {
fun testOpenAIChatCompletionCall() {
useOpenAIService()
ConfigurationSettings.getCurrentState().systemPrompt = "TEST_SYSTEM_PROMPT"
service<PersonaSettings>().state.selectedPersona.instructions = "TEST_SYSTEM_PROMPT"
val message = Message("TEST_PROMPT")
val conversation = ConversationService.getInstance().startConversation()
val requestHandler = CompletionRequestHandler(getRequestEventListener(message))
@ -47,7 +49,7 @@ class DefaultCompletionRequestHandlerTest : IntegrationTest() {
fun testAzureChatCompletionCall() {
useAzureService()
ConfigurationSettings.getCurrentState().systemPrompt = "TEST_SYSTEM_PROMPT"
service<PersonaSettings>().state.selectedPersona.instructions = "TEST_SYSTEM_PROMPT"
val conversationService = ConversationService.getInstance()
val prevMessage = Message("TEST_PREV_PROMPT")
prevMessage.response = "TEST_PREV_RESPONSE"
@ -85,7 +87,7 @@ class DefaultCompletionRequestHandlerTest : IntegrationTest() {
fun testLlamaChatCompletionCall() {
useLlamaService()
ConfigurationSettings.getCurrentState().maxTokens = 99
ConfigurationSettings.getCurrentState().systemPrompt = "TEST_SYSTEM_PROMPT"
service<PersonaSettings>().state.selectedPersona.instructions = "TEST_SYSTEM_PROMPT"
val message = Message("TEST_PROMPT")
val conversation = ConversationService.getInstance().startConversation()
conversation.addMessage(Message("Ping", "Pong"))
@ -120,7 +122,7 @@ class DefaultCompletionRequestHandlerTest : IntegrationTest() {
fun testOllamaChatCompletionCall() {
useOllamaService()
ConfigurationSettings.getCurrentState().maxTokens = 99
ConfigurationSettings.getCurrentState().systemPrompt = "TEST_SYSTEM_PROMPT"
service<PersonaSettings>().state.selectedPersona.instructions = "TEST_SYSTEM_PROMPT"
val message = Message("TEST_PROMPT")
val conversation = ConversationService.getInstance().startConversation()
val requestHandler = CompletionRequestHandler(getRequestEventListener(message))
@ -156,7 +158,7 @@ class DefaultCompletionRequestHandlerTest : IntegrationTest() {
fun testGoogleChatCompletionCall() {
useGoogleService()
ConfigurationSettings.getCurrentState().systemPrompt = "TEST_SYSTEM_PROMPT"
service<PersonaSettings>().state.selectedPersona.instructions = "TEST_SYSTEM_PROMPT"
val message = Message("TEST_PROMPT")
val conversation = ConversationService.getInstance().startConversation()
val requestHandler = CompletionRequestHandler(getRequestEventListener(message))
@ -192,7 +194,7 @@ class DefaultCompletionRequestHandlerTest : IntegrationTest() {
fun testCodeGPTServiceChatCompletionCall() {
useCodeGPTService()
ConfigurationSettings.getCurrentState().systemPrompt = "TEST_SYSTEM_PROMPT"
service<PersonaSettings>().state.selectedPersona.instructions = "TEST_SYSTEM_PROMPT"
val message = Message("TEST_PROMPT")
val conversation = ConversationService.getInstance().startConversation()
val requestHandler = CompletionRequestHandler(getRequestEventListener(message))

View file

@ -1,5 +1,6 @@
package ee.carlrobert.codegpt.toolwindow.chat
import com.intellij.openapi.components.service
import ee.carlrobert.codegpt.CodeGPTKeys
import ee.carlrobert.codegpt.EncodingManager
import ee.carlrobert.codegpt.ReferencedFile
@ -10,6 +11,7 @@ import ee.carlrobert.codegpt.completions.llama.PromptTemplate.LLAMA
import ee.carlrobert.codegpt.conversations.ConversationService
import ee.carlrobert.codegpt.conversations.message.Message
import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings
import ee.carlrobert.codegpt.settings.persona.PersonaSettings
import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings
import ee.carlrobert.llm.client.http.RequestEntity
import ee.carlrobert.llm.client.http.exchange.StreamHttpExchange
@ -30,7 +32,7 @@ class ChatToolWindowTabPanelTest : IntegrationTest() {
fun testSendingOpenAIMessage() {
useOpenAIService()
ConfigurationSettings.getCurrentState().systemPrompt = "TEST_SYSTEM_PROMPT"
service<PersonaSettings>().state.selectedPersona.instructions = "TEST_SYSTEM_PROMPT"
val message = Message("Hello!")
val conversation = ConversationService.getInstance().startConversation()
val panel = ChatToolWindowTabPanel(project, conversation)
@ -92,7 +94,7 @@ class ChatToolWindowTabPanelTest : IntegrationTest() {
ReferencedFile("TEST_FILE_NAME_2", "TEST_FILE_PATH_2", "TEST_FILE_CONTENT_2"),
ReferencedFile("TEST_FILE_NAME_3", "TEST_FILE_PATH_3", "TEST_FILE_CONTENT_3")))
useOpenAIService()
ConfigurationSettings.getCurrentState().systemPrompt = "TEST_SYSTEM_PROMPT"
service<PersonaSettings>().state.selectedPersona.instructions = "TEST_SYSTEM_PROMPT"
val message = Message("TEST_MESSAGE")
message.userMessage = "TEST_MESSAGE"
message.referencedFilePaths = listOf("TEST_FILE_PATH_1", "TEST_FILE_PATH_2", "TEST_FILE_PATH_3")
@ -179,7 +181,7 @@ class ChatToolWindowTabPanelTest : IntegrationTest() {
val testImagePath = Objects.requireNonNull(javaClass.getResource("/images/test-image.png")).path
project.putUserData(CodeGPTKeys.IMAGE_ATTACHMENT_FILE_PATH, testImagePath)
useOpenAIService("gpt-4-vision-preview")
ConfigurationSettings.getCurrentState().systemPrompt = "TEST_SYSTEM_PROMPT"
service<PersonaSettings>().state.selectedPersona.instructions = "TEST_SYSTEM_PROMPT"
val message = Message("TEST_MESSAGE")
val conversation = ConversationService.getInstance().startConversation()
val panel = ChatToolWindowTabPanel(project, conversation)
@ -255,7 +257,7 @@ class ChatToolWindowTabPanelTest : IntegrationTest() {
ReferencedFile("TEST_FILE_NAME_2", "TEST_FILE_PATH_2", "TEST_FILE_CONTENT_2"),
ReferencedFile("TEST_FILE_NAME_3", "TEST_FILE_PATH_3", "TEST_FILE_CONTENT_3")))
useOpenAIService()
ConfigurationSettings.getCurrentState().systemPrompt = "TEST_SYSTEM_PROMPT"
service<PersonaSettings>().state.selectedPersona.instructions = "TEST_SYSTEM_PROMPT"
val message = Message("TEST_MESSAGE")
message.userMessage = "TEST_MESSAGE"
message.referencedFilePaths = listOf("TEST_FILE_PATH_1", "TEST_FILE_PATH_2", "TEST_FILE_PATH_3")
@ -341,7 +343,7 @@ class ChatToolWindowTabPanelTest : IntegrationTest() {
fun testSendingLlamaMessage() {
useLlamaService()
val configurationState = ConfigurationSettings.getCurrentState()
configurationState.systemPrompt = "TEST_SYSTEM_PROMPT"
service<PersonaSettings>().state.selectedPersona.instructions = "TEST_SYSTEM_PROMPT"
configurationState.maxTokens = 1000
configurationState.temperature = 0.1
val llamaSettings = LlamaSettings.getCurrentState()