diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 95500085..89d07c80 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -11,11 +11,11 @@ updates: interval: "daily" - package-ecosystem: "gradle" - directory: "/buildSrc" + directory: "/buildSrc/src/main/kotlin" # /buildSrc and /codegpt-telemetry use only references schedule: interval: "daily" - package-ecosystem: "gradle" - directory: "/codegpt-core" + directory: "/codegpt-treesitter" schedule: interval: "daily" diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ae98a6d..b5ebbcd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,10 +6,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [2.6.0-233] - 2024-04-07 + +### Added + +- Vision support (image understanding) for OpenAI GPT-4 and Anthropic Claude models +- Total token panel for all providers +- Support for configuring code completions via settings +- Autofocus for UserTextArea when the tool window is visible + +### Fixed + +- Git commit message generation +- Fixed several UI/UX issues related to code completions for IDE versions starting from 233 +- Error when adding a single file to the context +- Several IntelliJ platform warnings + ### Removed - Azure custom configuration (use OpenAI-compatible service to override the default configuration) +### Changed + +- Supported minimum IDE build from 213 to 222 + ## [2.5.1] - 2024-03-14 ### Added @@ -396,7 +416,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `OPENAI_API_KEY` persistence, key is saved in the OS password safe from now on -[Unreleased]: https://github.com/carlrobertoh/CodeGPT/compare/v2.5.1...HEAD +[Unreleased]: https://github.com/carlrobertoh/CodeGPT/compare/v2.6.0-233...HEAD +[2.6.0-233]: https://github.com/carlrobertoh/CodeGPT/compare/v2.5.1...v2.6.0-233 [2.5.1]: https://github.com/carlrobertoh/CodeGPT/compare/v2.5.0...v2.5.1 [2.5.0]: https://github.com/carlrobertoh/CodeGPT/compare/v2.4.0...v2.5.0 [2.4.0]: https://github.com/carlrobertoh/CodeGPT/compare/v2.3.1...v2.4.0 diff --git a/DESCRIPTION.md b/DESCRIPTION.md index 05ef6fdb..23b201ef 100644 --- a/DESCRIPTION.md +++ b/DESCRIPTION.md @@ -2,104 +2,68 @@ ## Introducing CodeGPT: Your Free, Open-Source AI Copilot for Coding -CodeGPT is your go-to AI assistant, designed to enhance your coding skills and optimize your programming time. -Access state-of-the-art LLMs like GPT-4, Claude 3, Code LLama and more, all for free. +CodeGPT is your go-to AI coding assistant, offering assistance throughout your entire software development journey while keeping privacy in mind. Access state-of-the-art large language models from leading providers such as OpenAI, Anthropic, Azure, Mistral, and others, or connect to a locally hosted model for a completely offline and transparent development experience. -## Quick Start Guide +## Core Features -1. **Download the Plugin** +Leveraging large language models, CodeGPT offers a wide range of features to enhance your coding experience, including, but not limited to: -2. **Choose Your Preferred Service** +### Code Completions - a) **OpenAI** - Requires authentication via OpenAI API key. +Receive single-line or whole-function autocomplete suggestions as you type. - b) **Custom OpenAI-compatible service** - Choose between multiple different providers, such as Together, Anyscale, Groq, Ollama and many more. - - c) **Anthropic** - Requires authentication via API key. +![Code Completions](https://github.com/carlrobertoh/CodeGPT-docs/blob/main/images/new/inline-completion.png?raw=true) - d) **Azure** - Requires authentication via Active Directory or API key. +> Completions are currently supported only for OpenAI GPT-3.5 and locally hosted models. - e) **You.com** - A free, web-connected service with an optional upgrade to You⚡Pro for enhanced features. +### Chat (with Vision) - f) **LLaMA C/C++ Port** - Run Code Llama, WizardCoder, Deepseek Coder, and other state-of-the-art models locally for free. +Get instant coding advice through a ChatGPT-like interface. Ask questions, seek explanations, or get guidance on your projects without leaving your IDE. -3. **Start Using the Features** +CodeGPT also supports vision models and image understanding, allowing you to attach images for more context-aware assistance. It can detect new screenshots automatically, saving you time by eliminating the need to manually upload images each time you take a screenshot. -### OpenAI +![Chat with Vision](https://github.com/carlrobertoh/CodeGPT-docs/blob/main/images/new/chat-interface.png?raw=true) -After successful installation, configure your API key. Navigate to the plugin's settings via **File | Settings/Preferences | Tools | CodeGPT**. Paste your OpenAI API key into the field and click `Apply/OK`. +### Commit Message Generation -### Azure OpenAI +CodeGPT can generate meaningful commit messages based on the changes made in your codebase. It analyzes the diff of your staged changes and suggests concise and descriptive commit messages, saving you time and effort. -For Azure OpenAI services, you'll need to input three additional fields: +![Commit Message Generation](https://github.com/carlrobertoh/CodeGPT-docs/blob/main/images/new/generate-commit-message.png?raw=true) -- **Resource name**: The name of your Azure OpenAI Cognitive Services. -- **Deployment ID**: The name of your Deployment. -- **API version**: The most recent non-preview version. +### Reference Files -Also, input one of the two provided API keys. +CodeGPT allows you to reference specific files or documentation during your chat sessions, ensuring that responses are always relevant and accurate. -### You.com (Free) +![Reference Files](https://github.com/carlrobertoh/CodeGPT-docs/blob/main/images/new/reference-files-modal.png?raw=true) -**You**.com is a search engine that summarizes the best parts of the internet for **you**, with private ads and with privacy options. +### Name Suggestions -**You⚡Pro** +Stuck on naming a method or variable? CodeGPT offers context-aware suggestions, helping you adhere to best practices and maintain readability in your codebase. -Use the **CodeGPT** coupon for a free month of unlimited GPT-4 usage. +![Name Suggestions](https://github.com/carlrobertoh/CodeGPT-docs/blob/main/images/method-name-suggestions.png?raw=true) -Check out the full [feature list](https://about.you.com/hc/youpro/what-features-are-included-in-youpro/) for more details. +### Offline Development Support -### LLaMA C/C++ Port (Free, Local) +CodeGPT supports a completely offline development workflow by allowing you to connect to a locally hosted language model. This ensures that your code and data remain private and secure within your local environment, eliminating the need for an internet connection or sharing sensitive information with third-party servers. -> **Note**: This feature is currently supported only on Linux and MacOS. +![Offline Development Support](https://github.com/carlrobertoh/CodeGPT-docs/blob/main/images/new/llama-settings.png?raw=true) -The main goal of `llama.cpp` is to run the LLaMA model using 4-bit integer quantization on a MacBook. +## Privacy -#### Getting Started +**Your data stays yours.** CodeGPT **does not** collect or store any kind of sensitive information. -1. **Select the Model**: Depending on your hardware capabilities, choose the appropriate model from the provided list. Once selected, click on the `Download Model` link. A progress bar will appear, indicating the download process. +However, with users' consent, we do collect anonymous usage data, which we use to understand how users interact with the extension, including the most-used features and preferred providers. -2. **Start the Server**: After successfully downloading the model, initiate the server by clicking on the `Start Server` button. A status message will be displayed, indicating that the server is starting up. +## License -3. **Apply Settings**: With the server running, you can now apply the settings to start using the features. Click on the `Apply/OK` button to save your settings and start using the application. +CodeGPT's code is open source under the Apache License 2.0. -animated +## Feedback -> **Note**: If you're already running a server and wish to configure the plugin against that, then simply select the port and click `Apply/OK`. +Your input helps us grow. Reach out through: -## Features - -The plugin provides several key features, such as: - -### Chat with AI - -Ask anything you'd like. - -animated - -### Select and Ask - -Ask anything related to your selected code. - -animated - -### Replace Generated Code - -Instantly replace a selected code block in the editor with suggested code generated by AI. - -animated - -### Regenerate Response - -Expected a different answer? Re-generate any response of your choosing. - -animated - -## Other features - -- **Conversation History** - View recent conversation history and restore previous sessions, making it easy to pick up where you left off -- **Concurrent conversations** - Chat with the AI in multiple tabs simultaneously -- **Seamless conversations** - Chat with the AI regardless of the maximum token limitations -- **Predefined Actions** - Create your own editor actions or override the existing ones, saving time rewriting the same prompt repeatedly +- [Issue Tracker](https://github.com/carlrobertoh/CodeGPT/issues) +- [Discord](https://discord.gg/8dTGGrwcnR) +- [Email](mailto:carlrobertoh@gmail.com) diff --git a/GETTING_STARTED.md b/GETTING_STARTED.md new file mode 100644 index 00000000..02558808 --- /dev/null +++ b/GETTING_STARTED.md @@ -0,0 +1,86 @@ +## Getting Started + +1. **Download the Plugin** + +2. **Choose Your Preferred Service**: + + a) **OpenAI** - Requires authentication via OpenAI API key. + + b) **Azure** - Requires authentication via Active Directory or API key. + + c) **Custom OpenAI-compatible service** - Choose between multiple different providers, such as Together, Anyscale, + Groq, Ollama and many more. + + d) **Anthropic** - Requires authentication via API key. + + e) **You.com** - A free, web-connected service with an optional upgrade to You⚡Pro for enhanced features. + + f) **LLaMA C/C++ Port** - Recommended to have a decent computer to handle the computational requirements of running + inference. + > **Note**: Currently supported only on Linux and MacOS. + +3. **Start Using the Features** + +### Installation + +The plugin is available from [JetBrains Marketplace][plugin-repo]. +You can install it directly from your IDE via the `File | Settings/Preferences | Plugins` screen. +On the `Marketplace` tab simply search for `codegpt` and select the `CodeGPT` suggestion: + +![marketplace][marketplace-img] + +### OpenAI + +After successful installation, configure your API key. Navigate to the plugin's settings via **File | +Settings/Preferences | Tools | CodeGPT**. Paste your OpenAI API key into the field and click `Apply/OK`. + +### Azure OpenAI + +For Azure OpenAI services, you'll need to input three additional fields: + +- **Resource name**: The name of your Azure OpenAI Cognitive Services. It's the first part of the url you're provided to + use the service: "https://**my-resource-name**.openai.azure.com/". You can find it in your Azure Cognitive Services + page, under `Resource Management` → `Resource Management` → `Keys and Endpoints`. +- **Deployment ID**: The name of your Deployment. You can find it in the Azure AI Studio, + under `Management` → `Deployment` → `Deployment Name` column in the table. +- **API version**: The most recent non-preview version. + +In addition to these, you need to input one of the two API Keys provided, found along with the `Resource Name`. + +### You.com (Free) + +**You.com** is a search engine that summarizes the best parts of the internet for **you**, with private ads and with +privacy options. + +**You⚡Pro** + +Use the **CodeGPT** coupon for a free month of unlimited GPT-4 usage. + +Check out the full [feature list](https://about.you.com/hc/youpro/what-features-are-included-in-youpro/) for more +details. + +### LLaMA C/C++ Port (Free, Local) + +> **Note**: Currently supported only on Linux and MacOS. + +The main goal of `llama.cpp` is to run the LLaMA model using 4-bit integer quantization on a MacBook. + +#### Getting Started + +1. **Select the Model**: Depending on your hardware capabilities, choose the appropriate model from the provided list. + Once selected, click on the `Download Model` link. A progress bar will appear, indicating the download process. + +2. **Start the Server**: After successfully downloading the model, initiate the server by clicking on the `Start Server` + button. A status message will be displayed, indicating that the server is starting up. + +3. **Apply Settings**: With the server running, you can now apply the settings to start using the features. Click on + the `Apply/OK` button to save your settings and start using the application. + +animated + +> **Note**: If you're already running a server and wish to configure the plugin against that, then simply select the +> port and click `Apply/OK`. + +[marketplace-img]: https://github.com/carlrobertoh/CodeGPT-docs/blob/main/images/marketplace.png?raw=true + +[plugin-repo]: https://plugins.jetbrains.com/plugin/21056-codegpt diff --git a/README.md b/README.md index eddfa98e..8db07bdd 100644 --- a/README.md +++ b/README.md @@ -38,122 +38,53 @@ ## About The Project -This is an extension for JetBrains IDEs that integrates AI into your coding environment. -By leveraging the power of Large Language Models (LLMs), this makes it an invaluable tool for developers looking to streamline their workflow and gain a deeper understanding of the code they're working on. +CodeGPT is your go-to AI coding assistant, offering assistance throughout your entire software development journey while keeping privacy in mind. Access state-of-the-art large language models from leading providers such as OpenAI, Anthropic, Azure, Mistral, and others, or connect to a locally hosted model for a completely offline and transparent development experience. -## Features +## Core Features -The plugin provides several key features, such as: +Leveraging large language models, CodeGPT offers a wide range of features to enhance your coding experience, including, but not limited to: -### Chat with AI +### Code Completions -

- animated -

+Receive single-line or whole-function autocomplete suggestions as you type. -### Chat With Multiple Files +![Code Completions](https://github.com/carlrobertoh/CodeGPT-docs/blob/main/images/new/inline-completion.png?raw=true) -

- animated -

+> **Note**: Currently supported only on GPT-3.5 and locally-hosted models. -### Choose Between Different Providers +### Chat (with Vision) -

- -

+Get instant coding advice through a ChatGPT-like interface. Ask questions, seek explanations, or get guidance on your projects without leaving your IDE. -### Method Name Suggestions +CodeGPT also supports vision models and image understanding, allowing you to attach images for more context-aware assistance. It can detect new screenshots automatically, saving you time by eliminating the need to manually upload images each time you take a screenshot. -

- -

+![Chat with Vision](https://github.com/carlrobertoh/CodeGPT-docs/blob/main/images/new/chat-interface.png?raw=true) -### Generate Commit Messages +### Commit Message Generation -

- -

+CodeGPT can generate meaningful commit messages based on the changes made in your codebase. It analyzes the diff of your staged changes and suggests concise and descriptive commit messages, saving you time and effort. -### Other features +![Commit Message Generation](https://github.com/carlrobertoh/CodeGPT-docs/blob/main/images/new/generate-commit-message.png?raw=true) -- **Conversation History** - View recent conversation history and restore previous sessions, making it easy to pick up where you left off -- **Concurrent conversations** - Chat with AI in multiple tabs simultaneously -- **Seamless conversations** - Chat with AI regardless of the maximum token limitations -- **Predefined Prompts** - Create your own editor prompt or override the existing ones +### Reference Files -## Getting Started +CodeGPT allows you to reference specific files or documentation during your chat sessions, ensuring that responses are always relevant and accurate. -1. **Download the Plugin** +![Reference Files](https://github.com/carlrobertoh/CodeGPT-docs/blob/main/images/new/reference-files-modal.png?raw=true) -2. **Choose Your Preferred Service**: +### Name Suggestions - a) **OpenAI** - Requires authentication via OpenAI API key. +Stuck on naming a method or variable? CodeGPT offers context-aware suggestions, helping you adhere to best practices and maintain readability in your codebase. - b) **Azure** - Requires authentication via Active Directory or API key. +![Name Suggestions](https://github.com/carlrobertoh/CodeGPT-docs/blob/main/images/method-name-suggestions.png?raw=true) - c) **Custom OpenAI-compatible service** - Choose between multiple different providers, such as Together, Anyscale, Groq, Ollama and many more. +### Offline Development Support - d) **Anthropic** - Requires authentication via API key. +CodeGPT supports a completely offline development workflow by allowing you to connect to a locally hosted language model. This ensures that your code and data remain private and secure within your local environment, eliminating the need for an internet connection or sharing sensitive information with third-party servers. - e) **You.com** - A free, web-connected service with an optional upgrade to You⚡Pro for enhanced features. +![Offline Development Support](https://github.com/carlrobertoh/CodeGPT-docs/blob/main/images/new/llama-settings.png?raw=true) - f) **LLaMA C/C++ Port** - Recommended to have a decent computer to handle the computational requirements of running inference. - > **Note**: Currently supported only on Linux and MacOS. - -3. **Start Using the Features** - -### Installation - -The plugin is available from [JetBrains Marketplace][plugin-repo]. -You can install it directly from your IDE via the `File | Settings/Preferences | Plugins` screen. -On the `Marketplace` tab simply search for `codegpt` and select the `CodeGPT` suggestion: - -![marketplace][marketplace-img] - -### OpenAI - -After successful installation, configure your API key. Navigate to the plugin's settings via **File | Settings/Preferences | Tools | CodeGPT**. Paste your OpenAI API key into the field and click `Apply/OK`. - -### Azure OpenAI - -For Azure OpenAI services, you'll need to input three additional fields: - -- **Resource name**: The name of your Azure OpenAI Cognitive Services. It's the first part of the url you're provided to use the service: "https://**my-resource-name**.openai.azure.com/". You can find it in your Azure Cognitive Services page, under `Resource Management` → `Resource Management` → `Keys and Endpoints`. -- **Deployment ID**: The name of your Deployment. You can find it in the Azure AI Studio, under `Management` → `Deployment` → `Deployment Name` column in the table. -- **API version**: The most recent non-preview version. - -In addition to these, you need to input one of the two API Keys provided, found along with the `Resource Name`. - -### You.com (Free) - -**You.com** is a search engine that summarizes the best parts of the internet for **you**, with private ads and with privacy options. - -**You⚡Pro** - -Use the **CodeGPT** coupon for a free month of unlimited GPT-4 usage. - -Check out the full [feature list](https://about.you.com/hc/youpro/what-features-are-included-in-youpro/) for more details. - -### LLaMA C/C++ Port (Free, Local) - -> **Note**: Currently supported only on Linux and MacOS. - -The main goal of `llama.cpp` is to run the LLaMA model using 4-bit integer quantization on a MacBook. - -#### Getting Started - -1. **Select the Model**: Depending on your hardware capabilities, choose the appropriate model from the provided list. Once selected, click on the `Download Model` link. A progress bar will appear, indicating the download process. - -2. **Start the Server**: After successfully downloading the model, initiate the server by clicking on the `Start Server` button. A status message will be displayed, indicating that the server is starting up. - -3. **Apply Settings**: With the server running, you can now apply the settings to start using the features. Click on the `Apply/OK` button to save your settings and start using the application. - -animated - -> **Note**: If you're already running a server and wish to configure the plugin against that, then simply select the port and click `Apply/OK`. - -### Running locally +## Running locally **Linux or macOS** ```shell @@ -173,9 +104,19 @@ git submodule update tail -f build/idea-sandbox/system/log/idea.log ``` -## Issues +## Privacy -See the [open issues][open-issues] for a full list of proposed features (and known issues). +**Your data stays yours.** CodeGPT **does not** collect or store any kind of sensitive information. + +However, with users' consent, we do collect anonymous usage data, which we use to understand how users interact with the extension, including the most-used features and preferred providers. + +## Feedback + +Your input helps us grow. Reach out through: + +- [Issue Tracker](https://github.com/carlrobertoh/CodeGPT/issues) +- [Discord](https://discord.gg/8dTGGrwcnR) +- [Email](mailto:carlrobertoh@gmail.com) ## License @@ -184,7 +125,6 @@ Apache 2.0 © [Carl-Robert Linnupuu][portfolio] If you found this project interesting, kindly rate it on the marketplace and don't forget to give it a star. Thanks again!

(back to top)

- diff --git a/build.gradle.kts b/build.gradle.kts index d942d5a6..53825dc1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -23,12 +23,16 @@ fun environment(key: String) = providers.environmentVariable(key) plugins { id("codegpt.java-conventions") - id("org.jetbrains.changelog") version "2.2.0" + alias(libs.plugins.changelog) } group = properties("pluginGroup").get() version = properties("pluginVersion").get() + "-" + properties("pluginSinceBuild").get() +checkstyle { + toolVersion = libs.versions.checkstyle.get() +} + repositories { mavenCentral() gradlePluginPortal() @@ -50,17 +54,16 @@ dependencies { implementation(project(":codegpt-telemetry")) implementation(project(":codegpt-treesitter")) - implementation("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.16.1") - implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.16.2") - implementation("com.vladsch.flexmark:flexmark-all:0.64.8") { + implementation(enforcedPlatform(libs.jackson.bom)) + implementation("com.fasterxml.jackson.datatype:jackson-datatype-jdk8") + implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") + implementation(libs.flexmark.all) { // vulnerable transitive dependency exclude(group = "org.jsoup", module = "jsoup") } - implementation("org.jsoup:jsoup:1.17.2") - implementation("org.apache.commons:commons-text:1.11.0") - implementation("com.knuddels:jtokkit:1.0.0") - - testImplementation("org.awaitility:awaitility:4.2.0") + implementation(libs.jsoup) + implementation(libs.commons.text) + implementation(libs.jtokkit) } tasks.register("updateSubmodules") { @@ -156,4 +159,4 @@ tasks { showStandardStreams = true } } -} \ No newline at end of file +} diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index a7ce63c8..006b09c9 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -8,6 +8,6 @@ repositories { } dependencies { - implementation("org.jetbrains.intellij.plugins", "gradle-intellij-plugin", "1.17.2") - implementation("org.jetbrains.kotlin", "kotlin-gradle-plugin", "1.9.22") + implementation(libs.gradle.intellij.plugin) + implementation(libs.kotlin.gradle.plugin) } diff --git a/buildSrc/settings.gradle.kts b/buildSrc/settings.gradle.kts new file mode 100644 index 00000000..85123139 --- /dev/null +++ b/buildSrc/settings.gradle.kts @@ -0,0 +1,7 @@ +dependencyResolutionManagement { + versionCatalogs { + create("libs") { + from(files("../gradle/libs.versions.toml")) // Allow references + } + } +} diff --git a/buildSrc/src/main/kotlin/codegpt.java-conventions.gradle.kts b/buildSrc/src/main/kotlin/codegpt.java-conventions.gradle.kts index 4a2878aa..0a578abf 100644 --- a/buildSrc/src/main/kotlin/codegpt.java-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/codegpt.java-conventions.gradle.kts @@ -22,17 +22,18 @@ intellij { } checkstyle { - toolVersion = "10.12.5" + toolVersion = "10.15.0" } dependencies { - implementation("ee.carlrobert:llm-client:0.6.2") + implementation("ee.carlrobert:llm-client:0.7.0") + testImplementation(enforcedPlatform("org.junit:junit-bom:5.10.2")) testImplementation("org.assertj:assertj-core:3.25.3") - testImplementation("org.junit.jupiter:junit-jupiter-params:5.10.2") - testRuntimeOnly("org.junit.platform:junit-platform-launcher:1.10.2") - testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.2") - testRuntimeOnly("org.junit.vintage:junit-vintage-engine:5.10.2") + testImplementation("org.junit.jupiter:junit-jupiter-params") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") + testRuntimeOnly("org.junit.vintage:junit-vintage-engine") } tasks { diff --git a/codegpt-telemetry/build.gradle.kts b/codegpt-telemetry/build.gradle.kts index ca2c2a3a..60546d9e 100644 --- a/codegpt-telemetry/build.gradle.kts +++ b/codegpt-telemetry/build.gradle.kts @@ -3,5 +3,5 @@ plugins { } dependencies { - implementation("com.rudderstack.sdk.java.analytics:analytics:3.0.0") -} \ No newline at end of file + implementation(libs.analytics) +} diff --git a/codegpt-treesitter/build.gradle.kts b/codegpt-treesitter/build.gradle.kts index ed515253..532f50f5 100644 --- a/codegpt-treesitter/build.gradle.kts +++ b/codegpt-treesitter/build.gradle.kts @@ -3,7 +3,7 @@ plugins { } dependencies { - implementation("io.github.bonede:tree-sitter:0.21.0") + implementation(libs.tree.sitter) implementation("io.github.bonede:tree-sitter-erlang:0.1.0") implementation("io.github.bonede:tree-sitter-elixir:0.1.1") implementation("io.github.bonede:tree-sitter-dockerfile:0.1.2") @@ -37,4 +37,4 @@ dependencies { implementation("io.github.bonede:tree-sitter-php:0.20.0") implementation("io.github.bonede:tree-sitter-typescript:0.20.3") implementation("io.github.bonede:tree-sitter-query:0.1.0") -} \ No newline at end of file +} diff --git a/gradle.properties b/gradle.properties index b4935599..81081887 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ pluginGroup = ee.carlrobert pluginName = CodeGPT pluginRepositoryUrl = https://github.com/carlrobertoh/CodeGPT # SemVer format -> https://semver.org -pluginVersion = 2.5.1 +pluginVersion = 2.6.0 # Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html pluginSinceBuild = 241 @@ -21,7 +21,7 @@ platformPlugins = javaVersion = 17 # Gradle Releases -> https://github.com/gradle/gradle/releases -gradleVersion = 8.5 +gradleVersion = 8.7 # Opt-out flag for bundling Kotlin standard library -> https://jb.gg/intellij-platform-kotlin-stdlib kotlin.stdlib.default.dependency = false @@ -38,4 +38,4 @@ org.gradle.caching = true systemProp.org.gradle.unsafe.kotlin.assignment = true # Temporary workaround for Kotlin Compiler OutOfMemoryError -> https://jb.gg/intellij-platform-kotlin-oom -kotlin.incremental.useClasspathSnapshot = false \ No newline at end of file +kotlin.incremental.useClasspathSnapshot = false diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 00000000..13b69c36 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,29 @@ +[versions] +analytics = "3.0.0" +changelog = "2.2.0" +checkstyle = "10.15.0" +commons-text = "1.11.0" +flexmark = "0.64.8" +gradle-intellij-plugin-version = "1.17.3" +jackson = "2.17.0" +jsoup = "1.17.2" +jtokkit = "1.0.0" +junit = "5.10.2" +kotlin = "1.9.23" +llm-client = "0.7.0" +tree-sitter = "0.22.2" + +[libraries] +analytics = { module = "com.rudderstack.sdk.java.analytics:analytics", version.ref = "analytics" } +commons-text = { module = "org.apache.commons:commons-text", version.ref = "commons-text" } +flexmark-all = { module = "com.vladsch.flexmark:flexmark-all", version.ref = "flexmark" } +gradle-intellij-plugin = { module = "org.jetbrains.intellij.plugins:gradle-intellij-plugin", version.ref = "gradle-intellij-plugin-version" } +jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" } +jtokkit = { module = "com.knuddels:jtokkit", version.ref = "jtokkit" } +jackson-bom = { module = "com.fasterxml.jackson:jackson-bom", version.ref = "jackson" } +kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } +llm-client = { module = "ee.carlrobert:llm-client", version.ref = "llm-client" } +tree-sitter = { module = "io.github.bonede:tree-sitter", version.ref = "tree-sitter" } + +[plugins] +changelog = { id = "org.jetbrains.changelog", version.ref = "changelog" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7f93135c..d64cd491 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 1af9e093..b82aa23a 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index 0adc8e1a..1aa94a42 100755 --- a/gradlew +++ b/gradlew @@ -145,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -153,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -202,11 +202,11 @@ fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ diff --git a/src/main/java/ee/carlrobert/codegpt/CodeGPTKeys.java b/src/main/java/ee/carlrobert/codegpt/CodeGPTKeys.java index 3cf177f2..a96ade5b 100644 --- a/src/main/java/ee/carlrobert/codegpt/CodeGPTKeys.java +++ b/src/main/java/ee/carlrobert/codegpt/CodeGPTKeys.java @@ -9,4 +9,6 @@ public class CodeGPTKeys { Key.create("codegpt.editor.inlay.prev-value"); public static final Key> SELECTED_FILES = Key.create("codegpt.selectedFiles"); + public static final Key IMAGE_ATTACHMENT_FILE_PATH = + Key.create("codegpt.imageAttachmentFilePath"); } diff --git a/src/main/java/ee/carlrobert/codegpt/EncodingManager.java b/src/main/java/ee/carlrobert/codegpt/EncodingManager.java index 8e3962d5..46022bfa 100644 --- a/src/main/java/ee/carlrobert/codegpt/EncodingManager.java +++ b/src/main/java/ee/carlrobert/codegpt/EncodingManager.java @@ -9,7 +9,10 @@ import com.knuddels.jtokkit.api.EncodingRegistry; import com.knuddels.jtokkit.api.EncodingType; import com.knuddels.jtokkit.api.IntArrayList; import ee.carlrobert.codegpt.conversations.Conversation; +import ee.carlrobert.llm.client.openai.completion.request.OpenAIChatCompletionDetailedMessage; import ee.carlrobert.llm.client.openai.completion.request.OpenAIChatCompletionMessage; +import ee.carlrobert.llm.client.openai.completion.request.OpenAIChatCompletionStandardMessage; +import ee.carlrobert.llm.client.openai.completion.request.OpenAIMessageTextContent; import java.util.List; @Service @@ -38,7 +41,16 @@ public final class EncodingManager { } public int countMessageTokens(OpenAIChatCompletionMessage message) { - return countMessageTokens(message.getRole(), message.getContent()); + if (message instanceof OpenAIChatCompletionStandardMessage standardMessage) { + return countMessageTokens(standardMessage.getRole(), standardMessage.getContent()); + } + + return ((OpenAIChatCompletionDetailedMessage) message).getContent().stream() + .filter(it -> it instanceof OpenAIMessageTextContent) + .map(it -> countMessageTokens( + ((OpenAIChatCompletionDetailedMessage) message).getRole(), + ((OpenAIMessageTextContent) it).getText())) + .reduce(0, Integer::sum); } public int countMessageTokens(String role, String content) { diff --git a/src/main/java/ee/carlrobert/codegpt/Icons.java b/src/main/java/ee/carlrobert/codegpt/Icons.java index d7aae08b..6dbeaf12 100644 --- a/src/main/java/ee/carlrobert/codegpt/Icons.java +++ b/src/main/java/ee/carlrobert/codegpt/Icons.java @@ -17,4 +17,5 @@ public final class Icons { public static final Icon You = IconLoader.getIcon("/icons/you.svg", Icons.class); public static final Icon YouSmall = IconLoader.getIcon("/icons/you_small.png", Icons.class); public static final Icon User = IconLoader.getIcon("/icons/user.svg", Icons.class); + public static final Icon Upload = IconLoader.getIcon("/icons/upload.svg", Icons.class); } diff --git a/src/main/java/ee/carlrobert/codegpt/ProjectCompilationStatusListener.java b/src/main/java/ee/carlrobert/codegpt/ProjectCompilationStatusListener.java index 73d292c2..1563d497 100644 --- a/src/main/java/ee/carlrobert/codegpt/ProjectCompilationStatusListener.java +++ b/src/main/java/ee/carlrobert/codegpt/ProjectCompilationStatusListener.java @@ -16,7 +16,7 @@ import com.intellij.openapi.project.Project; import ee.carlrobert.codegpt.completions.CompletionRequestProvider; import ee.carlrobert.codegpt.conversations.message.Message; import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings; -import ee.carlrobert.codegpt.toolwindow.chat.standard.StandardChatToolWindowContentManager; +import ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowContentManager; import ee.carlrobert.codegpt.ui.OverlayUtil; import java.io.File; import java.util.ArrayList; @@ -50,10 +50,10 @@ public class ProjectCompilationStatusListener implements CompilationStatusListen NotificationType.INFORMATION) .addAction(NotificationAction.createSimpleExpiring( CodeGPTBundle.get("notification.compilationError.okLabel"), - () -> project.getService(StandardChatToolWindowContentManager.class) + () -> project.getService(ChatToolWindowContentManager.class) .sendMessage(getMultiFileMessage(compileContext), FIX_COMPILE_ERRORS))) .addAction(NotificationAction.createSimpleExpiring( - CodeGPTBundle.get("checkForUpdatesTask.notification.hideButton"), + CodeGPTBundle.get("shared.notification.doNotShowAgain"), () -> ConfigurationSettings.getCurrentState().setCaptureCompileErrors(false))) .notify(project); } diff --git a/src/main/java/ee/carlrobert/codegpt/actions/CodeCompletionEnabledListener.java b/src/main/java/ee/carlrobert/codegpt/actions/CodeCompletionEnabledListener.java deleted file mode 100644 index 1e8a855c..00000000 --- a/src/main/java/ee/carlrobert/codegpt/actions/CodeCompletionEnabledListener.java +++ /dev/null @@ -1,26 +0,0 @@ -package ee.carlrobert.codegpt.actions; - -import com.intellij.util.messages.Topic; -import com.intellij.util.messages.Topic.BroadcastDirection; -import ee.carlrobert.codegpt.settings.configuration.ConfigurationState; -import java.util.EventListener; - -/** - * {@link EventListener} for changes of {@link ConfigurationState#isCodeCompletionsEnabled()}. - * - * @see EnableCompletionsAction - * @see DisableCompletionsAction - */ -public interface CodeCompletionEnabledListener extends EventListener { - - /** - * Topic for subscribing to {@link ConfigurationState#isCodeCompletionsEnabled()} changes.
- * Broadcasts from Application-Level to all projects. - */ - @Topic.AppLevel - Topic TOPIC = new Topic<>(CodeCompletionEnabledListener.class, - BroadcastDirection.TO_DIRECT_CHILDREN); - - void onCodeCompletionsEnabledChange(boolean codeCompletionsEnabled); -} - diff --git a/src/main/java/ee/carlrobert/codegpt/actions/DisableCompletionsAction.java b/src/main/java/ee/carlrobert/codegpt/actions/DisableCompletionsAction.java deleted file mode 100644 index 1b08fa0a..00000000 --- a/src/main/java/ee/carlrobert/codegpt/actions/DisableCompletionsAction.java +++ /dev/null @@ -1,41 +0,0 @@ -package ee.carlrobert.codegpt.actions; - -import static ee.carlrobert.codegpt.settings.service.ServiceType.LLAMA_CPP; -import static ee.carlrobert.codegpt.settings.service.ServiceType.OPENAI; - -import com.intellij.openapi.actionSystem.ActionUpdateThread; -import com.intellij.openapi.actionSystem.AnAction; -import com.intellij.openapi.actionSystem.AnActionEvent; -import com.intellij.openapi.application.ApplicationManager; -import ee.carlrobert.codegpt.settings.GeneralSettings; -import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings; -import java.util.List; -import org.jetbrains.annotations.NotNull; - -/** - * Disables code-completion.
Publishes message to {@link CodeCompletionEnabledListener#TOPIC} - */ -public class DisableCompletionsAction extends AnAction { - - @Override - public void actionPerformed(@NotNull AnActionEvent e) { - ConfigurationSettings.getCurrentState().setCodeCompletionsEnabled(false); - ApplicationManager.getApplication() - .getMessageBus().syncPublisher(CodeCompletionEnabledListener.TOPIC) - .onCodeCompletionsEnabledChange(false); - } - - @Override - public void update(@NotNull AnActionEvent e) { - var selectedService = GeneralSettings.getCurrentState().getSelectedService(); - var codeCompletionEnabled = ConfigurationSettings.getCurrentState().isCodeCompletionsEnabled(); - e.getPresentation().setEnabled(codeCompletionEnabled); - e.getPresentation() - .setVisible(codeCompletionEnabled && List.of(OPENAI, LLAMA_CPP).contains(selectedService)); - } - - @Override - public @NotNull ActionUpdateThread getActionUpdateThread() { - return ActionUpdateThread.BGT; - } -} diff --git a/src/main/java/ee/carlrobert/codegpt/actions/EnableCompletionsAction.java b/src/main/java/ee/carlrobert/codegpt/actions/EnableCompletionsAction.java deleted file mode 100644 index e646e997..00000000 --- a/src/main/java/ee/carlrobert/codegpt/actions/EnableCompletionsAction.java +++ /dev/null @@ -1,41 +0,0 @@ -package ee.carlrobert.codegpt.actions; - -import static ee.carlrobert.codegpt.settings.service.ServiceType.LLAMA_CPP; -import static ee.carlrobert.codegpt.settings.service.ServiceType.OPENAI; - -import com.intellij.openapi.actionSystem.ActionUpdateThread; -import com.intellij.openapi.actionSystem.AnAction; -import com.intellij.openapi.actionSystem.AnActionEvent; -import com.intellij.openapi.application.ApplicationManager; -import ee.carlrobert.codegpt.settings.GeneralSettings; -import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings; -import java.util.List; -import org.jetbrains.annotations.NotNull; - -/** - * Enables code-completion.
Publishes message to {@link CodeCompletionEnabledListener#TOPIC} - */ -public class EnableCompletionsAction extends AnAction { - - @Override - public void actionPerformed(@NotNull AnActionEvent e) { - ConfigurationSettings.getCurrentState().setCodeCompletionsEnabled(true); - ApplicationManager.getApplication() - .getMessageBus().syncPublisher(CodeCompletionEnabledListener.TOPIC) - .onCodeCompletionsEnabledChange(true); - } - - @Override - public void update(@NotNull AnActionEvent e) { - var selectedService = GeneralSettings.getCurrentState().getSelectedService(); - var codeCompletionEnabled = ConfigurationSettings.getCurrentState().isCodeCompletionsEnabled(); - e.getPresentation().setEnabled(!codeCompletionEnabled); - e.getPresentation() - .setVisible(!codeCompletionEnabled && List.of(OPENAI, LLAMA_CPP).contains(selectedService)); - } - - @Override - public @NotNull ActionUpdateThread getActionUpdateThread() { - return ActionUpdateThread.BGT; - } -} diff --git a/src/main/java/ee/carlrobert/codegpt/actions/GenerateGitCommitMessageAction.java b/src/main/java/ee/carlrobert/codegpt/actions/GenerateGitCommitMessageAction.java index 0e5b07bf..9553a886 100644 --- a/src/main/java/ee/carlrobert/codegpt/actions/GenerateGitCommitMessageAction.java +++ b/src/main/java/ee/carlrobert/codegpt/actions/GenerateGitCommitMessageAction.java @@ -4,7 +4,6 @@ import static com.intellij.openapi.ui.Messages.OK; import static com.intellij.util.ObjectUtils.tryCast; import static ee.carlrobert.codegpt.settings.service.ServiceType.YOU; import static java.util.stream.Collectors.joining; -import static java.util.stream.Collectors.toList; import com.intellij.notification.Notification; import com.intellij.notification.NotificationType; @@ -20,11 +19,8 @@ import com.intellij.openapi.editor.ex.EditorEx; import com.intellij.openapi.project.Project; import com.intellij.openapi.vcs.FilePath; import com.intellij.openapi.vcs.VcsDataKeys; -import com.intellij.openapi.vcs.changes.Change; -import com.intellij.openapi.vcs.changes.ui.ChangesBrowserBase; -import com.intellij.openapi.vcs.changes.ui.CommitDialogChangesBrowser; import com.intellij.openapi.vcs.ui.CommitMessage; -import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.vcs.commit.CommitWorkflowUi; import ee.carlrobert.codegpt.CodeGPTBundle; import ee.carlrobert.codegpt.EncodingManager; import ee.carlrobert.codegpt.Icons; @@ -37,11 +33,13 @@ import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.AbstractMap; import java.util.ArrayList; import java.util.List; import java.util.Objects; -import java.util.function.Function; +import java.util.Optional; import java.util.stream.Stream; import okhttp3.sse.EventSource; import org.jetbrains.annotations.NotNull; @@ -61,19 +59,17 @@ public class GenerateGitCommitMessageAction extends AnAction { @Override public void update(@NotNull AnActionEvent event) { + var commitWorkflowUi = event.getData(VcsDataKeys.COMMIT_WORKFLOW_UI); var selectedService = GeneralSettings.getCurrentState().getSelectedService(); - if (selectedService == YOU) { + if (selectedService == YOU || commitWorkflowUi == null) { event.getPresentation().setVisible(false); return; } - var includedChangesFilePaths = getIncludedChangesFilePaths(event); - var includedUnversionedChangesFilePaths = getIncludedUnversionedFilePaths(event); - var filesSelected = - !includedChangesFilePaths.isEmpty() || !includedUnversionedChangesFilePaths.isEmpty(); var callAllowed = CompletionRequestService.isRequestAllowed( GeneralSettings.getCurrentState().getSelectedService()); - event.getPresentation().setEnabled(callAllowed && filesSelected); + event.getPresentation().setEnabled(callAllowed + && new CommitWorkflowChanges(commitWorkflowUi).isFilesSelected()); event.getPresentation().setText(CodeGPTBundle.get(callAllowed ? "action.generateCommitMessage.title" : "action.generateCommitMessage.missingCredentials")); @@ -86,11 +82,7 @@ public class GenerateGitCommitMessageAction extends AnAction { return; } - var gitDiff = getGitDiff( - project, - getIncludedChangesFilePaths(event), - getIncludedUnversionedFilePaths(event)); - + var gitDiff = getGitDiff(event, project); var tokenCount = encodingManager.countTokens(gitDiff); if (tokenCount > MAX_TOKEN_COUNT_WARNING && OverlayUtil.showTokenSoftLimitWarningDialog(tokenCount) != OK) { @@ -142,21 +134,48 @@ public class GenerateGitCommitMessageAction extends AnAction { return commitMessage != null ? commitMessage.getEditorField().getEditor() : null; } - private String getGitDiff( - Project project, - List includedChangesFilePaths, - List includedUnversionedFilePaths) { + private String getGitDiff(AnActionEvent event, Project project) { + var commitWorkflowUi = Optional.ofNullable(event.getData(VcsDataKeys.COMMIT_WORKFLOW_UI)) + .orElseThrow(() -> new IllegalStateException("Could not retrieve commit workflow ui.")); + var changes = new CommitWorkflowChanges(commitWorkflowUi); + var projectBasePath = project.getBasePath(); + var gitDiff = getGitDiff(projectBasePath, changes.getIncludedVersionedFilePaths(), false); + var stagedGitDiff = getGitDiff(projectBasePath, changes.getIncludedVersionedFilePaths(), true); + var newFilesContent = + getNewFilesDiff(projectBasePath, changes.getIncludedUnversionedFilePaths()); + return Stream.of( - new AbstractMap.SimpleEntry<>(includedChangesFilePaths, true), - new AbstractMap.SimpleEntry<>(includedUnversionedFilePaths, false)) - .filter(entry -> !entry.getKey().isEmpty()) - .map(entry -> { - var process = - createGitDiffProcess(project.getBasePath(), entry.getKey(), entry.getValue()); - return new BufferedReader(new InputStreamReader(process.getInputStream())) - .lines() - .collect(joining("\n")); + new AbstractMap.SimpleEntry<>("Git diff", gitDiff), + new AbstractMap.SimpleEntry<>("Staged git diff", stagedGitDiff), + new AbstractMap.SimpleEntry<>("New files", newFilesContent)) + .filter(entry -> !entry.getValue().isEmpty()) + .map(entry -> "%s:\n%s".formatted(entry.getKey(), entry.getValue())) + .collect(joining("\n\n")); + } + + private String getGitDiff(String projectPath, List filePaths, boolean cached) { + if (filePaths.isEmpty()) { + return ""; + } + + var process = createGitDiffProcess(projectPath, filePaths, cached); + return new BufferedReader(new InputStreamReader(process.getInputStream())) + .lines() + .collect(joining("\n")); + } + + private String getNewFilesDiff(String projectPath, List filePaths) { + return filePaths.stream() + .map(pathString -> { + var filePath = Path.of(pathString); + var relativePath = Path.of(projectPath).relativize(filePath); + try { + return "New file '" + relativePath + "' content:\n" + Files.readString(filePath); + } catch (IOException ignored) { + return null; + } }) + .filter(Objects::nonNull) .collect(joining("\n")); } @@ -178,29 +197,31 @@ public class GenerateGitCommitMessageAction extends AnAction { } } - private @NotNull List getFilePaths( - AnActionEvent event, - Function> extractor) { - var changesBrowserBase = event.getData(ChangesBrowserBase.DATA_KEY); - if (changesBrowserBase == null) { - return List.of(); + static class CommitWorkflowChanges { + + private final List includedVersionedFilePaths; + private final List includedUnversionedFilePaths; + + CommitWorkflowChanges(CommitWorkflowUi commitWorkflowUi) { + includedVersionedFilePaths = commitWorkflowUi.getIncludedChanges().stream() + .map(it -> it.getVirtualFile() == null ? null : it.getVirtualFile().getPath()) + .filter(Objects::nonNull) + .toList(); + includedUnversionedFilePaths = commitWorkflowUi.getIncludedUnversionedFiles().stream() + .map(FilePath::getPath) + .toList(); } - return extractor.apply((CommitDialogChangesBrowser) changesBrowserBase) - .map(obj -> obj instanceof Change - ? ((Change) obj).getVirtualFile() - : ((FilePath) obj).getVirtualFile()) - .filter(Objects::nonNull) - .map(VirtualFile::getPath) - .distinct() - .collect(toList()); - } + public List getIncludedVersionedFilePaths() { + return includedVersionedFilePaths; + } - private @NotNull List getIncludedChangesFilePaths(AnActionEvent event) { - return getFilePaths(event, browser -> browser.getIncludedChanges().stream()); - } + public List getIncludedUnversionedFilePaths() { + return includedUnversionedFilePaths; + } - private @NotNull List getIncludedUnversionedFilePaths(AnActionEvent event) { - return getFilePaths(event, browser -> browser.getIncludedUnversionedFiles().stream()); + public boolean isFilesSelected() { + return !includedVersionedFilePaths.isEmpty() || !includedUnversionedFilePaths.isEmpty(); + } } } diff --git a/src/main/java/ee/carlrobert/codegpt/actions/editor/AskAction.java b/src/main/java/ee/carlrobert/codegpt/actions/editor/AskAction.java index f0d5836d..b83f4b88 100644 --- a/src/main/java/ee/carlrobert/codegpt/actions/editor/AskAction.java +++ b/src/main/java/ee/carlrobert/codegpt/actions/editor/AskAction.java @@ -5,7 +5,7 @@ import com.intellij.openapi.actionSystem.AnAction; import com.intellij.openapi.actionSystem.AnActionEvent; import ee.carlrobert.codegpt.Icons; import ee.carlrobert.codegpt.conversations.ConversationsState; -import ee.carlrobert.codegpt.toolwindow.chat.standard.StandardChatToolWindowContentManager; +import ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowContentManager; import org.jetbrains.annotations.NotNull; public class AskAction extends AnAction { @@ -26,7 +26,7 @@ public class AskAction extends AnAction { if (project != null) { ConversationsState.getInstance().setCurrentConversation(null); var tabPanel = - project.getService(StandardChatToolWindowContentManager.class).createNewTabPanel(); + project.getService(ChatToolWindowContentManager.class).createNewTabPanel(); if (tabPanel != null) { tabPanel.displayLandingView(); } diff --git a/src/main/java/ee/carlrobert/codegpt/actions/editor/CustomPromptAction.java b/src/main/java/ee/carlrobert/codegpt/actions/editor/CustomPromptAction.java index f870f0b3..05db8729 100644 --- a/src/main/java/ee/carlrobert/codegpt/actions/editor/CustomPromptAction.java +++ b/src/main/java/ee/carlrobert/codegpt/actions/editor/CustomPromptAction.java @@ -11,7 +11,7 @@ import com.intellij.util.ui.FormBuilder; import com.intellij.util.ui.JBUI; import com.intellij.util.ui.UI; import ee.carlrobert.codegpt.conversations.message.Message; -import ee.carlrobert.codegpt.toolwindow.chat.standard.StandardChatToolWindowContentManager; +import ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowContentManager; import ee.carlrobert.codegpt.ui.UIUtil; import ee.carlrobert.codegpt.util.file.FileUtil; import java.awt.event.ActionEvent; @@ -42,7 +42,7 @@ public class CustomPromptAction extends BaseEditorAction { format("%s\n```%s\n%s\n```", previousUserPrompt, fileExtension, selectedText)); message.setUserMessage(previousUserPrompt); SwingUtilities.invokeLater(() -> - project.getService(StandardChatToolWindowContentManager.class).sendMessage(message)); + project.getService(ChatToolWindowContentManager.class).sendMessage(message)); } } } diff --git a/src/main/java/ee/carlrobert/codegpt/actions/editor/EditorActionsUtil.java b/src/main/java/ee/carlrobert/codegpt/actions/editor/EditorActionsUtil.java index 06d02345..5c242df7 100644 --- a/src/main/java/ee/carlrobert/codegpt/actions/editor/EditorActionsUtil.java +++ b/src/main/java/ee/carlrobert/codegpt/actions/editor/EditorActionsUtil.java @@ -14,7 +14,7 @@ import ee.carlrobert.codegpt.CodeGPTKeys; import ee.carlrobert.codegpt.ReferencedFile; import ee.carlrobert.codegpt.conversations.message.Message; import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings; -import ee.carlrobert.codegpt.toolwindow.chat.standard.StandardChatToolWindowContentManager; +import ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowContentManager; import ee.carlrobert.codegpt.util.file.FileUtil; import java.util.Collection; import java.util.LinkedHashMap; @@ -65,7 +65,7 @@ public class EditorActionsUtil { format("\n```%s\n%s\n```", fileExtension, selectedText))); message.setUserMessage(prompt.replace("{{selectedCode}}", "")); var toolWindowContentManager = - project.getService(StandardChatToolWindowContentManager.class); + project.getService(ChatToolWindowContentManager.class); toolWindowContentManager.getToolWindow().show(); message.setReferencedFilePaths( diff --git a/src/main/java/ee/carlrobert/codegpt/actions/toolwindow/DeleteAllConversationsAction.java b/src/main/java/ee/carlrobert/codegpt/actions/toolwindow/DeleteAllConversationsAction.java index d9f7d71f..6490853a 100644 --- a/src/main/java/ee/carlrobert/codegpt/actions/toolwindow/DeleteAllConversationsAction.java +++ b/src/main/java/ee/carlrobert/codegpt/actions/toolwindow/DeleteAllConversationsAction.java @@ -11,7 +11,7 @@ import ee.carlrobert.codegpt.actions.ActionType; import ee.carlrobert.codegpt.actions.editor.EditorActionsUtil; import ee.carlrobert.codegpt.conversations.ConversationService; import ee.carlrobert.codegpt.telemetry.TelemetryAction; -import ee.carlrobert.codegpt.toolwindow.chat.standard.StandardChatToolWindowContentManager; +import ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowContentManager; import org.jetbrains.annotations.NotNull; public class DeleteAllConversationsAction extends AnAction { @@ -44,7 +44,7 @@ public class DeleteAllConversationsAction extends AnAction { if (project != null) { try { ConversationService.getInstance().clearAll(); - project.getService(StandardChatToolWindowContentManager.class).resetAll(); + project.getService(ChatToolWindowContentManager.class).resetAll(); } finally { TelemetryAction.IDE_ACTION.createActionMessage() .property("action", ActionType.DELETE_ALL_CONVERSATIONS.name()) diff --git a/src/main/java/ee/carlrobert/codegpt/completions/CallParameters.java b/src/main/java/ee/carlrobert/codegpt/completions/CallParameters.java index 3123da64..68d8134d 100644 --- a/src/main/java/ee/carlrobert/codegpt/completions/CallParameters.java +++ b/src/main/java/ee/carlrobert/codegpt/completions/CallParameters.java @@ -2,6 +2,7 @@ package ee.carlrobert.codegpt.completions; import ee.carlrobert.codegpt.conversations.Conversation; import ee.carlrobert.codegpt.conversations.message.Message; +import org.jetbrains.annotations.Nullable; public class CallParameters { @@ -9,6 +10,8 @@ public class CallParameters { private final ConversationType conversationType; private final Message message; private final boolean retry; + private @Nullable String imageMediaType; + private byte[] imageData; public CallParameters(Conversation conversation, Message message) { this(conversation, ConversationType.DEFAULT, message, false); @@ -40,4 +43,20 @@ public class CallParameters { public boolean isRetry() { return retry; } + + public String getImageMediaType() { + return imageMediaType; + } + + public void setImageMediaType(String imageMediaType) { + this.imageMediaType = imageMediaType; + } + + public byte[] getImageData() { + return imageData; + } + + public void setImageData(byte[] imageData) { + this.imageData = imageData; + } } diff --git a/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestProvider.java b/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestProvider.java index d43109fb..35976cf1 100644 --- a/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestProvider.java +++ b/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestProvider.java @@ -30,15 +30,29 @@ import ee.carlrobert.codegpt.settings.service.openai.OpenAISettings; import ee.carlrobert.codegpt.settings.service.you.YouSettings; import ee.carlrobert.codegpt.telemetry.core.configuration.TelemetryConfiguration; import ee.carlrobert.codegpt.telemetry.core.service.UserId; +import ee.carlrobert.codegpt.util.file.FileUtil; +import ee.carlrobert.llm.client.anthropic.completion.ClaudeBase64Source; +import ee.carlrobert.llm.client.anthropic.completion.ClaudeCompletionDetailedMessage; +import ee.carlrobert.llm.client.anthropic.completion.ClaudeCompletionMessage; import ee.carlrobert.llm.client.anthropic.completion.ClaudeCompletionRequest; -import ee.carlrobert.llm.client.anthropic.completion.ClaudeCompletionRequestMessage; +import ee.carlrobert.llm.client.anthropic.completion.ClaudeCompletionStandardMessage; +import ee.carlrobert.llm.client.anthropic.completion.ClaudeMessageImageContent; +import ee.carlrobert.llm.client.anthropic.completion.ClaudeMessageTextContent; import ee.carlrobert.llm.client.llama.completion.LlamaCompletionRequest; import ee.carlrobert.llm.client.openai.completion.OpenAIChatCompletionModel; +import ee.carlrobert.llm.client.openai.completion.request.OpenAIChatCompletionDetailedMessage; import ee.carlrobert.llm.client.openai.completion.request.OpenAIChatCompletionMessage; import ee.carlrobert.llm.client.openai.completion.request.OpenAIChatCompletionRequest; +import ee.carlrobert.llm.client.openai.completion.request.OpenAIChatCompletionStandardMessage; +import ee.carlrobert.llm.client.openai.completion.request.OpenAIImageUrl; +import ee.carlrobert.llm.client.openai.completion.request.OpenAIMessageImageURLContent; +import ee.carlrobert.llm.client.openai.completion.request.OpenAIMessageTextContent; import ee.carlrobert.llm.client.you.completion.YouCompletionRequest; import ee.carlrobert.llm.client.you.completion.YouCompletionRequestMessage; +import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -91,9 +105,10 @@ public class CompletionRequestProvider { public static OpenAIChatCompletionRequest buildOpenAILookupCompletionRequest(String context) { return new OpenAIChatCompletionRequest.Builder( List.of( - new OpenAIChatCompletionMessage("system", + new OpenAIChatCompletionStandardMessage( + "system", getResourceContent("/prompts/method-name-generator.txt")), - new OpenAIChatCompletionMessage("user", context))) + new OpenAIChatCompletionStandardMessage("user", context))) .setModel(OpenAISettings.getCurrentState().getModel()) .setStream(false) .build(); @@ -103,8 +118,8 @@ public class CompletionRequestProvider { return buildCustomOpenAIChatCompletionRequest( CustomServiceSettings.getCurrentState(), List.of( - new OpenAIChatCompletionMessage("system", system), - new OpenAIChatCompletionMessage("user", context)), + new OpenAIChatCompletionStandardMessage("system", system), + new OpenAIChatCompletionStandardMessage("user", context)), true); } @@ -112,10 +127,10 @@ public class CompletionRequestProvider { return buildCustomOpenAIChatCompletionRequest( CustomServiceSettings.getCurrentState(), List.of( - new OpenAIChatCompletionMessage( + new OpenAIChatCompletionStandardMessage( "system", getResourceContent("/prompts/method-name-generator.txt")), - new OpenAIChatCompletionMessage("user", context)), + new OpenAIChatCompletionStandardMessage("user", context)), false); } @@ -201,10 +216,10 @@ public class CompletionRequestProvider { List messages, boolean streamRequest) { var requestBuilder = new Request.Builder().url(customConfiguration.getUrl().trim()); + var credential = CredentialsStore.INSTANCE.getCredential(CUSTOM_SERVICE_API_KEY); for (var entry : customConfiguration.getHeaders().entrySet()) { String value = entry.getValue(); - var credential = CredentialsStore.INSTANCE.getCredential(CUSTOM_SERVICE_API_KEY); - if (value.contains("$CUSTOM_SERVICE_API_KEY") && credential != null) { + if (credential != null && value.contains("$CUSTOM_SERVICE_API_KEY")) { value = value.replace("$CUSTOM_SERVICE_API_KEY", credential); } requestBuilder.addHeader(entry.getKey(), value); @@ -246,15 +261,25 @@ public class CompletionRequestProvider { request.setMaxTokens(configuration.getMaxTokens()); request.setStream(true); request.setSystem(COMPLETION_SYSTEM_PROMPT); - var messages = conversation.getMessages().stream() + List messages = conversation.getMessages().stream() .filter(prevMessage -> prevMessage.getResponse() != null && !prevMessage.getResponse().isEmpty()) .flatMap(prevMessage -> Stream.of( - new ClaudeCompletionRequestMessage("user", prevMessage.getPrompt()), - new ClaudeCompletionRequestMessage("assistant", prevMessage.getResponse()))) + new ClaudeCompletionStandardMessage("user", prevMessage.getPrompt()), + new ClaudeCompletionStandardMessage("assistant", prevMessage.getResponse()))) .collect(toList()); - messages.add( - new ClaudeCompletionRequestMessage("user", callParameters.getMessage().getPrompt())); + + if (callParameters.getImageMediaType() != null && callParameters.getImageData().length > 0) { + messages.add(new ClaudeCompletionDetailedMessage("user", + List.of( + new ClaudeMessageImageContent(new ClaudeBase64Source( + callParameters.getImageMediaType(), + callParameters.getImageData())), + new ClaudeMessageTextContent(callParameters.getMessage().getPrompt())))); + } else { + messages.add( + new ClaudeCompletionStandardMessage("user", callParameters.getMessage().getPrompt())); + } request.setMessages(messages); return request; } @@ -263,22 +288,48 @@ public class CompletionRequestProvider { var message = callParameters.getMessage(); var messages = new ArrayList(); if (callParameters.getConversationType() == ConversationType.DEFAULT) { - messages.add(new OpenAIChatCompletionMessage( + messages.add(new OpenAIChatCompletionStandardMessage( "system", ConfigurationSettings.getCurrentState().getSystemPrompt())); } if (callParameters.getConversationType() == ConversationType.FIX_COMPILE_ERRORS) { - messages.add(new OpenAIChatCompletionMessage("system", FIX_COMPILE_ERRORS_SYSTEM_PROMPT)); + messages.add( + new OpenAIChatCompletionStandardMessage("system", FIX_COMPILE_ERRORS_SYSTEM_PROMPT)); } for (var prevMessage : conversation.getMessages()) { if (callParameters.isRetry() && prevMessage.getId().equals(message.getId())) { break; } - messages.add(new OpenAIChatCompletionMessage("user", prevMessage.getPrompt())); - messages.add(new OpenAIChatCompletionMessage("assistant", prevMessage.getResponse())); + var prevMessageImageFilePath = prevMessage.getImageFilePath(); + if (prevMessageImageFilePath != null && !prevMessageImageFilePath.isEmpty()) { + try { + var imageFilePath = Path.of(prevMessageImageFilePath); + var imageData = Files.readAllBytes(imageFilePath); + var imageMediaType = FileUtil.getImageMediaType(imageFilePath.getFileName().toString()); + messages.add(new OpenAIChatCompletionDetailedMessage("user", + List.of( + new OpenAIMessageImageURLContent(new OpenAIImageUrl(imageMediaType, imageData)), + new OpenAIMessageTextContent(prevMessage.getPrompt())))); + } catch (IOException e) { + throw new RuntimeException(e); + } + } else { + messages.add(new OpenAIChatCompletionStandardMessage("user", prevMessage.getPrompt())); + } + messages.add(new OpenAIChatCompletionStandardMessage("assistant", prevMessage.getResponse())); + } + + if (callParameters.getImageMediaType() != null && callParameters.getImageData().length > 0) { + messages.add(new OpenAIChatCompletionDetailedMessage("user", + List.of( + new OpenAIMessageImageURLContent( + new OpenAIImageUrl(callParameters.getImageMediaType(), + callParameters.getImageData())), + new OpenAIMessageTextContent(message.getPrompt())))); + } else { + messages.add(new OpenAIChatCompletionStandardMessage("user", message.getPrompt())); } - messages.add(new OpenAIChatCompletionMessage("user", message.getPrompt())); return messages; } @@ -324,8 +375,11 @@ public class CompletionRequestProvider { break; } - totalUsage -= encodingManager.countMessageTokens(messages.get(i)); - messages.set(i, null); + var message = messages.get(i); + if (message instanceof OpenAIChatCompletionStandardMessage) { + totalUsage -= encodingManager.countMessageTokens(message); + messages.set(i, null); + } } return messages.stream().filter(Objects::nonNull).collect(toList()); diff --git a/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestService.java b/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestService.java index 5805921b..ba4608ee 100644 --- a/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestService.java +++ b/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestService.java @@ -26,11 +26,11 @@ import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings; import ee.carlrobert.codegpt.settings.service.openai.OpenAISettings; import ee.carlrobert.llm.client.DeserializationUtil; import ee.carlrobert.llm.client.anthropic.completion.ClaudeCompletionRequest; -import ee.carlrobert.llm.client.anthropic.completion.ClaudeCompletionRequestMessage; +import ee.carlrobert.llm.client.anthropic.completion.ClaudeCompletionStandardMessage; import ee.carlrobert.llm.client.llama.completion.LlamaCompletionRequest; import ee.carlrobert.llm.client.openai.completion.OpenAIChatCompletionEventSourceListener; -import ee.carlrobert.llm.client.openai.completion.request.OpenAIChatCompletionMessage; import ee.carlrobert.llm.client.openai.completion.request.OpenAIChatCompletionRequest; +import ee.carlrobert.llm.client.openai.completion.request.OpenAIChatCompletionStandardMessage; import ee.carlrobert.llm.client.openai.completion.response.OpenAIChatCompletionResponse; import ee.carlrobert.llm.completion.CompletionEventListener; import java.io.IOException; @@ -117,8 +117,8 @@ public final class CompletionRequestService { var configuration = ConfigurationSettings.getCurrentState(); var commitMessagePrompt = configuration.getCommitMessagePrompt(); var openaiRequest = new OpenAIChatCompletionRequest.Builder(List.of( - new OpenAIChatCompletionMessage("system", commitMessagePrompt), - new OpenAIChatCompletionMessage("user", prompt))) + new OpenAIChatCompletionStandardMessage("system", commitMessagePrompt), + new OpenAIChatCompletionStandardMessage("user", prompt))) .setModel(OpenAISettings.getCurrentState().getModel()) .build(); var selectedService = GeneralSettings.getCurrentState().getSelectedService(); @@ -142,8 +142,7 @@ public final class CompletionRequestService { claudeRequest.setStream(true); claudeRequest.setMaxTokens(configuration.getMaxTokens()); claudeRequest.setModel(anthropicSettings.getModel()); - claudeRequest.setMessages( - List.of(new ClaudeCompletionRequestMessage("user", prompt))); + claudeRequest.setMessages(List.of(new ClaudeCompletionStandardMessage("user", prompt))); CompletionClientProvider.getClaudeClient() .getCompletionAsync(claudeRequest, eventListener); break; diff --git a/src/main/java/ee/carlrobert/codegpt/completions/you/auth/YouAuthenticationService.java b/src/main/java/ee/carlrobert/codegpt/completions/you/auth/YouAuthenticationService.java index a7d195ed..f6e20b72 100644 --- a/src/main/java/ee/carlrobert/codegpt/completions/you/auth/YouAuthenticationService.java +++ b/src/main/java/ee/carlrobert/codegpt/completions/you/auth/YouAuthenticationService.java @@ -60,7 +60,6 @@ public final class YouAuthenticationService { if (response.code() == 200) { try { - var messageBus = ApplicationManager.getApplication().getMessageBus(); var userManager = YouUserManager.getInstance(); var authenticationResponse = @@ -72,6 +71,7 @@ public final class YouAuthenticationService { YouApiClient.getInstance().getSubscription(authenticationResponse); var subscribed = subscription != null && "youpro".equals(subscription.getService()); userManager.setSubscribed(subscribed); + var messageBus = ApplicationManager.getApplication().getMessageBus(); if (subscribed) { messageBus.syncPublisher(YouSubscriptionNotifier.SUBSCRIPTION_TOPIC).subscribed(); } diff --git a/src/main/java/ee/carlrobert/codegpt/conversations/message/Message.java b/src/main/java/ee/carlrobert/codegpt/conversations/message/Message.java index 5ff03f71..9b1c05b5 100644 --- a/src/main/java/ee/carlrobert/codegpt/conversations/message/Message.java +++ b/src/main/java/ee/carlrobert/codegpt/conversations/message/Message.java @@ -6,6 +6,7 @@ import ee.carlrobert.llm.client.you.completion.YouSerpResult; import java.util.List; import java.util.Objects; import java.util.UUID; +import org.jetbrains.annotations.Nullable; public class Message { @@ -15,6 +16,7 @@ public class Message { private String userMessage; private List serpResults; private List referencedFilePaths; + private @Nullable String imageFilePath; public Message(String prompt, String response) { this(prompt); @@ -71,6 +73,14 @@ public class Message { this.referencedFilePaths = referencedFilePaths; } + public @Nullable String getImageFilePath() { + return imageFilePath; + } + + public void setImageFilePath(@Nullable String imageFilePath) { + this.imageFilePath = imageFilePath; + } + @Override public boolean equals(Object obj) { if (obj == this) { diff --git a/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettingsConfigurable.java b/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettingsConfigurable.java index 11063a53..b9d781bc 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettingsConfigurable.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettingsConfigurable.java @@ -26,7 +26,7 @@ import ee.carlrobert.codegpt.settings.service.openai.OpenAISettingsForm; import ee.carlrobert.codegpt.settings.service.you.YouSettings; import ee.carlrobert.codegpt.settings.service.you.YouSettingsForm; import ee.carlrobert.codegpt.telemetry.TelemetryAction; -import ee.carlrobert.codegpt.toolwindow.chat.standard.StandardChatToolWindowContentManager; +import ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowContentManager; import ee.carlrobert.codegpt.util.ApplicationUtil; import javax.swing.JComponent; import org.jetbrains.annotations.Nls; @@ -160,6 +160,6 @@ public class GeneralSettingsConfigurable implements Configurable { throw new RuntimeException("Could not find current project."); } - project.getService(StandardChatToolWindowContentManager.class).resetAll(); + project.getService(ChatToolWindowContentManager.class).resetAll(); } } diff --git a/src/main/java/ee/carlrobert/codegpt/settings/configuration/ConfigurationComponent.java b/src/main/java/ee/carlrobert/codegpt/settings/configuration/ConfigurationComponent.java index 54aa2713..bfe6c0c7 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/configuration/ConfigurationComponent.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/configuration/ConfigurationComponent.java @@ -44,6 +44,7 @@ public class ConfigurationComponent { private final JPanel mainPanel; private final JBTable table; private final JBCheckBox checkForPluginUpdatesCheckBox; + private final JBCheckBox checkForNewScreenshotsCheckBox; private final JBCheckBox openNewTabCheckBox; private final JBCheckBox methodNameGenerationCheckBox; private final JBCheckBox autoFormattingCheckBox; @@ -111,6 +112,9 @@ public class ConfigurationComponent { checkForPluginUpdatesCheckBox = new JBCheckBox( CodeGPTBundle.get("configurationConfigurable.checkForPluginUpdates.label"), + configuration.isCheckForNewScreenshots()); + checkForNewScreenshotsCheckBox = new JBCheckBox( + CodeGPTBundle.get("configurationConfigurable.checkForNewScreenshots.label"), configuration.isCheckForPluginUpdates()); openNewTabCheckBox = new JBCheckBox( CodeGPTBundle.get("configurationConfigurable.openNewTabCheckBox.label"), @@ -126,6 +130,7 @@ public class ConfigurationComponent { .addComponent(tablePanel) .addVerticalGap(4) .addComponent(checkForPluginUpdatesCheckBox) + .addComponent(checkForNewScreenshotsCheckBox) .addComponent(openNewTabCheckBox) .addComponent(methodNameGenerationCheckBox) .addComponent(autoFormattingCheckBox) @@ -152,11 +157,10 @@ public class ConfigurationComponent { state.setSystemPrompt(systemPromptTextArea.getText()); state.setCommitMessagePrompt(commitMessagePromptTextArea.getText()); state.setCheckForPluginUpdates(checkForPluginUpdatesCheckBox.isSelected()); + state.setCheckForNewScreenshots(checkForNewScreenshotsCheckBox.isSelected()); state.setCreateNewChatOnEachAction(openNewTabCheckBox.isSelected()); state.setMethodNameGenerationEnabled(methodNameGenerationCheckBox.isSelected()); state.setAutoFormattingEnabled(autoFormattingCheckBox.isSelected()); - state.setCodeCompletionsEnabled( - ConfigurationSettings.getCurrentState().isCodeCompletionsEnabled()); return state; } @@ -168,6 +172,7 @@ public class ConfigurationComponent { systemPromptTextArea.setText(configuration.getSystemPrompt()); commitMessagePromptTextArea.setText(configuration.getCommitMessagePrompt()); checkForPluginUpdatesCheckBox.setSelected(configuration.isCheckForPluginUpdates()); + checkForNewScreenshotsCheckBox.setSelected(configuration.isCheckForNewScreenshots()); openNewTabCheckBox.setSelected(configuration.isCreateNewChatOnEachAction()); methodNameGenerationCheckBox.setSelected(configuration.isMethodNameGenerationEnabled()); autoFormattingCheckBox.setSelected(configuration.isAutoFormattingEnabled()); diff --git a/src/main/java/ee/carlrobert/codegpt/settings/configuration/ConfigurationState.java b/src/main/java/ee/carlrobert/codegpt/settings/configuration/ConfigurationState.java index 7ae70912..dbf9bb6e 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/configuration/ConfigurationState.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/configuration/ConfigurationState.java @@ -14,12 +14,12 @@ public class ConfigurationState { private int maxTokens = 1000; private double temperature = 0.1; private boolean checkForPluginUpdates = true; + private boolean checkForNewScreenshots = true; private boolean createNewChatOnEachAction; private boolean ignoreGitCommitTokenLimit; private boolean methodNameGenerationEnabled = true; private boolean captureCompileErrors = true; private boolean autoFormattingEnabled = true; - private boolean codeCompletionsEnabled; private Map tableData = EditorActionsUtil.DEFAULT_ACTIONS; public String getSystemPrompt() { @@ -62,6 +62,14 @@ public class ConfigurationState { this.createNewChatOnEachAction = createNewChatOnEachAction; } + public boolean isCheckForNewScreenshots() { + return checkForNewScreenshots; + } + + public void setCheckForNewScreenshots(boolean checkForNewScreenshots) { + this.checkForNewScreenshots = checkForNewScreenshots; + } + public Map getTableData() { return tableData; } @@ -110,14 +118,6 @@ public class ConfigurationState { this.autoFormattingEnabled = autoFormattingEnabled; } - public boolean isCodeCompletionsEnabled() { - return codeCompletionsEnabled; - } - - public void setCodeCompletionsEnabled(boolean codeCompletionsEnabled) { - this.codeCompletionsEnabled = codeCompletionsEnabled; - } - @Override public boolean equals(Object o) { if (this == o) { @@ -135,7 +135,6 @@ public class ConfigurationState { && methodNameGenerationEnabled == that.methodNameGenerationEnabled && captureCompileErrors == that.captureCompileErrors && autoFormattingEnabled == that.autoFormattingEnabled - && codeCompletionsEnabled == that.codeCompletionsEnabled && Objects.equals(systemPrompt, that.systemPrompt) && Objects.equals(commitMessagePrompt, that.commitMessagePrompt) && Objects.equals(tableData, that.tableData); @@ -145,7 +144,6 @@ public class ConfigurationState { public int hashCode() { return Objects.hash(systemPrompt, commitMessagePrompt, maxTokens, temperature, checkForPluginUpdates, createNewChatOnEachAction, ignoreGitCommitTokenLimit, - methodNameGenerationEnabled, captureCompileErrors, autoFormattingEnabled, - codeCompletionsEnabled, tableData); + methodNameGenerationEnabled, captureCompileErrors, autoFormattingEnabled, tableData); } } diff --git a/src/main/java/ee/carlrobert/codegpt/settings/service/llama/LlamaSettingsState.java b/src/main/java/ee/carlrobert/codegpt/settings/service/llama/LlamaSettingsState.java index c0c6de0b..cb81c722 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/service/llama/LlamaSettingsState.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/service/llama/LlamaSettingsState.java @@ -27,6 +27,8 @@ public class LlamaSettingsState { private double topP = 0.9; private double minP = 0.05; private double repeatPenalty = 1.1; + private boolean codeCompletionsEnabled = true; + private int codeCompletionMaxTokens = 128; public boolean isUseCustomModel() { return useCustomModel; @@ -168,6 +170,22 @@ public class LlamaSettingsState { this.repeatPenalty = repeatPenalty; } + public boolean isCodeCompletionsEnabled() { + return codeCompletionsEnabled; + } + + public void setCodeCompletionsEnabled(boolean codeCompletionsEnabled) { + this.codeCompletionsEnabled = codeCompletionsEnabled; + } + + public int getCodeCompletionMaxTokens() { + return codeCompletionMaxTokens; + } + + public void setCodeCompletionMaxTokens(int codeCompletionMaxTokens) { + this.codeCompletionMaxTokens = codeCompletionMaxTokens; + } + private static Integer getRandomAvailablePortOrDefault() { try (ServerSocket socket = new ServerSocket(0)) { return socket.getLocalPort(); @@ -201,7 +219,9 @@ public class LlamaSettingsState { && remoteModelInfillPromptTemplate == that.remoteModelInfillPromptTemplate && Objects.equals(baseHost, that.baseHost) && Objects.equals(serverPort, that.serverPort) - && Objects.equals(additionalParameters, that.additionalParameters); + && Objects.equals(additionalParameters, that.additionalParameters) + && codeCompletionsEnabled == that.codeCompletionsEnabled + && codeCompletionMaxTokens == that.codeCompletionMaxTokens; } @Override @@ -209,6 +229,7 @@ public class LlamaSettingsState { return Objects.hash(runLocalServer, useCustomModel, customLlamaModelPath, huggingFaceModel, localModelPromptTemplate, remoteModelPromptTemplate, localModelInfillPromptTemplate, remoteModelInfillPromptTemplate, baseHost, serverPort, contextSize, threads, - additionalParameters, topK, topP, minP, repeatPenalty); + additionalParameters, topK, topP, minP, repeatPenalty, codeCompletionsEnabled, + codeCompletionMaxTokens); } } diff --git a/src/main/java/ee/carlrobert/codegpt/settings/service/llama/form/LlamaSettingsForm.java b/src/main/java/ee/carlrobert/codegpt/settings/service/llama/form/LlamaSettingsForm.java index caa9de03..7147eceb 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/service/llama/form/LlamaSettingsForm.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/service/llama/form/LlamaSettingsForm.java @@ -5,6 +5,7 @@ import static ee.carlrobert.codegpt.ui.UIUtil.withEmptyLeftBorder; import com.intellij.ui.TitledSeparator; import com.intellij.util.ui.FormBuilder; import ee.carlrobert.codegpt.CodeGPTBundle; +import ee.carlrobert.codegpt.settings.service.CodeCompletionConfigurationForm; import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings; import ee.carlrobert.codegpt.settings.service.llama.LlamaSettingsState; import java.awt.BorderLayout; @@ -14,10 +15,14 @@ public class LlamaSettingsForm extends JPanel { private final LlamaServerPreferencesForm llamaServerPreferencesForm; private final LlamaRequestPreferencesForm llamaRequestPreferencesForm; + private final CodeCompletionConfigurationForm codeCompletionConfigurationForm; public LlamaSettingsForm(LlamaSettingsState settings) { llamaServerPreferencesForm = new LlamaServerPreferencesForm(settings); llamaRequestPreferencesForm = new LlamaRequestPreferencesForm(settings); + codeCompletionConfigurationForm = new CodeCompletionConfigurationForm( + settings.isCodeCompletionsEnabled(), + settings.getCodeCompletionMaxTokens()); init(); } @@ -44,6 +49,8 @@ public class LlamaSettingsForm extends JPanel { state.setLocalModelPromptTemplate(modelPreferencesForm.getPromptTemplate()); state.setLocalModelInfillPromptTemplate(modelPreferencesForm.getInfillPromptTemplate()); + state.setCodeCompletionsEnabled(codeCompletionConfigurationForm.isCodeCompletionsEnabled()); + state.setCodeCompletionMaxTokens(codeCompletionConfigurationForm.getMaxTokens()); return state; } @@ -51,6 +58,8 @@ public class LlamaSettingsForm extends JPanel { var state = LlamaSettings.getCurrentState(); llamaServerPreferencesForm.resetForm(state); llamaRequestPreferencesForm.resetForm(state); + codeCompletionConfigurationForm.setCodeCompletionsEnabled(state.isCodeCompletionsEnabled()); + codeCompletionConfigurationForm.setMaxTokens(state.getCodeCompletionMaxTokens()); } public LlamaServerPreferencesForm getLlamaServerPreferencesForm() { @@ -60,6 +69,8 @@ public class LlamaSettingsForm extends JPanel { private void init() { setLayout(new BorderLayout()); add(FormBuilder.createFormBuilder() + .addComponent(new TitledSeparator("Code Completions")) + .addComponent(withEmptyLeftBorder(codeCompletionConfigurationForm.getForm())) .addComponent(new TitledSeparator( CodeGPTBundle.get("settingsConfigurable.service.llama.serverPreferences.title"))) .addComponent(llamaServerPreferencesForm.getForm()) diff --git a/src/main/java/ee/carlrobert/codegpt/settings/service/openai/OpenAISettings.java b/src/main/java/ee/carlrobert/codegpt/settings/service/openai/OpenAISettings.java index d65a1576..4ba05dcd 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/service/openai/OpenAISettings.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/service/openai/OpenAISettings.java @@ -7,6 +7,7 @@ import com.intellij.openapi.components.PersistentStateComponent; import com.intellij.openapi.components.State; import com.intellij.openapi.components.Storage; import ee.carlrobert.codegpt.credentials.CredentialsStore; +import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings; import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.NotNull; diff --git a/src/main/java/ee/carlrobert/codegpt/settings/service/openai/OpenAISettingsForm.java b/src/main/java/ee/carlrobert/codegpt/settings/service/openai/OpenAISettingsForm.java index fe3d22b6..f0bdc78b 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/service/openai/OpenAISettingsForm.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/service/openai/OpenAISettingsForm.java @@ -12,6 +12,7 @@ import com.intellij.util.ui.FormBuilder; import com.intellij.util.ui.UI; import ee.carlrobert.codegpt.CodeGPTBundle; import ee.carlrobert.codegpt.credentials.CredentialsStore; +import ee.carlrobert.codegpt.settings.service.CodeCompletionConfigurationForm; import ee.carlrobert.codegpt.ui.UIUtil; import ee.carlrobert.llm.client.openai.completion.OpenAIChatCompletionModel; import javax.swing.JPanel; @@ -22,6 +23,7 @@ public class OpenAISettingsForm { private final JBPasswordField apiKeyField; private final JBTextField organizationField; private final ComboBox completionModelComboBox; + private final CodeCompletionConfigurationForm codeCompletionConfigurationForm; public OpenAISettingsForm(OpenAISettingsState settings) { apiKeyField = new JBPasswordField(); @@ -32,6 +34,9 @@ public class OpenAISettingsForm { new EnumComboBoxModel<>(OpenAIChatCompletionModel.class)); completionModelComboBox.setSelectedItem( OpenAIChatCompletionModel.findByCode(settings.getModel())); + codeCompletionConfigurationForm = new CodeCompletionConfigurationForm( + settings.isCodeCompletionsEnabled(), + settings.getCodeCompletionMaxTokens()); } public JPanel getForm() { @@ -52,6 +57,8 @@ public class OpenAISettingsForm { .createPanel(); return FormBuilder.createFormBuilder() + .addComponent(new TitledSeparator(CodeGPTBundle.get("shared.codeCompletions"))) + .addComponent(withEmptyLeftBorder(codeCompletionConfigurationForm.getForm())) .addComponent(new TitledSeparator(CodeGPTBundle.get("shared.configuration"))) .addComponent(withEmptyLeftBorder(configurationGrid)) .addComponentFillVertically(new JPanel(), 0) @@ -73,6 +80,8 @@ public class OpenAISettingsForm { var state = new OpenAISettingsState(); state.setModel(getModel()); state.setOrganization(organizationField.getText()); + state.setCodeCompletionsEnabled(codeCompletionConfigurationForm.isCodeCompletionsEnabled()); + state.setCodeCompletionMaxTokens(codeCompletionConfigurationForm.getMaxTokens()); return state; } @@ -82,5 +91,7 @@ public class OpenAISettingsForm { completionModelComboBox.setSelectedItem( OpenAIChatCompletionModel.findByCode(state.getModel())); organizationField.setText(state.getOrganization()); + codeCompletionConfigurationForm.setCodeCompletionsEnabled(state.isCodeCompletionsEnabled()); + codeCompletionConfigurationForm.setMaxTokens(state.getCodeCompletionMaxTokens()); } } diff --git a/src/main/java/ee/carlrobert/codegpt/settings/service/openai/OpenAISettingsState.java b/src/main/java/ee/carlrobert/codegpt/settings/service/openai/OpenAISettingsState.java index 75134d65..b13df7d1 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/service/openai/OpenAISettingsState.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/service/openai/OpenAISettingsState.java @@ -7,6 +7,8 @@ public class OpenAISettingsState { private String organization = ""; private String model = OpenAIChatCompletionModel.GPT_3_5_0125_16k.getCode(); + private boolean codeCompletionsEnabled = true; + private int codeCompletionMaxTokens = 128; public String getOrganization() { return organization; @@ -24,6 +26,22 @@ public class OpenAISettingsState { this.model = model; } + public boolean isCodeCompletionsEnabled() { + return codeCompletionsEnabled; + } + + public void setCodeCompletionsEnabled(boolean codeCompletionsEnabled) { + this.codeCompletionsEnabled = codeCompletionsEnabled; + } + + public int getCodeCompletionMaxTokens() { + return codeCompletionMaxTokens; + } + + public void setCodeCompletionMaxTokens(int codeCompletionMaxTokens) { + this.codeCompletionMaxTokens = codeCompletionMaxTokens; + } + @Override public boolean equals(Object o) { if (this == o) { @@ -33,11 +51,14 @@ public class OpenAISettingsState { return false; } OpenAISettingsState that = (OpenAISettingsState) o; - return Objects.equals(organization, that.organization) && Objects.equals(model, that.model); + return Objects.equals(organization, that.organization) + && Objects.equals(model, that.model) + && codeCompletionsEnabled == that.codeCompletionsEnabled + && codeCompletionMaxTokens == that.codeCompletionMaxTokens; } @Override public int hashCode() { - return Objects.hash(organization, model); + return Objects.hash(organization, model, codeCompletionsEnabled, codeCompletionMaxTokens); } } diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/ProjectToolWindowFactory.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/ProjectToolWindowFactory.java index e3c43b2c..655cdc68 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/ProjectToolWindowFactory.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/ProjectToolWindowFactory.java @@ -6,7 +6,7 @@ import com.intellij.openapi.wm.ToolWindow; import com.intellij.openapi.wm.ToolWindowFactory; import com.intellij.ui.content.ContentManagerEvent; import com.intellij.ui.content.ContentManagerListener; -import ee.carlrobert.codegpt.toolwindow.chat.standard.StandardChatToolWindowPanel; +import ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowPanel; import ee.carlrobert.codegpt.toolwindow.conversations.ConversationsToolWindow; import javax.swing.JComponent; import org.jetbrains.annotations.NotNull; @@ -14,7 +14,7 @@ import org.jetbrains.annotations.NotNull; public class ProjectToolWindowFactory implements ToolWindowFactory, DumbAware { public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindow toolWindow) { - var chatToolWindowPanel = new StandardChatToolWindowPanel(project, toolWindow.getDisposable()); + var chatToolWindowPanel = new ChatToolWindowPanel(project, toolWindow.getDisposable()); var conversationsToolWindow = new ConversationsToolWindow(project); addContent(toolWindow, chatToolWindowPanel, "Chat"); diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/StandardChatToolWindowContentManager.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowContentManager.java similarity index 81% rename from src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/StandardChatToolWindowContentManager.java rename to src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowContentManager.java index 27ea30bd..b66814c9 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/StandardChatToolWindowContentManager.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowContentManager.java @@ -1,4 +1,4 @@ -package ee.carlrobert.codegpt.toolwindow.chat.standard; +package ee.carlrobert.codegpt.toolwindow.chat; import static java.util.Objects.requireNonNull; @@ -22,11 +22,11 @@ import java.util.Optional; import org.jetbrains.annotations.NotNull; @Service(Service.Level.PROJECT) -public final class StandardChatToolWindowContentManager { +public final class ChatToolWindowContentManager { private final Project project; - public StandardChatToolWindowContentManager(Project project) { + public ChatToolWindowContentManager(Project project) { this.project = project; } @@ -55,15 +55,14 @@ public final class StandardChatToolWindowContentManager { .ifPresent(tabbedPane -> tabbedPane.tryFindTabTitle(conversation.getId()) .ifPresentOrElse( title -> tabbedPane.setSelectedIndex(tabbedPane.indexOfTab(title)), - () -> tabbedPane.addNewTab( - new StandardChatToolWindowTabPanel(project, conversation)))); + () -> tabbedPane.addNewTab(new ChatToolWindowTabPanel(project, conversation)))); } - public StandardChatToolWindowTabPanel createNewTabPanel() { + public ChatToolWindowTabPanel createNewTabPanel() { displayChatTab(); return tryFindChatTabbedPane() .map(item -> { - var panel = new StandardChatToolWindowTabPanel( + var panel = new ChatToolWindowTabPanel( project, ConversationService.getInstance().startConversation()); item.addNewTab(panel); @@ -83,26 +82,26 @@ public final class StandardChatToolWindowContentManager { ); } - public Optional tryFindChatTabbedPane() { + public Optional tryFindChatTabbedPane() { var chatTabContent = tryFindFirstChatTabContent(); if (chatTabContent.isPresent()) { - var chatToolWindowPanel = (StandardChatToolWindowPanel) chatTabContent.get().getComponent(); + var chatToolWindowPanel = (ChatToolWindowPanel) chatTabContent.get().getComponent(); return Optional.of(chatToolWindowPanel.getChatTabbedPane()); } return Optional.empty(); } - public Optional tryFindChatToolWindowPanel() { + public Optional tryFindChatToolWindowPanel() { return tryFindFirstChatTabContent() .map(ComponentContainer::getComponent) - .filter(component -> component instanceof StandardChatToolWindowPanel) - .map(component -> (StandardChatToolWindowPanel) component); + .filter(component -> component instanceof ChatToolWindowPanel) + .map(component -> (ChatToolWindowPanel) component); } public void resetAll() { tryFindChatTabbedPane().ifPresent(tabbedPane -> { tabbedPane.clearAll(); - tabbedPane.addNewTab(new StandardChatToolWindowTabPanel( + tabbedPane.addNewTab(new ChatToolWindowTabPanel( project, ConversationService.getInstance().startConversation())); }); diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowPanel.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowPanel.java new file mode 100644 index 00000000..ea40de64 --- /dev/null +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowPanel.java @@ -0,0 +1,154 @@ +package ee.carlrobert.codegpt.toolwindow.chat; + +import static java.lang.String.format; +import static java.util.Collections.emptyList; + +import com.intellij.openapi.Disposable; +import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.actionSystem.ActionToolbar; +import com.intellij.openapi.actionSystem.DefaultCompactActionGroup; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.SimpleToolWindowPanel; +import com.intellij.openapi.util.Disposer; +import com.intellij.util.ui.JBUI; +import ee.carlrobert.codegpt.CodeGPTKeys; +import ee.carlrobert.codegpt.ReferencedFile; +import ee.carlrobert.codegpt.actions.IncludeFilesInContextNotifier; +import ee.carlrobert.codegpt.actions.toolwindow.ClearChatWindowAction; +import ee.carlrobert.codegpt.actions.toolwindow.CreateNewConversationAction; +import ee.carlrobert.codegpt.actions.toolwindow.OpenInEditorAction; +import ee.carlrobert.codegpt.conversations.ConversationService; +import ee.carlrobert.codegpt.conversations.ConversationsState; +import ee.carlrobert.codegpt.toolwindow.chat.ui.ToolWindowFooterNotification; +import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.AttachImageNotifier; +import java.awt.BorderLayout; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.stream.Collectors; +import javax.swing.BoxLayout; +import javax.swing.JPanel; +import org.jetbrains.annotations.NotNull; + +public class ChatToolWindowPanel extends SimpleToolWindowPanel { + + private final ToolWindowFooterNotification selectedFilesNotification; + private final ToolWindowFooterNotification imageFileAttachmentNotification; + private ChatToolWindowTabbedPane tabbedPane; + + public ChatToolWindowPanel( + @NotNull Project project, + @NotNull Disposable parentDisposable) { + super(true); + selectedFilesNotification = new ToolWindowFooterNotification( + () -> clearSelectedFilesNotification(project)); + imageFileAttachmentNotification = new ToolWindowFooterNotification(() -> + project.putUserData(CodeGPTKeys.IMAGE_ATTACHMENT_FILE_PATH, "")); + init(project, parentDisposable); + + project.getMessageBus() + .connect() + .subscribe(IncludeFilesInContextNotifier.FILES_INCLUDED_IN_CONTEXT_TOPIC, + (IncludeFilesInContextNotifier) this::displaySelectedFilesNotification); + project.getMessageBus() + .connect() + .subscribe(AttachImageNotifier.IMAGE_ATTACHMENT_FILE_PATH_TOPIC, + (AttachImageNotifier) filePath -> imageFileAttachmentNotification.show( + Path.of(filePath).getFileName().toString(), + "File path: " + filePath)); + } + + public ChatToolWindowTabbedPane getChatTabbedPane() { + return tabbedPane; + } + + public void displaySelectedFilesNotification(List referencedFiles) { + if (referencedFiles.isEmpty()) { + return; + } + + var referencedFilePaths = referencedFiles.stream() + .map(ReferencedFile::getFilePath) + .collect(Collectors.toList()); + selectedFilesNotification.show( + referencedFiles.size() + " files selected", + selectedFilesNotificationDescription(referencedFilePaths)); + } + + private String selectedFilesNotificationDescription(List referencedFilePaths) { + var html = referencedFilePaths.stream() + .map(filePath -> format("
  • %s
  • ", Paths.get(filePath).getFileName().toString())) + .collect(Collectors.joining()); + return format("
      %s
    ", html); + } + + public void clearNotifications(Project project) { + selectedFilesNotification.hideNotification(); + imageFileAttachmentNotification.hideNotification(); + + project.putUserData(CodeGPTKeys.IMAGE_ATTACHMENT_FILE_PATH, ""); + project.putUserData(CodeGPTKeys.SELECTED_FILES, emptyList()); + } + + private void init(Project project, Disposable parentDisposable) { + var conversation = ConversationsState.getCurrentConversation(); + if (conversation == null) { + conversation = ConversationService.getInstance().startConversation(); + } + + var tabPanel = new ChatToolWindowTabPanel(project, conversation); + tabbedPane = createTabbedPane(tabPanel, parentDisposable); + Runnable onAddNewTab = () -> { + tabbedPane.addNewTab(new ChatToolWindowTabPanel( + project, + ConversationService.getInstance().startConversation())); + repaint(); + revalidate(); + }; + var actionToolbarPanel = new JPanel(new BorderLayout()); + actionToolbarPanel.add( + createActionToolbar(project, tabbedPane, onAddNewTab).getComponent(), + BorderLayout.LINE_START); + + setToolbar(actionToolbarPanel); + var notificationContainer = new JPanel(new BorderLayout()); + notificationContainer.setLayout(new BoxLayout(notificationContainer, BoxLayout.PAGE_AXIS)); + notificationContainer.add(selectedFilesNotification); + notificationContainer.add(imageFileAttachmentNotification); + setContent(JBUI.Panels.simplePanel(tabbedPane).addToBottom(notificationContainer)); + + Disposer.register(parentDisposable, tabPanel); + } + + private ActionToolbar createActionToolbar( + Project project, + ChatToolWindowTabbedPane tabbedPane, + Runnable onAddNewTab) { + var actionGroup = new DefaultCompactActionGroup("TOOLBAR_ACTION_GROUP", false); + actionGroup.add(new CreateNewConversationAction(onAddNewTab)); + actionGroup.add( + new ClearChatWindowAction(() -> tabbedPane.resetCurrentlyActiveTabPanel(project))); + actionGroup.addSeparator(); + actionGroup.add(new OpenInEditorAction()); + + var toolbar = ActionManager.getInstance() + .createActionToolbar("NAVIGATION_BAR_TOOLBAR", actionGroup, true); + toolbar.setTargetComponent(this); + return toolbar; + } + + private ChatToolWindowTabbedPane createTabbedPane( + ChatToolWindowTabPanel tabPanel, + Disposable parentDisposable) { + var tabbedPane = new ChatToolWindowTabbedPane(parentDisposable); + tabbedPane.addNewTab(tabPanel); + return tabbedPane; + } + + public void clearSelectedFilesNotification(Project project) { + project.putUserData(CodeGPTKeys.SELECTED_FILES, emptyList()); + project.getMessageBus() + .syncPublisher(IncludeFilesInContextNotifier.FILES_INCLUDED_IN_CONTEXT_TOPIC) + .filesIncluded(emptyList()); + } +} diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanel.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanel.java index b59867ed..95cf70a8 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanel.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanel.java @@ -6,12 +6,13 @@ import static java.lang.String.format; import static java.util.stream.Collectors.toList; import com.intellij.openapi.Disposable; -import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.actionSystem.ActionPlaces; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.impl.EditorImpl; import com.intellij.openapi.project.Project; import com.intellij.ui.JBColor; import com.intellij.util.ui.JBUI; +import com.intellij.util.ui.JBUI.Borders; import ee.carlrobert.codegpt.CodeGPTKeys; import ee.carlrobert.codegpt.EncodingManager; import ee.carlrobert.codegpt.ReferencedFile; @@ -25,47 +26,47 @@ import ee.carlrobert.codegpt.conversations.ConversationService; import ee.carlrobert.codegpt.conversations.message.Message; import ee.carlrobert.codegpt.settings.GeneralSettings; import ee.carlrobert.codegpt.settings.service.ServiceType; +import ee.carlrobert.codegpt.settings.service.you.YouSettings; import ee.carlrobert.codegpt.telemetry.TelemetryAction; -import ee.carlrobert.codegpt.toolwindow.chat.standard.StandardChatToolWindowContentManager; -import ee.carlrobert.codegpt.toolwindow.chat.standard.StandardChatToolWindowPanel; import ee.carlrobert.codegpt.toolwindow.chat.ui.ChatMessageResponseBody; import ee.carlrobert.codegpt.toolwindow.chat.ui.ChatToolWindowScrollablePanel; import ee.carlrobert.codegpt.toolwindow.chat.ui.ResponsePanel; import ee.carlrobert.codegpt.toolwindow.chat.ui.UserMessagePanel; +import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.ModelComboBoxAction; import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.TotalTokensDetails; import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.TotalTokensPanel; import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.UserPromptTextArea; -import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.UserPromptTextAreaHeader; +import ee.carlrobert.codegpt.toolwindow.ui.ChatToolWindowLandingPanel; +import ee.carlrobert.codegpt.ui.OverlayUtil; import ee.carlrobert.codegpt.util.EditorUtil; import ee.carlrobert.codegpt.util.file.FileUtil; import java.awt.BorderLayout; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.UUID; import javax.swing.JComponent; import javax.swing.JPanel; import javax.swing.SwingUtilities; +import kotlin.Unit; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; -public abstract class ChatToolWindowTabPanel implements Disposable { +public class ChatToolWindowTabPanel implements Disposable { private static final Logger LOG = Logger.getInstance(ChatToolWindowTabPanel.class); + private final Project project; private final JPanel rootPanel; private final Conversation conversation; private final UserPromptTextArea userPromptTextArea; private final ConversationService conversationService; + private final TotalTokensPanel totalTokensPanel; + private final ChatToolWindowScrollablePanel toolWindowScrollablePanel; - protected final Project project; - protected final TotalTokensPanel totalTokensPanel; - protected final ChatToolWindowScrollablePanel toolWindowScrollablePanel; - - protected abstract JComponent getLandingView(); - - public ChatToolWindowTabPanel( - @NotNull Project project, - @NotNull Conversation conversation, - boolean useContextualSearch) { + public ChatToolWindowTabPanel(@NotNull Project project, @NotNull Conversation conversation) { this.project = project; this.conversation = conversation; conversationService = ConversationService.getInstance(); @@ -79,6 +80,12 @@ public abstract class ChatToolWindowTabPanel implements Disposable { rootPanel = createRootPanel(); userPromptTextArea.requestFocusInWindow(); userPromptTextArea.requestFocus(); + + if (conversation.getMessages().isEmpty()) { + displayLandingView(); + } else { + displayConversation(conversation); + } } public void dispose() { @@ -93,58 +100,6 @@ public abstract class ChatToolWindowTabPanel implements Disposable { return conversation; } - public void sendMessage(Message message) { - sendMessage(message, ConversationType.DEFAULT); - } - - public void sendMessage(Message message, ConversationType conversationType) { - Runnable runnable = () -> { - var referencedFiles = project.getUserData(CodeGPTKeys.SELECTED_FILES); - if (referencedFiles != null && !referencedFiles.isEmpty()) { - var referencedFilePaths = referencedFiles.stream() - .map(ReferencedFile::getFilePath) - .collect(toList()); - message.setReferencedFilePaths(referencedFilePaths); - message.setUserMessage(message.getPrompt()); - message.setPrompt(getPromptWithContext(referencedFiles, message.getPrompt())); - - totalTokensPanel.updateReferencedFilesTokens(referencedFiles); - - project.getService(StandardChatToolWindowContentManager.class) - .tryFindChatToolWindowPanel() - .ifPresent(StandardChatToolWindowPanel::clearSelectedFilesNotification); - } - - var messagePanel = toolWindowScrollablePanel.addMessage(message.getId()); - messagePanel.add(new UserMessagePanel(project, message, this)); - var responsePanel = createResponsePanel(message, conversationType); - messagePanel.add(responsePanel); - - updateTotalTokens(message); - - call(message, conversationType, responsePanel, false); - }; - // TODO - if (ApplicationManager.getApplication().isUnitTestMode()) { - runnable.run(); - } else { - SwingUtilities.invokeLater(runnable); - } - } - - private void updateTotalTokens(Message message) { - int userPromptTokens = EncodingManager.getInstance().countTokens(message.getPrompt()); - int conversationTokens = EncodingManager.getInstance().countConversationTokens(conversation); - totalTokensPanel.updateConversationTokens(conversationTokens + userPromptTokens); - } - - private ResponsePanel createResponsePanel(Message message, ConversationType conversationType) { - return new ResponsePanel() - .withReloadAction(() -> reloadMessage(message, conversation, conversationType)) - .withDeleteAction(() -> removeMessage(message.getId(), conversation)) - .addContent(new ChatMessageResponseBody(project, true, this)); - } - public TotalTokensDetails getTokenDetails() { return totalTokensPanel.getTokenDetails(); } @@ -158,7 +113,77 @@ public abstract class ChatToolWindowTabPanel implements Disposable { totalTokensPanel.updateConversationTokens(conversation); } - protected void reloadMessage( + public void sendMessage(Message message) { + sendMessage(message, ConversationType.DEFAULT); + } + + public void sendMessage(Message message, ConversationType conversationType) { + SwingUtilities.invokeLater(() -> { + var referencedFiles = project.getUserData(CodeGPTKeys.SELECTED_FILES); + var chatToolWindowPanel = project.getService(ChatToolWindowContentManager.class) + .tryFindChatToolWindowPanel(); + if (referencedFiles != null && !referencedFiles.isEmpty()) { + var referencedFilePaths = referencedFiles.stream() + .map(ReferencedFile::getFilePath) + .collect(toList()); + message.setReferencedFilePaths(referencedFilePaths); + message.setUserMessage(message.getPrompt()); + message.setPrompt(getPromptWithContext(referencedFiles, message.getPrompt())); + + totalTokensPanel.updateReferencedFilesTokens(referencedFiles); + + chatToolWindowPanel.ifPresent(panel -> panel.clearNotifications(project)); + } + + var userMessagePanel = new UserMessagePanel(project, message, this); + var attachedFilePath = CodeGPTKeys.IMAGE_ATTACHMENT_FILE_PATH.get(project); + var callParameters = getCallParameters(conversationType, message, attachedFilePath); + if (callParameters.getImageData() != null) { + message.setImageFilePath(attachedFilePath); + chatToolWindowPanel.ifPresent(panel -> panel.clearNotifications(project)); + userMessagePanel.displayImage(attachedFilePath); + } + + var messagePanel = toolWindowScrollablePanel.addMessage(message.getId()); + messagePanel.add(userMessagePanel); + + var responsePanel = createResponsePanel(message, conversationType); + messagePanel.add(responsePanel); + updateTotalTokens(message); + call(callParameters, responsePanel); + }); + } + + private CallParameters getCallParameters( + ConversationType conversationType, + Message message, + @Nullable String attachedFilePath) { + var callParameters = new CallParameters(conversation, conversationType, message, false); + if (attachedFilePath != null && !attachedFilePath.isEmpty()) { + try { + callParameters.setImageData(Files.readAllBytes(Path.of(attachedFilePath))); + callParameters.setImageMediaType(FileUtil.getImageMediaType(attachedFilePath)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + return callParameters; + } + + private void updateTotalTokens(Message message) { + int userPromptTokens = EncodingManager.getInstance().countTokens(message.getPrompt()); + int conversationTokens = EncodingManager.getInstance().countConversationTokens(conversation); + totalTokensPanel.updateConversationTokens(conversationTokens + userPromptTokens); + } + + private ResponsePanel createResponsePanel(Message message, ConversationType conversationType) { + return new ResponsePanel() + .withReloadAction(() -> reloadMessage(message, conversation, conversationType)) + .withDeleteAction(() -> removeMessage(message.getId(), conversation)) + .addContent(new ChatMessageResponseBody(project, true, this)); + } + + private void reloadMessage( Message message, Conversation conversation, ConversationType conversationType) { @@ -175,7 +200,7 @@ public abstract class ChatToolWindowTabPanel implements Disposable { if (responsePanel != null) { message.setResponse(""); conversationService.saveMessage(conversation, message); - call(message, conversationType, responsePanel, true); + call(new CallParameters(conversation, conversationType, message, true), responsePanel); } totalTokensPanel.updateConversationTokens(conversation); @@ -186,7 +211,7 @@ public abstract class ChatToolWindowTabPanel implements Disposable { } } - protected void removeMessage(UUID messageId, Conversation conversation) { + private void removeMessage(UUID messageId, Conversation conversation) { toolWindowScrollablePanel.removeMessage(messageId); conversation.removeMessage(messageId); conversationService.saveConversation(conversation); @@ -197,16 +222,12 @@ public abstract class ChatToolWindowTabPanel implements Disposable { } } - protected void clearWindow() { + private void clearWindow() { toolWindowScrollablePanel.clearAll(); totalTokensPanel.updateConversationTokens(conversation); } - private void call( - Message message, - ConversationType conversationType, - ResponsePanel responsePanel, - boolean retry) { + private void call(CallParameters callParameters, ResponsePanel responsePanel) { var responseContainer = (ChatMessageResponseBody) responsePanel.getContent(); if (!CompletionRequestService.getInstance().isRequestAllowed()) { @@ -222,13 +243,13 @@ public abstract class ChatToolWindowTabPanel implements Disposable { userPromptTextArea) { @Override public void handleTokensExceededPolicyAccepted() { - call(message, conversationType, responsePanel, true); + call(callParameters, responsePanel); } }); userPromptTextArea.setRequestHandler(requestHandler); userPromptTextArea.setSubmitEnabled(false); - requestHandler.call(new CallParameters(conversation, conversationType, message, retry)); + requestHandler.call(callParameters); } private void handleSubmit(String text) { @@ -253,15 +274,79 @@ public abstract class ChatToolWindowTabPanel implements Disposable { panel.setBorder(JBUI.Borders.compound( JBUI.Borders.customLine(JBColor.border(), 1, 0, 0, 0), JBUI.Borders.empty(8))); - var contentManager = project.getService(StandardChatToolWindowContentManager.class); - panel.add(JBUI.Panels.simplePanel(new UserPromptTextAreaHeader( + var contentManager = project.getService(ChatToolWindowContentManager.class); + panel.add(JBUI.Panels.simplePanel(createUserPromptTextAreaHeader( selectedService, - totalTokensPanel, - contentManager::createNewTabPanel)), BorderLayout.NORTH); + () -> { + ConversationService.getInstance().startConversation(); + contentManager.createNewTabPanel(); + })), BorderLayout.NORTH); panel.add(JBUI.Panels.simplePanel(userPromptTextArea), BorderLayout.CENTER); return panel; } + private JPanel createUserPromptTextAreaHeader( + ServiceType selectedService, + Runnable onModelChange) { + return JBUI.Panels.simplePanel() + .withBorder(Borders.emptyBottom(8)) + .andTransparent() + .addToLeft(totalTokensPanel) + .addToRight(new ModelComboBoxAction(onModelChange, selectedService) + .createCustomComponent(ActionPlaces.UNKNOWN)); + } + + private JComponent getLandingView() { + return new ChatToolWindowLandingPanel((action, locationOnScreen) -> { + var editor = EditorUtil.getSelectedEditor(project); + if (editor == null || !editor.getSelectionModel().hasSelection()) { + OverlayUtil.showWarningBalloon( + editor == null ? "Unable to locate a selected editor" + : "Please select a target code before proceeding", + locationOnScreen); + return Unit.INSTANCE; + } + + var fileExtension = FileUtil.getFileExtension( + ((EditorImpl) editor).getVirtualFile().getName()); + var message = new Message(action.getPrompt().replace( + "{{selectedCode}}", + format("\n```%s\n%s\n```", fileExtension, editor.getSelectionModel().getSelectedText()))); + message.setUserMessage(action.getUserMessage()); + + sendMessage(message, ConversationType.DEFAULT); + return Unit.INSTANCE; + }); + } + + private void displayConversation(@NotNull Conversation conversation) { + clearWindow(); + conversation.getMessages().forEach(message -> { + var messageResponseBody = + new ChatMessageResponseBody(project, this).withResponse(message.getResponse()); + + var serpResults = message.getSerpResults(); + if (YouSettings.getCurrentState().isDisplayWebSearchResults() + && serpResults != null && !serpResults.isEmpty()) { + messageResponseBody.displaySerpResults(serpResults); + } + messageResponseBody.hideCaret(); + + var userMessagePanel = new UserMessagePanel(project, message, this); + var imageFilePath = message.getImageFilePath(); + if (imageFilePath != null && !imageFilePath.isEmpty()) { + userMessagePanel.displayImage(imageFilePath); + } + + var messagePanel = toolWindowScrollablePanel.addMessage(message.getId()); + messagePanel.add(userMessagePanel); + messagePanel.add(new ResponsePanel() + .withReloadAction(() -> reloadMessage(message, conversation, ConversationType.DEFAULT)) + .withDeleteAction(() -> removeMessage(message.getId(), conversation)) + .addContent(messageResponseBody)); + }); + } + private JPanel createRootPanel() { var gbc = new GridBagConstraints(); gbc.fill = GridBagConstraints.BOTH; diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/StandardChatToolWindowTabbedPane.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabbedPane.java similarity index 88% rename from src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/StandardChatToolWindowTabbedPane.java rename to src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabbedPane.java index d623b032..ca7551f5 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/StandardChatToolWindowTabbedPane.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabbedPane.java @@ -1,4 +1,4 @@ -package ee.carlrobert.codegpt.toolwindow.chat.standard; +package ee.carlrobert.codegpt.toolwindow.chat; import com.intellij.icons.AllIcons; import com.intellij.openapi.Disposable; @@ -25,9 +25,9 @@ import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.SwingUtilities; -public class StandardChatToolWindowTabbedPane extends JBTabbedPane { +public class ChatToolWindowTabbedPane extends JBTabbedPane { - private final Map activeTabMapping = new TreeMap<>( + private final Map activeTabMapping = new TreeMap<>( (o1, o2) -> { int n1 = Integer.parseInt(o1.replaceAll("\\D", "")); int n2 = Integer.parseInt(o2.replaceAll("\\D", "")); @@ -35,18 +35,18 @@ public class StandardChatToolWindowTabbedPane extends JBTabbedPane { }); private final Disposable parentDisposable; - public StandardChatToolWindowTabbedPane(Disposable parentDisposable) { + public ChatToolWindowTabbedPane(Disposable parentDisposable) { this.parentDisposable = parentDisposable; setTabComponentInsets(null); setComponentPopupMenu(new TabPopupMenu()); addChangeListener(e -> refreshTabState()); } - public Map getActiveTabMapping() { + public Map getActiveTabMapping() { return activeTabMapping; } - public void addNewTab(StandardChatToolWindowTabPanel toolWindowPanel) { + public void addNewTab(ChatToolWindowTabPanel toolWindowPanel) { var tabIndices = activeTabMapping.keySet().toArray(new String[0]); var nextIndex = 0; for (String title : tabIndices) { @@ -81,7 +81,7 @@ public class StandardChatToolWindowTabbedPane extends JBTabbedPane { .map(Map.Entry::getKey); } - public Optional tryFindActiveTabPanel() { + public Optional tryFindActiveTabPanel() { var selectedIndex = getSelectedIndex(); if (selectedIndex == -1) { return Optional.empty(); @@ -116,7 +116,7 @@ public class StandardChatToolWindowTabbedPane extends JBTabbedPane { Disposer.dispose(tabPanel); activeTabMapping.remove(getTitleAt(getSelectedIndex())); removeTabAt(getSelectedIndex()); - addNewTab(new StandardChatToolWindowTabPanel( + addNewTab(new ChatToolWindowTabPanel( project, ConversationService.getInstance().startConversation())); repaint(); @@ -180,8 +180,8 @@ public class StandardChatToolWindowTabbedPane extends JBTabbedPane { @Override public void show(Component invoker, int x, int y) { - selectedPopupTabIndex = StandardChatToolWindowTabbedPane.this.getUI() - .tabForCoordinate(StandardChatToolWindowTabbedPane.this, x, y); + selectedPopupTabIndex = ChatToolWindowTabbedPane.this.getUI() + .tabForCoordinate(ChatToolWindowTabbedPane.this, x, y); if (selectedPopupTabIndex > 0) { super.show(invoker, x, y); } diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/EditorAction.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/EditorAction.java deleted file mode 100644 index 5baf396d..00000000 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/EditorAction.java +++ /dev/null @@ -1,46 +0,0 @@ -package ee.carlrobert.codegpt.toolwindow.chat.standard; - -enum EditorAction { - FIND_BUGS( - "Find Bugs", - "Find bugs in the selected code", - "Find bugs and output code with bugs fixed in the selected code: {{selectedCode}}"), - WRITE_TESTS( - "Write Tests", - "Write unit tests for the selected code", - "Write unit tests for the selected code: {{selectedCode}}"), - EXPLAIN( - "Explain", - "Explain the selected code", - "Explain the selected code: {{selectedCode}}"), - REFACTOR( - "Refactor", - "Refactor the selected code", - "Refactor the selected code: {{selectedCode}}"), - OPTIMIZE( - "Optimize", - "Optimize the selected code", - "Optimize the selected code: {{selectedCode}}"); - - private final String label; - private final String userMessage; - private final String prompt; - - EditorAction(String label, String userMessage, String prompt) { - this.label = label; - this.userMessage = userMessage; - this.prompt = prompt; - } - - public String getLabel() { - return label; - } - - public String getPrompt() { - return prompt; - } - - public String getUserMessage() { - return userMessage; - } -} diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/EditorActionEvent.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/EditorActionEvent.java deleted file mode 100644 index c8af738c..00000000 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/EditorActionEvent.java +++ /dev/null @@ -1,9 +0,0 @@ -package ee.carlrobert.codegpt.toolwindow.chat.standard; - -import java.awt.Point; - -@FunctionalInterface -public interface EditorActionEvent { - - void handleAction(EditorAction action, Point locationOnScreen); -} diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/StandardChatToolWindowLandingPanel.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/StandardChatToolWindowLandingPanel.java deleted file mode 100644 index eab81f22..00000000 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/StandardChatToolWindowLandingPanel.java +++ /dev/null @@ -1,70 +0,0 @@ -package ee.carlrobert.codegpt.toolwindow.chat.standard; - -import static java.lang.String.format; - -import com.intellij.ui.components.ActionLink; -import com.intellij.util.ui.JBUI; -import ee.carlrobert.codegpt.Icons; -import ee.carlrobert.codegpt.settings.GeneralSettings; -import ee.carlrobert.codegpt.toolwindow.chat.ui.ResponsePanel; -import ee.carlrobert.codegpt.ui.UIUtil; -import java.awt.BorderLayout; -import javax.swing.Box; -import javax.swing.BoxLayout; -import javax.swing.JPanel; - -class StandardChatToolWindowLandingPanel extends ResponsePanel { - - StandardChatToolWindowLandingPanel(EditorActionEvent onAction) { - addContent(createContent(onAction)); - } - - private ActionLink createEditorActionLink(EditorAction action, EditorActionEvent onAction) { - var link = new ActionLink(action.getUserMessage(), event -> { - onAction.handleAction(action, ((ActionLink) event.getSource()).getLocationOnScreen()); - }); - link.setIcon(Icons.Sparkle); - return link; - } - - private JPanel createContent(EditorActionEvent onAction) { - var panel = new JPanel(new BorderLayout()); - panel.add(UIUtil.createTextPane( - "" - + format( - "

    " - + "Welcome %s, I'm your intelligent code companion, here to be" - + " your partner-in-crime for getting things done in a flash." - + "

    ", GeneralSettings.getCurrentState().getDisplayName()) - + "

    " - + "Feel free to ask me anything you'd like, but my true superpower lies in assisting " - + "you with your code! Here are a few examples of how I can assist you:" - + "

    " - + "", - false), BorderLayout.NORTH); - panel.add(createEditorActionsListPanel(onAction), BorderLayout.CENTER); - panel.add(UIUtil.createTextPane( - "" - + "

    " - + "Being an AI-powered assistant, I may occasionally have surprises or make mistakes. " - + "Therefore, it's wise to double-check any code or suggestions I provide." - + "

    " - + "", - false), BorderLayout.SOUTH); - return panel; - } - - private JPanel createEditorActionsListPanel(EditorActionEvent onAction) { - var listPanel = new JPanel(); - listPanel.setLayout(new BoxLayout(listPanel, BoxLayout.PAGE_AXIS)); - listPanel.setBorder(JBUI.Borders.emptyLeft(4)); - listPanel.add(Box.createVerticalStrut(4)); - listPanel.add(createEditorActionLink(EditorAction.WRITE_TESTS, onAction)); - listPanel.add(Box.createVerticalStrut(4)); - listPanel.add(createEditorActionLink(EditorAction.EXPLAIN, onAction)); - listPanel.add(Box.createVerticalStrut(4)); - listPanel.add(createEditorActionLink(EditorAction.FIND_BUGS, onAction)); - listPanel.add(Box.createVerticalStrut(4)); - return listPanel; - } -} diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/StandardChatToolWindowPanel.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/StandardChatToolWindowPanel.java deleted file mode 100644 index 18e1d43b..00000000 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/StandardChatToolWindowPanel.java +++ /dev/null @@ -1,108 +0,0 @@ -package ee.carlrobert.codegpt.toolwindow.chat.standard; - -import com.intellij.openapi.Disposable; -import com.intellij.openapi.actionSystem.ActionManager; -import com.intellij.openapi.actionSystem.ActionToolbar; -import com.intellij.openapi.actionSystem.DefaultCompactActionGroup; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.ui.SimpleToolWindowPanel; -import com.intellij.openapi.util.Disposer; -import com.intellij.util.ui.JBUI; -import ee.carlrobert.codegpt.ReferencedFile; -import ee.carlrobert.codegpt.actions.IncludeFilesInContextNotifier; -import ee.carlrobert.codegpt.actions.toolwindow.ClearChatWindowAction; -import ee.carlrobert.codegpt.actions.toolwindow.CreateNewConversationAction; -import ee.carlrobert.codegpt.actions.toolwindow.OpenInEditorAction; -import ee.carlrobert.codegpt.conversations.ConversationService; -import ee.carlrobert.codegpt.conversations.ConversationsState; -import ee.carlrobert.codegpt.toolwindow.chat.ui.SelectedFilesNotification; -import java.awt.BorderLayout; -import java.util.List; -import javax.swing.JPanel; -import org.jetbrains.annotations.NotNull; - -public class StandardChatToolWindowPanel extends SimpleToolWindowPanel { - - private final SelectedFilesNotification selectedFilesNotification; - private StandardChatToolWindowTabbedPane tabbedPane; - - public StandardChatToolWindowPanel( - @NotNull Project project, - @NotNull Disposable parentDisposable) { - super(true); - selectedFilesNotification = new SelectedFilesNotification(project); - init(project, selectedFilesNotification, parentDisposable); - - project.getMessageBus() - .connect() - .subscribe(IncludeFilesInContextNotifier.FILES_INCLUDED_IN_CONTEXT_TOPIC, - (IncludeFilesInContextNotifier) this::displaySelectedFilesNotification); - } - - public void displaySelectedFilesNotification(List referencedFiles) { - selectedFilesNotification.displaySelectedFilesNotification(referencedFiles); - } - - public void clearSelectedFilesNotification() { - selectedFilesNotification.clearSelectedFilesNotification(); - } - - private void init( - Project project, - SelectedFilesNotification selectedFilesNotification, - Disposable parentDisposable) { - var conversation = ConversationsState.getCurrentConversation(); - if (conversation == null) { - conversation = ConversationService.getInstance().startConversation(); - } - - var tabPanel = new StandardChatToolWindowTabPanel(project, conversation); - tabbedPane = createTabbedPane(tabPanel, parentDisposable); - Runnable onAddNewTab = () -> { - tabbedPane.addNewTab(new StandardChatToolWindowTabPanel( - project, - ConversationService.getInstance().startConversation())); - repaint(); - revalidate(); - }; - var actionToolbarPanel = new JPanel(new BorderLayout()); - actionToolbarPanel.add( - createActionToolbar(project, tabbedPane, onAddNewTab).getComponent(), - BorderLayout.LINE_START); - - setToolbar(actionToolbarPanel); - setContent( - JBUI.Panels.simplePanel(tabbedPane).addToBottom(selectedFilesNotification)); - - Disposer.register(parentDisposable, tabPanel); - } - - private ActionToolbar createActionToolbar( - Project project, - StandardChatToolWindowTabbedPane tabbedPane, - Runnable onAddNewTab) { - var actionGroup = new DefaultCompactActionGroup("TOOLBAR_ACTION_GROUP", false); - actionGroup.add(new CreateNewConversationAction(onAddNewTab)); - actionGroup.add( - new ClearChatWindowAction(() -> tabbedPane.resetCurrentlyActiveTabPanel(project))); - actionGroup.addSeparator(); - actionGroup.add(new OpenInEditorAction()); - - var toolbar = ActionManager.getInstance() - .createActionToolbar("NAVIGATION_BAR_TOOLBAR", actionGroup, true); - toolbar.setTargetComponent(this); - return toolbar; - } - - private StandardChatToolWindowTabbedPane createTabbedPane( - StandardChatToolWindowTabPanel tabPanel, - Disposable parentDisposable) { - var tabbedPane = new StandardChatToolWindowTabbedPane(parentDisposable); - tabbedPane.addNewTab(tabPanel); - return tabbedPane; - } - - public StandardChatToolWindowTabbedPane getChatTabbedPane() { - return tabbedPane; - } -} diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/StandardChatToolWindowTabPanel.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/StandardChatToolWindowTabPanel.java deleted file mode 100644 index ef340acf..00000000 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/StandardChatToolWindowTabPanel.java +++ /dev/null @@ -1,78 +0,0 @@ -package ee.carlrobert.codegpt.toolwindow.chat.standard; - -import static java.lang.String.format; - -import com.intellij.openapi.editor.impl.EditorImpl; -import com.intellij.openapi.project.Project; -import ee.carlrobert.codegpt.completions.ConversationType; -import ee.carlrobert.codegpt.conversations.Conversation; -import ee.carlrobert.codegpt.conversations.message.Message; -import ee.carlrobert.codegpt.settings.service.you.YouSettings; -import ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowTabPanel; -import ee.carlrobert.codegpt.toolwindow.chat.ui.ChatMessageResponseBody; -import ee.carlrobert.codegpt.toolwindow.chat.ui.ResponsePanel; -import ee.carlrobert.codegpt.toolwindow.chat.ui.UserMessagePanel; -import ee.carlrobert.codegpt.ui.OverlayUtil; -import ee.carlrobert.codegpt.util.EditorUtil; -import ee.carlrobert.codegpt.util.file.FileUtil; -import javax.swing.JComponent; -import org.jetbrains.annotations.NotNull; - -public class StandardChatToolWindowTabPanel extends ChatToolWindowTabPanel { - - public StandardChatToolWindowTabPanel( - @NotNull Project project, - @NotNull Conversation conversation) { - super(project, conversation, false); - if (conversation.getMessages().isEmpty()) { - displayLandingView(); - } else { - displayConversation(conversation); - } - } - - @Override - protected JComponent getLandingView() { - return new StandardChatToolWindowLandingPanel((action, locationOnScreen) -> { - var editor = EditorUtil.getSelectedEditor(project); - if (editor == null || !editor.getSelectionModel().hasSelection()) { - OverlayUtil.showWarningBalloon( - editor == null ? "Unable to locate a selected editor" - : "Please select a target code before proceeding", - locationOnScreen); - return; - } - - var fileExtension = FileUtil.getFileExtension( - ((EditorImpl) editor).getVirtualFile().getName()); - var message = new Message(action.getPrompt().replace( - "{{selectedCode}}", - format("\n```%s\n%s\n```", fileExtension, editor.getSelectionModel().getSelectedText()))); - message.setUserMessage(action.getUserMessage()); - - sendMessage(message, ConversationType.DEFAULT); - }); - } - - private void displayConversation(@NotNull Conversation conversation) { - clearWindow(); - conversation.getMessages().forEach(message -> { - var messageResponseBody = - new ChatMessageResponseBody(project, this).withResponse(message.getResponse()); - - var serpResults = message.getSerpResults(); - if (YouSettings.getCurrentState().isDisplayWebSearchResults() - && serpResults != null && !serpResults.isEmpty()) { - messageResponseBody.displaySerpResults(serpResults); - } - messageResponseBody.hideCaret(); - - var messagePanel = toolWindowScrollablePanel.addMessage(message.getId()); - messagePanel.add(new UserMessagePanel(project, message, this)); - messagePanel.add(new ResponsePanel() - .withReloadAction(() -> reloadMessage(message, conversation, ConversationType.DEFAULT)) - .withDeleteAction(() -> removeMessage(message.getId(), conversation)) - .addContent(messageResponseBody)); - }); - } -} diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/ImageAccordion.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/ImageAccordion.java new file mode 100644 index 00000000..250be68e --- /dev/null +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/ImageAccordion.java @@ -0,0 +1,86 @@ +package ee.carlrobert.codegpt.toolwindow.chat.ui; + +import static com.intellij.util.ui.JBUI.Panels.simplePanel; + +import com.intellij.icons.AllIcons.General; +import com.intellij.ui.components.JBLabel; +import com.intellij.util.ui.JBUI; +import ee.carlrobert.codegpt.CodeGPTBundle; +import java.awt.BorderLayout; +import java.awt.Graphics2D; +import java.awt.Image; +import java.awt.event.ItemEvent; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import javax.imageio.ImageIO; +import javax.swing.BoxLayout; +import javax.swing.ImageIcon; +import javax.swing.JPanel; +import javax.swing.JToggleButton; +import javax.swing.SwingConstants; + +public class ImageAccordion extends JPanel { + + public ImageAccordion(String fileName, byte[] imageData) { + super(new BorderLayout()); + setOpaque(false); + + var contentPanel = createContentPanel(fileName, imageData); + add(createToggleButton(contentPanel), BorderLayout.NORTH); + add(contentPanel, BorderLayout.CENTER); + } + + private JPanel createContentPanel(String fileName, byte[] imageData) { + var panel = new JPanel(); + panel.setOpaque(false); + panel.setVisible(true); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + panel.setBorder(JBUI.Borders.empty(4, 0)); + try { + ByteArrayInputStream inputStream = new ByteArrayInputStream(imageData); + BufferedImage originalImage = ImageIO.read(inputStream); + int maxHeight = 80; + BufferedImage resizedImage = resizeImage(originalImage, maxHeight); + panel.add(simplePanel() + .andTransparent() + .addToTop( + new JBLabel("%s".formatted(fileName)) + .withBorder(JBUI.Borders.emptyBottom(4))) + .addToLeft(new JBLabel(new ImageIcon(resizedImage))), BorderLayout.LINE_START); + } catch (IOException e) { + panel.add(new JBLabel("ERROR: Something went wrong while reading the image")); + throw new RuntimeException(e); + } + return panel; + } + + private JToggleButton createToggleButton(JPanel contentPane) { + var accordionToggle = new JToggleButton( + CodeGPTBundle.get("imageAccordion.title"), + General.ArrowDown); + accordionToggle.setFocusPainted(false); + accordionToggle.setContentAreaFilled(false); + accordionToggle.setBackground(getBackground()); + accordionToggle.setSelectedIcon(General.ArrowUp); + accordionToggle.setBorder(null); + accordionToggle.setSelected(true); + accordionToggle.setHorizontalAlignment(SwingConstants.LEADING); + accordionToggle.setHorizontalTextPosition(SwingConstants.LEADING); + accordionToggle.addItemListener(e -> + contentPane.setVisible(e.getStateChange() == ItemEvent.SELECTED)); + return accordionToggle; + } + + private BufferedImage resizeImage(BufferedImage originalImage, int maxHeight) { + double aspectRatio = (double) originalImage.getWidth() / originalImage.getHeight(); + int newWidth = (int) (maxHeight * aspectRatio); + Image resizedImage = originalImage.getScaledInstance(newWidth, maxHeight, Image.SCALE_SMOOTH); + BufferedImage bufferedResizedImage = new BufferedImage(newWidth, maxHeight, + BufferedImage.TYPE_INT_RGB); + Graphics2D g2d = bufferedResizedImage.createGraphics(); + g2d.drawImage(resizedImage, 0, 0, null); + g2d.dispose(); + return bufferedResizedImage; + } +} diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/SelectedFilesNotification.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/SelectedFilesNotification.java deleted file mode 100644 index 0b0072b2..00000000 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/SelectedFilesNotification.java +++ /dev/null @@ -1,86 +0,0 @@ -package ee.carlrobert.codegpt.toolwindow.chat.ui; - -import static java.lang.String.format; -import static java.util.Collections.emptyList; - -import com.intellij.icons.AllIcons.General; -import com.intellij.openapi.project.Project; -import com.intellij.ui.JBColor; -import com.intellij.ui.components.ActionLink; -import com.intellij.ui.components.JBLabel; -import com.intellij.util.ui.JBUI; -import com.intellij.util.ui.JBUI.CurrentTheme.NotificationInfo; -import ee.carlrobert.codegpt.CodeGPTKeys; -import ee.carlrobert.codegpt.ReferencedFile; -import ee.carlrobert.codegpt.actions.IncludeFilesInContextNotifier; -import java.awt.BorderLayout; -import java.nio.file.Paths; -import java.util.List; -import java.util.stream.Collectors; -import javax.swing.JPanel; -import javax.swing.SwingConstants; -import org.jetbrains.annotations.NotNull; - -public class SelectedFilesNotification extends JPanel { - - private final Project project; - private final JBLabel label; - - public SelectedFilesNotification(@NotNull Project project) { - super(new BorderLayout()); - this.project = project; - this.label = new JBLabel( - getSelectedFilesLabel(), - General.BalloonInformation, - SwingConstants.LEADING); - - setVisible(false); - setBorder(JBUI.Borders.compound( - JBUI.Borders.customLine(JBColor.border(), 1, 0, 0, 0), - JBUI.Borders.empty(8, 12))); - - setBackground(NotificationInfo.backgroundColor()); - setForeground(NotificationInfo.foregroundColor()); - add(label, BorderLayout.LINE_START); - add(new ActionLink("Remove", (event) -> { - clearSelectedFilesNotification(); - }), BorderLayout.LINE_END); - } - - public void displaySelectedFilesNotification(@NotNull List referencedFiles) { - if (referencedFiles.isEmpty()) { - return; - } - - label.setText(referencedFiles.size() + " files selected"); - var referencedFilePaths = referencedFiles.stream() - .map(ReferencedFile::getFilePath) - .collect(Collectors.toList()); - label.setToolTipText(getHtml(referencedFilePaths)); - setVisible(true); - } - - public void clearSelectedFilesNotification() { - project.putUserData(CodeGPTKeys.SELECTED_FILES, emptyList()); - project.getMessageBus() - .syncPublisher(IncludeFilesInContextNotifier.FILES_INCLUDED_IN_CONTEXT_TOPIC) - .filesIncluded(emptyList()); - - label.setText("0 files selected"); - label.setToolTipText(null); - setVisible(false); - } - - private String getHtml(List referencedFilePaths) { - var html = referencedFilePaths.stream() - .map(filePath -> format("
  • %s
  • ", Paths.get(filePath).getFileName().toString())) - .collect(Collectors.joining()); - return format("
      %s
    ", html); - } - - private String getSelectedFilesLabel() { - var selectedFiles = project.getUserData(CodeGPTKeys.SELECTED_FILES); - var fileCount = selectedFiles == null ? 0 : selectedFiles.size(); - return fileCount + " files selected"; - } -} diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/ToolWindowFooterNotification.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/ToolWindowFooterNotification.java new file mode 100644 index 00000000..52bcc66f --- /dev/null +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/ToolWindowFooterNotification.java @@ -0,0 +1,50 @@ +package ee.carlrobert.codegpt.toolwindow.chat.ui; + +import com.intellij.icons.AllIcons.General; +import com.intellij.ui.JBColor; +import com.intellij.ui.components.ActionLink; +import com.intellij.ui.components.JBLabel; +import com.intellij.util.ui.JBUI; +import com.intellij.util.ui.JBUI.CurrentTheme.NotificationInfo; +import java.awt.BorderLayout; +import javax.swing.JPanel; +import javax.swing.SwingConstants; + +public class ToolWindowFooterNotification extends JPanel { + + private final JBLabel label; + + public ToolWindowFooterNotification(Runnable onRemove) { + this("", onRemove); + } + + public ToolWindowFooterNotification(String text, Runnable onRemove) { + super(new BorderLayout()); + this.label = new JBLabel(text, General.BalloonInformation, SwingConstants.LEADING); + + setVisible(false); + setBorder(JBUI.Borders.compound( + JBUI.Borders.customLine(JBColor.border(), 1, 0, 0, 0), + JBUI.Borders.empty(8, 12))); + + setBackground(NotificationInfo.backgroundColor()); + setForeground(NotificationInfo.foregroundColor()); + add(label, BorderLayout.LINE_START); + add(new ActionLink("Remove", (event) -> { + hideNotification(); + onRemove.run(); + }), BorderLayout.LINE_END); + } + + public void show(String text, String toolTipText) { + label.setText(text); + label.setToolTipText(toolTipText); + setVisible(true); + } + + public void hideNotification() { + label.setText(""); + label.setToolTipText(null); + setVisible(false); + } +} diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/UserMessagePanel.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/UserMessagePanel.java index 5e723ed4..5db6de56 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/UserMessagePanel.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/UserMessagePanel.java @@ -1,5 +1,6 @@ package ee.carlrobert.codegpt.toolwindow.chat.ui; +import com.intellij.icons.AllIcons.General; import com.intellij.openapi.Disposable; import com.intellij.openapi.project.Project; import com.intellij.ui.ColorUtil; @@ -11,6 +12,9 @@ import ee.carlrobert.codegpt.Icons; import ee.carlrobert.codegpt.conversations.message.Message; import ee.carlrobert.codegpt.settings.GeneralSettings; import java.awt.BorderLayout; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; import javax.swing.JPanel; import javax.swing.SwingConstants; @@ -39,6 +43,18 @@ public class UserMessagePanel extends JPanel { } } + public void displayImage(String imageFilePath) { + try { + var path = Paths.get(imageFilePath); + add(new ImageAccordion(path.getFileName().toString(), Files.readAllBytes(path))); + } catch (IOException e) { + add(new JBLabel( + "Unable to load image %s".formatted(imageFilePath), + General.Error, + SwingConstants.LEFT)); + } + } + private ChatMessageResponseBody createResponseBody( Project project, String prompt, diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/AttachImageNotifier.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/AttachImageNotifier.java new file mode 100644 index 00000000..77acddf2 --- /dev/null +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/AttachImageNotifier.java @@ -0,0 +1,11 @@ +package ee.carlrobert.codegpt.toolwindow.chat.ui.textarea; + +import com.intellij.util.messages.Topic; + +public interface AttachImageNotifier { + + Topic IMAGE_ATTACHMENT_FILE_PATH_TOPIC = + Topic.create("imageAttachmentFilePath", AttachImageNotifier.class); + + void imageAttached(String filePath); +} diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/ModelComboBoxAction.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/ModelComboBoxAction.java index d7f11dc6..1d1b14b5 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/ModelComboBoxAction.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/ModelComboBoxAction.java @@ -18,8 +18,6 @@ import ee.carlrobert.codegpt.Icons; import ee.carlrobert.codegpt.completions.llama.LlamaModel; import ee.carlrobert.codegpt.completions.you.YouUserManager; import ee.carlrobert.codegpt.completions.you.auth.SignedOutNotifier; -import ee.carlrobert.codegpt.conversations.ConversationService; -import ee.carlrobert.codegpt.conversations.ConversationsState; import ee.carlrobert.codegpt.settings.GeneralSettings; import ee.carlrobert.codegpt.settings.GeneralSettingsState; import ee.carlrobert.codegpt.settings.service.ServiceType; @@ -39,13 +37,13 @@ import org.jetbrains.annotations.NotNull; public class ModelComboBoxAction extends ComboBoxAction { - private final Runnable onAddNewTab; + private final Runnable onModelChange; private final GeneralSettingsState settings; private final OpenAISettingsState openAISettings; private final YouSettingsState youSettings; - public ModelComboBoxAction(Runnable onAddNewTab, ServiceType selectedService) { - this.onAddNewTab = onAddNewTab; + public ModelComboBoxAction(Runnable onModelChange, ServiceType selectedService) { + this.onModelChange = onModelChange; settings = GeneralSettings.getCurrentState(); openAISettings = OpenAISettings.getCurrentState(); youSettings = YouSettings.getCurrentState(); @@ -74,6 +72,7 @@ public class ModelComboBoxAction extends ComboBoxAction { var actionGroup = new DefaultActionGroup(); actionGroup.addSeparator("OpenAI"); List.of( + OpenAIChatCompletionModel.GPT_4_VISION_PREVIEW, OpenAIChatCompletionModel.GPT_4_0125_128k, OpenAIChatCompletionModel.GPT_3_5_0125_16k, OpenAIChatCompletionModel.GPT_4_32k, @@ -210,7 +209,7 @@ public class ModelComboBoxAction extends ComboBoxAction { @Override public void actionPerformed(@NotNull AnActionEvent e) { - handleProviderChange(serviceType, label, icon, comboBoxPresentation); + handleModelChange(serviceType, label, icon, comboBoxPresentation); } @Override @@ -220,7 +219,7 @@ public class ModelComboBoxAction extends ComboBoxAction { }; } - private void handleProviderChange( + private void handleModelChange( ServiceType serviceType, String label, Icon icon, @@ -228,13 +227,7 @@ public class ModelComboBoxAction extends ComboBoxAction { settings.setSelectedService(serviceType); comboBoxPresentation.setIcon(icon); comboBoxPresentation.setText(label); - - var currentConversation = ConversationsState.getCurrentConversation(); - if (currentConversation != null && !currentConversation.getMessages().isEmpty()) { - onAddNewTab.run(); - } else { - ConversationService.getInstance().startConversation(); - } + onModelChange.run(); } private AnAction createOpenAIModelAction( @@ -252,7 +245,7 @@ public class ModelComboBoxAction extends ComboBoxAction { @Override public void actionPerformed(@NotNull AnActionEvent e) { openAISettings.setModel(model.getCode()); - handleProviderChange( + handleModelChange( OPENAI, model.getDescription(), Icons.OpenAI, @@ -281,7 +274,7 @@ public class ModelComboBoxAction extends ComboBoxAction { @Override public void actionPerformed(@NotNull AnActionEvent e) { youSettings.setChatMode(mode); - handleProviderChange( + handleModelChange( YOU, mode.getDescription(), Icons.YouSmall, @@ -311,7 +304,7 @@ public class ModelComboBoxAction extends ComboBoxAction { public void actionPerformed(@NotNull AnActionEvent e) { youSettings.setCustomModel(model); youSettings.setChatMode(YouCompletionMode.CUSTOM); - handleProviderChange( + handleModelChange( YOU, model.getDescription(), Icons.YouSmall, diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/TotalTokensPanel.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/TotalTokensPanel.java index 88763678..c9b0da5c 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/TotalTokensPanel.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/TotalTokensPanel.java @@ -17,6 +17,8 @@ import ee.carlrobert.codegpt.EncodingManager; import ee.carlrobert.codegpt.ReferencedFile; import ee.carlrobert.codegpt.actions.IncludeFilesInContextNotifier; import ee.carlrobert.codegpt.conversations.Conversation; +import ee.carlrobert.codegpt.settings.GeneralSettings; +import ee.carlrobert.codegpt.settings.service.ServiceType; import java.awt.FlowLayout; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; @@ -154,12 +156,28 @@ public class TotalTokensPanel extends JPanel { entry.getKey(), entry.getValue())) .collect(Collectors.joining()); - iconLabel.setToolTipText("" + html + ""); + iconLabel.setToolTipText(getIconToolTipText(html)); } }); return iconLabel; } + private String getIconToolTipText(String html) { + if (GeneralSettings.getCurrentState().getSelectedService() != ServiceType.OPENAI) { + return """ + + + ⓘ Keep in mind that the output values might vary across different + large language models due to variations in their encoding methods. + +

    + %s + """.formatted(html); + } + return ""; + } + private String getLabelHtml(int total) { return format("Tokens: %d", total); } diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/UserPromptTextArea.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/UserPromptTextArea.java index aca839b6..fe2e302f 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/UserPromptTextArea.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/UserPromptTextArea.java @@ -1,6 +1,12 @@ package ee.carlrobert.codegpt.toolwindow.chat.ui.textarea; +import static ee.carlrobert.codegpt.settings.service.ServiceType.ANTHROPIC; +import static ee.carlrobert.codegpt.settings.service.ServiceType.OPENAI; +import static ee.carlrobert.llm.client.openai.completion.OpenAIChatCompletionModel.GPT_4_VISION_PREVIEW; + import com.intellij.icons.AllIcons; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.ex.util.EditorUtil; import com.intellij.openapi.util.registry.Registry; @@ -10,11 +16,14 @@ import com.intellij.ui.components.JBTextArea; import com.intellij.util.ui.JBUI; import ee.carlrobert.codegpt.CodeGPTBundle; import ee.carlrobert.codegpt.Icons; +import ee.carlrobert.codegpt.actions.AttachImageAction; import ee.carlrobert.codegpt.completions.CompletionRequestHandler; +import ee.carlrobert.codegpt.settings.GeneralSettings; +import ee.carlrobert.codegpt.settings.service.openai.OpenAISettings; +import ee.carlrobert.codegpt.ui.IconActionButton; import ee.carlrobert.codegpt.ui.UIUtil; import java.awt.BasicStroke; import java.awt.BorderLayout; -import java.awt.Cursor; import java.awt.FlowLayout; import java.awt.Graphics; import java.awt.Graphics2D; @@ -23,16 +32,14 @@ import java.awt.RenderingHints; import java.awt.event.ActionEvent; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import javax.swing.AbstractAction; -import javax.swing.Icon; -import javax.swing.JButton; import javax.swing.JPanel; import javax.swing.UIManager; import javax.swing.event.DocumentEvent; import javax.swing.text.BadLocationException; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; public class UserPromptTextArea extends JPanel { @@ -41,11 +48,12 @@ public class UserPromptTextArea extends JPanel { private static final JBColor BACKGROUND_COLOR = JBColor.namedColor( "Editor.SearchField.background", com.intellij.util.ui.UIUtil.getTextFieldBackground()); + private final AtomicReference requestHandlerRef = + new AtomicReference<>(); private final JBTextArea textArea; - private final int textAreaRadius = 16; private final Consumer onSubmit; - private JButton stopButton; + private IconActionButton stopButton; private JPanel iconsPanel; private boolean submitEnabled = true; @@ -82,12 +90,6 @@ public class UserPromptTextArea extends JPanel { UserPromptTextArea.super.paintBorder(UserPromptTextArea.super.getGraphics()); } }); - textArea.getDocument().addDocumentListener(new DocumentAdapter() { - @Override - protected void textChanged(@NotNull DocumentEvent e) { - iconsPanel.getComponents()[0].setEnabled(e.getDocument().getLength() > 0); - } - }); updateFont(); init(); } @@ -150,6 +152,10 @@ public class UserPromptTextArea extends JPanel { stopButton.setEnabled(!submitEnabled); } + public void setRequestHandler(@NotNull CompletionRequestHandler handler) { + requestHandlerRef.set(handler); + } + private void handleSubmit() { if (submitEnabled && !textArea.getText().isEmpty()) { // Replacing each newline with two newlines to ensure proper Markdown formatting @@ -163,12 +169,34 @@ public class UserPromptTextArea extends JPanel { setOpaque(false); add(textArea, BorderLayout.CENTER); - stopButton = createIconButton(AllIcons.Actions.Suspend, null); + stopButton = new IconActionButton( + new AnAction("Stop", "Stop current inference", AllIcons.Actions.Suspend) { + @Override + public void actionPerformed(@NotNull AnActionEvent e) { + var handler = requestHandlerRef.get(); + if (handler != null) { + handler.cancel(); + } + } + }); + stopButton.setEnabled(false); var flowLayout = new FlowLayout(FlowLayout.RIGHT); flowLayout.setHgap(8); iconsPanel = new JPanel(flowLayout); - iconsPanel.add(createIconButton(Icons.Send, this::handleSubmit)); + iconsPanel.add(new IconActionButton( + new AnAction("Send Message", "Send message", Icons.Send) { + @Override + public void actionPerformed(@NotNull AnActionEvent e) { + handleSubmit(); + } + })); + var selectedService = GeneralSettings.getCurrentState().getSelectedService(); + if (selectedService == ANTHROPIC + || (selectedService == OPENAI + && GPT_4_VISION_PREVIEW.getCode().equals(OpenAISettings.getCurrentState().getModel()))) { + iconsPanel.add(new IconActionButton(new AttachImageAction())); + } iconsPanel.add(stopButton); add(iconsPanel, BorderLayout.EAST); } @@ -180,19 +208,4 @@ public class UserPromptTextArea extends JPanel { textArea.setFont(UIManager.getFont("TextField.font")); } } - - // TODO: IconActionButton? - private JButton createIconButton(Icon icon, @Nullable Runnable submitListener) { - var button = UIUtil.createIconButton(icon); - if (submitListener != null) { - button.addActionListener((e) -> handleSubmit()); - } - button.setCursor(new Cursor(Cursor.HAND_CURSOR)); - button.setEnabled(false); - return button; - } - - public void setRequestHandler(@NotNull CompletionRequestHandler requestService) { - stopButton.addActionListener(e -> requestService.cancel()); - } } diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/UserPromptTextAreaHeader.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/UserPromptTextAreaHeader.java deleted file mode 100644 index d350d857..00000000 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/UserPromptTextAreaHeader.java +++ /dev/null @@ -1,57 +0,0 @@ -package ee.carlrobert.codegpt.toolwindow.chat.ui.textarea; - -import com.intellij.openapi.actionSystem.ActionPlaces; -import com.intellij.openapi.application.ApplicationManager; -import com.intellij.ui.components.JBCheckBox; -import com.intellij.util.messages.MessageBusConnection; -import com.intellij.util.ui.JBUI; -import ee.carlrobert.codegpt.completions.you.YouSubscriptionNotifier; -import ee.carlrobert.codegpt.completions.you.auth.SignedOutNotifier; -import ee.carlrobert.codegpt.settings.service.ServiceType; -import java.awt.BorderLayout; -import javax.swing.JPanel; - -public class UserPromptTextAreaHeader extends JPanel { - - public UserPromptTextAreaHeader( - ServiceType selectedService, - TotalTokensPanel totalTokensPanel, - Runnable onAddNewTab) { - super(new BorderLayout()); - setOpaque(false); - setBorder(JBUI.Borders.emptyBottom(8)); - switch (selectedService) { - case OPENAI: - case AZURE: - add(totalTokensPanel, BorderLayout.LINE_START); - break; - case YOU: - break; - default: - } - add(new ModelComboBoxAction(onAddNewTab, selectedService) - .createCustomComponent(ActionPlaces.UNKNOWN), BorderLayout.LINE_END); - } - - private void subscribeToYouTopics(JBCheckBox gpt4CheckBox) { - var messageBusConnection = ApplicationManager.getApplication().getMessageBus().connect(); - subscribeToYouSubscriptionTopic(messageBusConnection, gpt4CheckBox); - subscribeToSignedOutTopic(messageBusConnection, gpt4CheckBox); - } - - private void subscribeToSignedOutTopic( - MessageBusConnection messageBusConnection, - JBCheckBox gpt4CheckBox) { - messageBusConnection.subscribe( - SignedOutNotifier.SIGNED_OUT_TOPIC, - (SignedOutNotifier) () -> gpt4CheckBox.setEnabled(false)); - } - - private void subscribeToYouSubscriptionTopic( - MessageBusConnection messageBusConnection, - JBCheckBox gpt4CheckBox) { - messageBusConnection.subscribe( - YouSubscriptionNotifier.SUBSCRIPTION_TOPIC, - (YouSubscriptionNotifier) () -> gpt4CheckBox.setEnabled(true)); - } -} diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/conversations/ConversationPanel.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/conversations/ConversationPanel.java index 4e82f767..d3386062 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/conversations/ConversationPanel.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/conversations/ConversationPanel.java @@ -9,7 +9,7 @@ import ee.carlrobert.codegpt.actions.toolwindow.DeleteConversationAction; import ee.carlrobert.codegpt.conversations.Conversation; import ee.carlrobert.codegpt.conversations.ConversationsState; import ee.carlrobert.codegpt.settings.GeneralSettings; -import ee.carlrobert.codegpt.toolwindow.chat.standard.StandardChatToolWindowContentManager; +import ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowContentManager; import ee.carlrobert.codegpt.ui.IconActionButton; import ee.carlrobert.codegpt.ui.ModelIconLabel; import java.awt.BorderLayout; @@ -30,12 +30,12 @@ class ConversationPanel extends JPanel { @NotNull Conversation conversation, @NotNull Runnable onDelete) { super(new BorderLayout()); - var toolWindowContentManager = project.getService(StandardChatToolWindowContentManager.class); + var toolWindowContentManager = project.getService(ChatToolWindowContentManager.class); init(toolWindowContentManager, conversation, onDelete); } private void init( - StandardChatToolWindowContentManager toolWindowContentManager, + ChatToolWindowContentManager toolWindowContentManager, Conversation conversation, Runnable onDelete) { setBackground(JBColor.background()); diff --git a/src/main/java/ee/carlrobert/codegpt/util/file/FileUtil.java b/src/main/java/ee/carlrobert/codegpt/util/file/FileUtil.java index 1ec5746d..9a898f08 100644 --- a/src/main/java/ee/carlrobert/codegpt/util/file/FileUtil.java +++ b/src/main/java/ee/carlrobert/codegpt/util/file/FileUtil.java @@ -141,6 +141,15 @@ public class FileUtil { } } + public static String getImageMediaType(String fileName) { + var fileExtension = getFileExtension(fileName); + return switch (fileExtension) { + case "png" -> "image/png"; + case "jpg", "jpeg" -> "image/jpeg"; + default -> throw new IllegalArgumentException("Unsupported image type: " + fileExtension); + }; + } + public static String getResourceContent(String name) { try (var stream = Objects.requireNonNull(FileUtil.class.getResourceAsStream(name))) { return new String(stream.readAllBytes(), StandardCharsets.UTF_8); diff --git a/src/main/kotlin/ee/carlrobert/codegpt/CodeGPTProjectActivity.kt b/src/main/kotlin/ee/carlrobert/codegpt/CodeGPTProjectActivity.kt index f8cfc21e..13317571 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/CodeGPTProjectActivity.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/CodeGPTProjectActivity.kt @@ -1,9 +1,11 @@ package ee.carlrobert.codegpt +import com.intellij.notification.NotificationAction import com.intellij.notification.NotificationType import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.project.Project import com.intellij.openapi.startup.ProjectActivity +import com.intellij.openapi.util.Disposer import ee.carlrobert.codegpt.actions.editor.EditorActionsUtil import ee.carlrobert.codegpt.completions.you.YouUserManager import ee.carlrobert.codegpt.completions.you.auth.AuthenticationHandler @@ -13,8 +15,11 @@ import ee.carlrobert.codegpt.completions.you.auth.response.YouAuthenticationResp import ee.carlrobert.codegpt.credentials.CredentialsStore import ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey import ee.carlrobert.codegpt.credentials.CredentialsStore.getCredential +import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings import ee.carlrobert.codegpt.settings.service.you.YouSettings +import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.AttachImageNotifier import ee.carlrobert.codegpt.ui.OverlayUtil +import java.nio.file.Paths class CodeGPTProjectActivity : ProjectActivity { @@ -23,12 +28,24 @@ class CodeGPTProjectActivity : ProjectActivity { CredentialsStore.loadAll() if (YouUserManager.getInstance().authenticationResponse == null) { - ApplicationManager.getApplication() - .executeOnPooledThread { this.handleYouServiceAuthentication() } + handleYouServiceAuthenticationAsync() + } + + if (!ApplicationManager.getApplication().isUnitTestMode + && ConfigurationSettings.getCurrentState().isCheckForNewScreenshots + ) { + val pathToWatch = Paths.get(System.getProperty("user.home"), "Desktop") + val fileWatcher = FileWatcher(pathToWatch) + fileWatcher.watch { + if (listOf("jpg", "jpeg", "png").contains(it.extension)) { + showImageAttachmentNotification(project, it.absolutePath) + } + } + Disposer.register(project, fileWatcher) } } - private fun handleYouServiceAuthentication() { + private fun handleYouServiceAuthenticationAsync() { val settings = YouSettings.getCurrentState() val password = getCredential(CredentialKey.YOU_ACCOUNT_PASSWORD) if (settings.email.isNotEmpty() && !password.isNullOrEmpty()) { @@ -57,4 +74,27 @@ class CodeGPTProjectActivity : ProjectActivity { }) } } + + private fun showImageAttachmentNotification(project: Project, filePath: String) { + OverlayUtil.getDefaultNotification( + CodeGPTBundle.get("imageAttachmentNotification.content"), + NotificationType.INFORMATION + ) + .addAction(NotificationAction.createSimpleExpiring( + CodeGPTBundle.get("imageAttachmentNotification.action") + ) { + CodeGPTKeys.IMAGE_ATTACHMENT_FILE_PATH.set(project, filePath) + project.messageBus + .syncPublisher( + AttachImageNotifier.IMAGE_ATTACHMENT_FILE_PATH_TOPIC + ) + .imageAttached(filePath) + }) + .addAction(NotificationAction.createSimpleExpiring( + CodeGPTBundle.get("shared.notification.doNotShowAgain") + ) { + ConfigurationSettings.getCurrentState().isCheckForNewScreenshots = false + }) + .notify(project) + } } \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/CodeGPTUpdateActivity.kt b/src/main/kotlin/ee/carlrobert/codegpt/CodeGPTUpdateActivity.kt index af31c937..505c5bea 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/CodeGPTUpdateActivity.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/CodeGPTUpdateActivity.kt @@ -1,4 +1,4 @@ -package ee.carlrobert.codegpt; +package ee.carlrobert.codegpt import com.intellij.ide.plugins.InstalledPluginsState import com.intellij.notification.NotificationAction @@ -39,7 +39,7 @@ class CodeGPTUpdateActivity : ProjectActivity { Task.Backgroundable(project, CodeGPTBundle.get("checkForUpdatesTask.title"), true) { override fun run(indicator: ProgressIndicator) { val isLatestVersion = - !InstalledPluginsState.getInstance().hasNewerVersion(CodeGPTPlugin.CODEGPT_ID); + !InstalledPluginsState.getInstance().hasNewerVersion(CodeGPTPlugin.CODEGPT_ID) if (project.isDisposed || isLatestVersion) { return } @@ -55,7 +55,7 @@ class CodeGPTUpdateActivity : ProjectActivity { .executeOnPooledThread { installCodeGPTUpdate(project) } }) .addAction(NotificationAction.createSimpleExpiring( - CodeGPTBundle.get("checkForUpdatesTask.notification.hideButton") + CodeGPTBundle.get("shared.notification.doNotShowAgain") ) { ConfigurationSettings.getCurrentState().isCheckForPluginUpdates = false }) diff --git a/src/main/kotlin/ee/carlrobert/codegpt/FileWatcher.kt b/src/main/kotlin/ee/carlrobert/codegpt/FileWatcher.kt new file mode 100644 index 00000000..d1c20669 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/FileWatcher.kt @@ -0,0 +1,29 @@ +package ee.carlrobert.codegpt + +import com.intellij.openapi.Disposable +import org.apache.commons.io.monitor.FileAlterationListenerAdaptor +import org.apache.commons.io.monitor.FileAlterationMonitor +import org.apache.commons.io.monitor.FileAlterationObserver +import java.io.File +import java.nio.file.Path + +class FileWatcher(private val pathToWatch: Path) : Disposable { + + private val fileMonitor = + FileAlterationMonitor(500, FileAlterationObserver(pathToWatch.toFile())) + + fun watch(onFileCreated: (File) -> Unit) { + val observer = FileAlterationObserver(pathToWatch.toFile()) + observer.addListener(object : FileAlterationListenerAdaptor() { + override fun onFileCreate(file: File) { + onFileCreated(file) + } + }) + fileMonitor.addObserver(observer) + fileMonitor.start() + } + + override fun dispose() { + fileMonitor.stop() + } +} \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/actions/AttachImageAction.kt b/src/main/kotlin/ee/carlrobert/codegpt/actions/AttachImageAction.kt new file mode 100644 index 00000000..509ed8c2 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/actions/AttachImageAction.kt @@ -0,0 +1,40 @@ +package ee.carlrobert.codegpt.actions + +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.fileChooser.FileChooser +import com.intellij.openapi.fileChooser.FileChooserDescriptor +import ee.carlrobert.codegpt.CodeGPTBundle +import ee.carlrobert.codegpt.CodeGPTKeys +import ee.carlrobert.codegpt.Icons +import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.AttachImageNotifier + +class AttachImageAction : AnAction( + CodeGPTBundle.get("action.attachImage"), + CodeGPTBundle.get("action.attachImageDescription"), + Icons.Upload +) { + + override fun actionPerformed(e: AnActionEvent) { + FileChooser.chooseFiles(createSingleImageFileDescriptor(), e.project, null).also { files -> + if (files.isNotEmpty()) { + check(files.size == 1) { "Expected exactly one file to be selected" } + e.project?.let { project -> + CodeGPTKeys.IMAGE_ATTACHMENT_FILE_PATH[project] = files.first().path + project.messageBus + .syncPublisher(AttachImageNotifier.IMAGE_ATTACHMENT_FILE_PATH_TOPIC) + .imageAttached(files.first().path) + } + } + } + } + + private fun createSingleImageFileDescriptor() = FileChooserDescriptor( + true, false, false, false, false, false + ).apply { + withFileFilter { file -> + file.extension in listOf("jpg", "jpeg", "png") + } + withTitle(CodeGPTBundle.get("imageFileChooser.title")) + } +} \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/actions/CodeCompletionFeatureToggleActions.kt b/src/main/kotlin/ee/carlrobert/codegpt/actions/CodeCompletionFeatureToggleActions.kt new file mode 100644 index 00000000..c9dc223e --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/actions/CodeCompletionFeatureToggleActions.kt @@ -0,0 +1,54 @@ +package ee.carlrobert.codegpt.actions + +import com.intellij.openapi.actionSystem.ActionUpdateThread +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.project.DumbAwareAction +import ee.carlrobert.codegpt.settings.GeneralSettings +import ee.carlrobert.codegpt.settings.service.ServiceType +import ee.carlrobert.codegpt.settings.service.ServiceType.LLAMA_CPP +import ee.carlrobert.codegpt.settings.service.ServiceType.OPENAI +import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings +import ee.carlrobert.codegpt.settings.service.openai.OpenAISettings + +abstract class CodeCompletionFeatureToggleActions( + private val enableFeatureAction: Boolean +) : DumbAwareAction() { + + override fun actionPerformed(e: AnActionEvent) { + GeneralSettings.getCurrentState().selectedService + .takeIf { it in listOf(OPENAI, LLAMA_CPP) } + ?.also { selectedService -> + if (OPENAI == selectedService) { + OpenAISettings.getCurrentState().isCodeCompletionsEnabled = enableFeatureAction + } else { + LlamaSettings.getCurrentState().isCodeCompletionsEnabled = enableFeatureAction + } + } + } + + override fun update(e: AnActionEvent) { + val selectedService = GeneralSettings.getCurrentState().selectedService + val codeCompletionEnabled = isCodeCompletionsEnabled(selectedService) + e.presentation.isEnabled = codeCompletionEnabled != enableFeatureAction + e.presentation.isVisible = + e.presentation.isEnabled && listOf(OPENAI, LLAMA_CPP).contains( + selectedService + ) + } + + override fun getActionUpdateThread(): ActionUpdateThread { + return ActionUpdateThread.BGT + } + + private fun isCodeCompletionsEnabled(serviceType: ServiceType): Boolean { + return when (serviceType) { + OPENAI -> OpenAISettings.getCurrentState().isCodeCompletionsEnabled + LLAMA_CPP -> LlamaSettings.getCurrentState().isCodeCompletionsEnabled + else -> false + } + } +} + +class EnableCompletionsAction : CodeCompletionFeatureToggleActions(true) + +class DisableCompletionsAction : CodeCompletionFeatureToggleActions(false) \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionRequestFactory.kt b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionRequestFactory.kt index 0738062c..26c35063 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionRequestFactory.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionRequestFactory.kt @@ -2,34 +2,34 @@ package ee.carlrobert.codegpt.codecompletions import ee.carlrobert.codegpt.completions.llama.LlamaModel import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings +import ee.carlrobert.codegpt.settings.service.llama.LlamaSettingsState +import ee.carlrobert.codegpt.settings.service.openai.OpenAISettings import ee.carlrobert.llm.client.llama.completion.LlamaCompletionRequest import ee.carlrobert.llm.client.openai.completion.request.OpenAITextCompletionRequest object CodeCompletionRequestFactory { - private const val MAX_TOKENS = 128 - fun buildOpenAIRequest(details: InfillRequestDetails): OpenAITextCompletionRequest { return OpenAITextCompletionRequest.Builder(details.prefix) .setSuffix(details.suffix) .setStream(true) - .setMaxTokens(MAX_TOKENS) + .setMaxTokens(OpenAISettings.getCurrentState().codeCompletionMaxTokens) .setTemperature(0.4) .build() } fun buildLlamaRequest(details: InfillRequestDetails): LlamaCompletionRequest { - val promptTemplate = getLlamaInfillPromptTemplate() + val settings = LlamaSettings.getCurrentState() + val promptTemplate = getLlamaInfillPromptTemplate(settings) val prompt = promptTemplate.buildPrompt(details.prefix, details.suffix) return LlamaCompletionRequest.Builder(prompt) - .setN_predict(MAX_TOKENS) + .setN_predict(settings.codeCompletionMaxTokens) .setStream(true) .setTemperature(0.4) .setStop(promptTemplate.stopTokens) .build() } - private fun getLlamaInfillPromptTemplate(): InfillPromptTemplate { - val settings = LlamaSettings.getCurrentState() + private fun getLlamaInfillPromptTemplate(settings: LlamaSettingsState): InfillPromptTemplate { if (!settings.isRunLocalServer) { return settings.remoteModelInfillPromptTemplate } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeGPTInlineCompletionProvider.kt b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeGPTInlineCompletionProvider.kt index 48f6890b..37045650 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeGPTInlineCompletionProvider.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeGPTInlineCompletionProvider.kt @@ -4,7 +4,6 @@ import com.intellij.codeInsight.inline.completion.InlineCompletionEvent import com.intellij.codeInsight.inline.completion.InlineCompletionProvider import com.intellij.codeInsight.inline.completion.InlineCompletionProviderID import com.intellij.codeInsight.inline.completion.InlineCompletionRequest -import com.intellij.codeInsight.inline.completion.elements.InlineCompletionElement import com.intellij.codeInsight.inline.completion.elements.InlineCompletionGrayTextElement import com.intellij.codeInsight.inline.completion.suggestion.InlineCompletionSingleSuggestion import com.intellij.codeInsight.inline.completion.suggestion.InlineCompletionSuggestionUpdateManager @@ -15,14 +14,18 @@ import com.intellij.codeInsight.inline.completion.suggestion.InlineCompletionVar import com.intellij.openapi.application.EDT import com.intellij.openapi.diagnostic.Logger import ee.carlrobert.codegpt.completions.CompletionRequestService -import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings +import ee.carlrobert.codegpt.settings.GeneralSettings +import ee.carlrobert.codegpt.settings.service.ServiceType +import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings +import ee.carlrobert.codegpt.settings.service.openai.OpenAISettings import ee.carlrobert.codegpt.treesitter.CodeCompletionParserFactory import ee.carlrobert.llm.completion.CompletionEventListener -import kotlinx.coroutines.* -import kotlinx.coroutines.channels.ProducerScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import okhttp3.sse.EventSource import java.util.concurrent.atomic.AtomicReference @@ -33,7 +36,6 @@ class CodeGPTInlineCompletionProvider : InlineCompletionProvider { } private val currentCall = AtomicReference(null) - private val providerScope = CoroutineScope(Dispatchers.Default + SupervisorJob()) override val id: InlineCompletionProviderID get() = InlineCompletionProviderID("CodeGPTInlineCompletionProvider") @@ -51,11 +53,18 @@ class CodeGPTInlineCompletionProvider : InlineCompletionProvider { val infillRequest = withContext(Dispatchers.EDT) { InfillRequestDetails.fromInlineCompletionRequest(request) } - cancelCurrentCall() currentCall.set( CompletionRequestService.getInstance().getCodeCompletionAsync( infillRequest, - getCodeCompletionEventListener(infillRequest) + CodeCompletionEventListener(infillRequest) { + launch { + try { + trySend(InlineCompletionGrayTextElement(it)) + } catch (e: Exception) { + LOG.error("Failed to send inline completion suggestion", e) + } + } + } ) ) awaitClose { cancelCurrentCall() } @@ -63,20 +72,13 @@ class CodeGPTInlineCompletionProvider : InlineCompletionProvider { } override fun isEnabled(event: InlineCompletionEvent): Boolean { - return event is InlineCompletionEvent.DocumentChange - && ConfigurationSettings.getCurrentState().isCodeCompletionsEnabled - } - - private fun ProducerScope.getCodeCompletionEventListener( - infillRequest: InfillRequestDetails - ) = CodeCompletionEventListener(infillRequest) { - providerScope.launch { - try { - send(InlineCompletionGrayTextElement(it)) - } catch (e: Exception) { - LOG.error("Failed to send inline completion suggestion", e) - } + val selectedService = GeneralSettings.getCurrentState().selectedService + val codeCompletionsEnabled = when (selectedService) { + ServiceType.OPENAI -> OpenAISettings.getCurrentState().isCodeCompletionsEnabled + ServiceType.LLAMA_CPP -> LlamaSettings.getCurrentState().isCodeCompletionsEnabled + else -> false } + return event is InlineCompletionEvent.DocumentChange && codeCompletionsEnabled } private fun cancelCurrentCall() { diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/CodeCompletionConfigurationForm.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/CodeCompletionConfigurationForm.kt new file mode 100644 index 00000000..1fddb3fa --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/CodeCompletionConfigurationForm.kt @@ -0,0 +1,49 @@ +package ee.carlrobert.codegpt.settings.service + +import com.intellij.openapi.ui.panel.ComponentPanelBuilder +import com.intellij.ui.components.JBCheckBox +import com.intellij.ui.components.fields.IntegerField +import com.intellij.util.ui.FormBuilder +import ee.carlrobert.codegpt.CodeGPTBundle +import javax.swing.JPanel + +class CodeCompletionConfigurationForm(codeCompletionsEnabled: Boolean, maxTokens: Int) { + + private val codeCompletionsEnabledCheckBox = JBCheckBox( + CodeGPTBundle.get("codeCompletionsForm.enableFeatureText"), + codeCompletionsEnabled + ) + private val codeCompletionMaxTokensField = + IntegerField("completion_max_tokens", 8, 4096).apply { + columns = 12 + value = maxTokens + } + + fun getForm(): JPanel { + return FormBuilder.createFormBuilder() + .addComponent(codeCompletionsEnabledCheckBox) + .addVerticalGap(4) + .addLabeledComponent( + CodeGPTBundle.get("codeCompletionsForm.maxTokensLabel"), + codeCompletionMaxTokensField + ) + .addComponentToRightColumn( + ComponentPanelBuilder.createCommentComponent( + CodeGPTBundle.get("codeCompletionsForm.maxTokensComment"), true, 48, true + ) + ) + .panel + } + + var isCodeCompletionsEnabled: Boolean + get() = codeCompletionsEnabledCheckBox.isSelected + set(enabled) { + codeCompletionsEnabledCheckBox.isSelected = enabled + } + + var maxTokens: Int + get() = codeCompletionMaxTokensField.value + set(maxTokens) { + codeCompletionMaxTokensField.value = maxTokens + } +} diff --git a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowListener.kt b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowListener.kt new file mode 100644 index 00000000..6c15f218 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowListener.kt @@ -0,0 +1,23 @@ +package ee.carlrobert.codegpt.toolwindow.chat + +import com.intellij.openapi.project.Project +import com.intellij.openapi.wm.ToolWindow +import com.intellij.openapi.wm.ex.ToolWindowManagerListener + +class ChatToolWindowListener : ToolWindowManagerListener { + + override fun toolWindowShown(toolWindow: ToolWindow) { + if ("CodeGPT" == toolWindow.id) { + requestFocusForTextArea(toolWindow.project) + } + } + + private fun requestFocusForTextArea(project: Project) { + val contentManager = project.getService(ChatToolWindowContentManager::class.java) + contentManager.tryFindChatTabbedPane().ifPresent { tabbedPane -> + tabbedPane.tryFindActiveTabPanel().ifPresent { tabPanel -> + tabPanel.requestFocusForTextArea() + } + } + } +} diff --git a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/ui/ChatToolWindowLandingPanel.kt b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/ui/ChatToolWindowLandingPanel.kt new file mode 100644 index 00000000..7318ea3b --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/ui/ChatToolWindowLandingPanel.kt @@ -0,0 +1,100 @@ +package ee.carlrobert.codegpt.toolwindow.ui + +import com.intellij.ui.components.ActionLink +import com.intellij.util.ui.JBUI +import ee.carlrobert.codegpt.Icons +import ee.carlrobert.codegpt.settings.GeneralSettings +import ee.carlrobert.codegpt.toolwindow.chat.ui.ResponsePanel +import ee.carlrobert.codegpt.ui.UIUtil.createTextPane +import java.awt.BorderLayout +import java.awt.Point +import java.awt.event.ActionListener +import javax.swing.Box +import javax.swing.BoxLayout +import javax.swing.JPanel + +class ChatToolWindowLandingPanel(onAction: (LandingPanelAction, Point) -> Unit) : ResponsePanel() { + + init { + addContent(createContent(onAction)) + } + + private fun createContent(onAction: (LandingPanelAction, Point) -> Unit): JPanel { + return JPanel(BorderLayout()).apply { + add(createTextPane(getWelcomeMessage(), false), BorderLayout.NORTH) + add(createActionsListPanel(onAction), BorderLayout.CENTER) + add(createTextPane(getCautionMessage(), false), BorderLayout.SOUTH) + } + } + + private fun createActionsListPanel(onAction: (LandingPanelAction, Point) -> Unit): JPanel { + val listPanel = JPanel() + listPanel.layout = BoxLayout(listPanel, BoxLayout.PAGE_AXIS) + listPanel.border = JBUI.Borders.emptyLeft(4) + listPanel.add(Box.createVerticalStrut(4)) + listPanel.add(createEditorActionLink(LandingPanelAction.WRITE_TESTS, onAction)) + listPanel.add(Box.createVerticalStrut(4)) + listPanel.add(createEditorActionLink(LandingPanelAction.EXPLAIN, onAction)) + listPanel.add(Box.createVerticalStrut(4)) + listPanel.add(createEditorActionLink(LandingPanelAction.FIND_BUGS, onAction)) + listPanel.add(Box.createVerticalStrut(4)) + return listPanel + } + + private fun createEditorActionLink( + action: LandingPanelAction, + onAction: (LandingPanelAction, Point) -> Unit + ): ActionLink { + return ActionLink(action.userMessage, ActionListener { event -> + onAction(action, (event.source as ActionLink).locationOnScreen) + }).apply { + icon = Icons.Sparkle + } + } + + private fun getWelcomeMessage(): String { + return """ + +

    + Welcome ${GeneralSettings.getCurrentState().displayName}, I'm your intelligent code companion, here to be your partner-in-crime for getting things done in a flash. +

    +

    + Feel free to ask me anything you'd like, but my true superpower lies in assisting you with your code! Here are a few examples of how I can assist you: +

    + + """.trimIndent() + } + + private fun getCautionMessage(): String { + return """ + +

    + Being an AI-powered assistant, I may occasionally have surprises or make mistakes. Therefore, it's wise to double-check any code or suggestions I provide. +

    + + """.trimIndent() + } +} + +enum class LandingPanelAction( + val label: String, + val userMessage: String, + val prompt: String +) { + FIND_BUGS( + "Find Bugs", + "Find bugs in the selected code", + "Find bugs and output code with bugs fixed in the selected code: {{selectedCode}}" + ), + WRITE_TESTS( + "Write Tests", + "Write unit tests for the selected code", + "Write unit tests for the selected code: {{selectedCode}}" + ), + EXPLAIN( + "Explain", + "Explain the selected code", + "Explain the selected code: {{selectedCode}}" + ) +} + diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 27da0491..0a72741c 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -9,6 +9,8 @@ + diff --git a/src/main/resources/icons/send.svg b/src/main/resources/icons/send.svg index 89cf3907..b6101d19 100644 --- a/src/main/resources/icons/send.svg +++ b/src/main/resources/icons/send.svg @@ -1,6 +1,6 @@ - + diff --git a/src/main/resources/icons/send_dark.svg b/src/main/resources/icons/send_dark.svg index 5dd4ba3b..d3d497c5 100644 --- a/src/main/resources/icons/send_dark.svg +++ b/src/main/resources/icons/send_dark.svg @@ -1,6 +1,6 @@ - + diff --git a/src/main/resources/icons/upload.svg b/src/main/resources/icons/upload.svg new file mode 100644 index 00000000..b26837f5 --- /dev/null +++ b/src/main/resources/icons/upload.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/main/resources/icons/upload_dark.svg b/src/main/resources/icons/upload_dark.svg new file mode 100644 index 00000000..2241155e --- /dev/null +++ b/src/main/resources/icons/upload_dark.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/main/resources/messages/codegpt.properties b/src/main/resources/messages/codegpt.properties index dc6c02ca..c31b2b00 100644 --- a/src/main/resources/messages/codegpt.properties +++ b/src/main/resources/messages/codegpt.properties @@ -87,6 +87,7 @@ configurationConfigurable.table.header.promptColumnLabel=Prompt configurationConfigurable.table.action.revertToDefaults.text=Revert to Defaults configurationConfigurable.table.action.addKeymap.text=Add Shortcut configurationConfigurable.checkForPluginUpdates.label=Check for plugin updates automatically +configurationConfigurable.checkForNewScreenshots.label=Check for new screenshots automatically configurationConfigurable.openNewTabCheckBox.label=Open a new chat on each action configurationConfigurable.enableMethodNameGeneration.label=Enable method name lookup suggestions configurationConfigurable.autoFormatting.label=Enable automatic code formatting @@ -178,7 +179,6 @@ validation.error.mustBeGreaterThanZero=Value must be greater than 0 checkForUpdatesTask.title=Checking for CodeGPT update... checkForUpdatesTask.notification.message=An update for CodeGPT is available. checkForUpdatesTask.notification.installButton=Install update -checkForUpdatesTask.notification.hideButton=Do not show again llamaServerAgent.buildingProject.description=Building llama.cpp... llamaServerAgent.serverBootup.description=Booting up server... notification.compilationError.description=CodeGPT has detected a compilation error. Would you like assistance in resolving it? @@ -190,4 +190,15 @@ shared.infillPromptTemplate=Infill template: shared.apiVersion=API version: shared.configuration=Configuration shared.port=Port: -codeCompletion.progress.title=Code completion in progress \ No newline at end of file +shared.notification.doNotShowAgain=Do not show again +codeCompletion.progress.title=Code completion in progress +imageAttachmentNotification.content=New image detected on desktop. Would you like to attach it to your current conversation? +imageAttachmentNotification.action=Attach image +action.attachImage=Attach Image +action.attachImageDescription=Attach an image +imageFileChooser.title=Select Image +imageAccordion.title=Attached image +shared.codeCompletions=Code Completions +codeCompletionsForm.enableFeatureText=Enable code completions +codeCompletionsForm.maxTokensLabel=Max tokens: +codeCompletionsForm.maxTokensComment=The maximum number of tokens that can be generated in the code completion. diff --git a/src/test/java/ee/carlrobert/codegpt/codecompletions/CodeCompletionServiceTest.java b/src/test/java/ee/carlrobert/codegpt/codecompletions/CodeCompletionServiceTest.java new file mode 100644 index 00000000..e69de29b diff --git a/src/test/java/ee/carlrobert/codegpt/completions/DefaultCompletionRequestHandlerTest.java b/src/test/java/ee/carlrobert/codegpt/completions/DefaultCompletionRequestHandlerTest.java index 5e1900d8..18fa3020 100644 --- a/src/test/java/ee/carlrobert/codegpt/completions/DefaultCompletionRequestHandlerTest.java +++ b/src/test/java/ee/carlrobert/codegpt/completions/DefaultCompletionRequestHandlerTest.java @@ -6,10 +6,8 @@ import static ee.carlrobert.llm.client.util.JSONUtil.e; import static ee.carlrobert.llm.client.util.JSONUtil.jsonArray; import static ee.carlrobert.llm.client.util.JSONUtil.jsonMap; import static ee.carlrobert.llm.client.util.JSONUtil.jsonMapResponse; -import static java.util.concurrent.TimeUnit.SECONDS; import static org.apache.http.HttpHeaders.AUTHORIZATION; import static org.assertj.core.api.Assertions.assertThat; -import static org.awaitility.Awaitility.await; import ee.carlrobert.codegpt.CodeGPTPlugin; import ee.carlrobert.codegpt.conversations.ConversationService; @@ -50,7 +48,7 @@ public class DefaultCompletionRequestHandlerTest extends IntegrationTest { requestHandler.call(new CallParameters(conversation, ConversationType.DEFAULT, message, false)); - await().atMost(5, SECONDS).until(() -> "Hello!".equals(message.getResponse())); + waitExpecting(() -> "Hello!".equals(message.getResponse())); } public void testAzureChatCompletionCall() { @@ -86,7 +84,7 @@ public class DefaultCompletionRequestHandlerTest extends IntegrationTest { requestHandler.call(new CallParameters(conversation, ConversationType.DEFAULT, message, false)); - await().atMost(5, SECONDS).until(() -> "Hello!".equals(message.getResponse())); + waitExpecting(() -> "Hello!".equals(message.getResponse())); } public void testYouChatCompletionCall() { @@ -137,7 +135,7 @@ public class DefaultCompletionRequestHandlerTest extends IntegrationTest { requestHandler.call(new CallParameters(conversation, ConversationType.DEFAULT, message, false)); - await().atMost(5, SECONDS).until(() -> "Hello!".equals(message.getResponse())); + waitExpecting(() -> "Hello!".equals(message.getResponse())); } public void testLlamaChatCompletionCall() { @@ -171,7 +169,7 @@ public class DefaultCompletionRequestHandlerTest extends IntegrationTest { requestHandler.call(new CallParameters(conversation, ConversationType.DEFAULT, message, false)); - await().atMost(5, SECONDS).until(() -> "Hello!".equals(message.getResponse())); + waitExpecting(() -> "Hello!".equals(message.getResponse())); } private CompletionResponseEventListener getRequestEventListener(Message message) { diff --git a/src/test/java/ee/carlrobert/codegpt/toolwindow/chat/StandardChatToolWindowTabPanelTest.java b/src/test/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanelTest.java similarity index 66% rename from src/test/java/ee/carlrobert/codegpt/toolwindow/chat/StandardChatToolWindowTabPanelTest.java rename to src/test/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanelTest.java index a7ec0ed0..4835894b 100644 --- a/src/test/java/ee/carlrobert/codegpt/toolwindow/chat/StandardChatToolWindowTabPanelTest.java +++ b/src/test/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanelTest.java @@ -7,10 +7,9 @@ import static ee.carlrobert.llm.client.util.JSONUtil.e; import static ee.carlrobert.llm.client.util.JSONUtil.jsonArray; import static ee.carlrobert.llm.client.util.JSONUtil.jsonMap; import static ee.carlrobert.llm.client.util.JSONUtil.jsonMapResponse; -import static java.util.concurrent.TimeUnit.SECONDS; +import static java.util.Objects.requireNonNull; import static org.apache.http.HttpHeaders.AUTHORIZATION; import static org.assertj.core.api.Assertions.assertThat; -import static org.awaitility.Awaitility.await; import ee.carlrobert.codegpt.CodeGPTKeys; import ee.carlrobert.codegpt.EncodingManager; @@ -21,20 +20,23 @@ 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.service.llama.LlamaSettings; -import ee.carlrobert.codegpt.toolwindow.chat.standard.StandardChatToolWindowTabPanel; import ee.carlrobert.llm.client.http.exchange.StreamHttpExchange; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Base64; import java.util.List; import java.util.Map; import testsupport.IntegrationTest; -public class StandardChatToolWindowTabPanelTest extends IntegrationTest { +public class ChatToolWindowTabPanelTest extends IntegrationTest { public void testSendingOpenAIMessage() { useOpenAIService(); ConfigurationSettings.getCurrentState().setSystemPrompt(COMPLETION_SYSTEM_PROMPT); var message = new Message("Hello!"); var conversation = ConversationService.getInstance().startConversation(); - var panel = new StandardChatToolWindowTabPanel(getProject(), conversation); + var panel = new ChatToolWindowTabPanel(getProject(), conversation); expectOpenAI((StreamHttpExchange) request -> { assertThat(request.getUri().getPath()).isEqualTo("/v1/chat/completions"); assertThat(request.getMethod()).isEqualTo("POST"); @@ -57,11 +59,10 @@ public class StandardChatToolWindowTabPanelTest extends IntegrationTest { panel.sendMessage(message); - await().atMost(5, SECONDS) - .until(() -> { - var messages = conversation.getMessages(); - return !messages.isEmpty() && "Hello!".equals(messages.get(0).getResponse()); - }); + waitExpecting(() -> { + var messages = conversation.getMessages(); + return !messages.isEmpty() && "Hello!".equals(messages.get(0).getResponse()); + }); var encodingManager = EncodingManager.getInstance(); assertThat(panel.getTokenDetails()).extracting( "systemPromptTokens", @@ -100,7 +101,7 @@ public class StandardChatToolWindowTabPanelTest extends IntegrationTest { message.setReferencedFilePaths( List.of("TEST_FILE_PATH_1", "TEST_FILE_PATH_2", "TEST_FILE_PATH_3")); var conversation = ConversationService.getInstance().startConversation(); - var panel = new StandardChatToolWindowTabPanel(getProject(), conversation); + var panel = new ChatToolWindowTabPanel(getProject(), conversation); expectOpenAI((StreamHttpExchange) request -> { assertThat(request.getUri().getPath()).isEqualTo("/v1/chat/completions"); assertThat(request.getMethod()).isEqualTo("POST"); @@ -114,23 +115,28 @@ public class StandardChatToolWindowTabPanelTest extends IntegrationTest { List.of( Map.of("role", "system", "content", COMPLETION_SYSTEM_PROMPT), Map.of("role", "user", "content", - "Use the following context to answer question at the end:\n\n" - + "File Path: TEST_FILE_PATH_1\n" - + "File Content:\n" - + "```TEST_FILE_NAME_1\n" - + "TEST_FILE_CONTENT_1\n" - + "```\n\n" - + "File Path: TEST_FILE_PATH_2\n" - + "File Content:\n" - + "```TEST_FILE_NAME_2\n" - + "TEST_FILE_CONTENT_2\n" - + "```\n\n" - + "File Path: TEST_FILE_PATH_3\n" - + "File Content:\n" - + "```TEST_FILE_NAME_3\n" - + "TEST_FILE_CONTENT_3\n" - + "```\n\n" - + "Question: TEST_MESSAGE"))); + """ + Use the following context to answer question at the end: + + File Path: TEST_FILE_PATH_1 + File Content: + ```TEST_FILE_NAME_1 + TEST_FILE_CONTENT_1 + ``` + + File Path: TEST_FILE_PATH_2 + File Content: + ```TEST_FILE_NAME_2 + TEST_FILE_CONTENT_2 + ``` + + File Path: TEST_FILE_PATH_3 + File Content: + ```TEST_FILE_NAME_3 + TEST_FILE_CONTENT_3 + ``` + + Question: TEST_MESSAGE"""))); return List.of( jsonMapResponse("choices", jsonArray(jsonMap("delta", jsonMap("role", "assistant")))), jsonMapResponse("choices", jsonArray(jsonMap("delta", jsonMap("content", "Hel")))), @@ -140,11 +146,10 @@ public class StandardChatToolWindowTabPanelTest extends IntegrationTest { panel.sendMessage(message); - await().atMost(5, SECONDS) - .until(() -> { - var messages = conversation.getMessages(); - return !messages.isEmpty() && "Hello!".equals(messages.get(0).getResponse()); - }); + waitExpecting(() -> { + var messages = conversation.getMessages(); + return !messages.isEmpty() && "Hello!".equals(messages.get(0).getResponse()); + }); var encodingManager = EncodingManager.getInstance(); assertThat(panel.getTokenDetails()).extracting( "systemPromptTokens", @@ -175,6 +180,79 @@ public class StandardChatToolWindowTabPanelTest extends IntegrationTest { List.of("TEST_FILE_PATH_1", "TEST_FILE_PATH_2", "TEST_FILE_PATH_3")); } + public void testSendingOpenAIMessageWithImage() { + var testImagePath = requireNonNull(getClass().getResource("/images/test-image.png")).getPath(); + getProject().putUserData(CodeGPTKeys.IMAGE_ATTACHMENT_FILE_PATH, testImagePath); + useOpenAIService("gpt-4-vision-preview"); + ConfigurationSettings.getCurrentState().setSystemPrompt(COMPLETION_SYSTEM_PROMPT); + var message = new Message("TEST_MESSAGE"); + var conversation = ConversationService.getInstance().startConversation(); + var panel = new ChatToolWindowTabPanel(getProject(), conversation); + expectOpenAI((StreamHttpExchange) request -> { + assertThat(request.getUri().getPath()).isEqualTo("/v1/chat/completions"); + assertThat(request.getMethod()).isEqualTo("POST"); + assertThat(request.getHeaders().get(AUTHORIZATION).get(0)).isEqualTo("Bearer TEST_API_KEY"); + try { + var testImageUrl = "data:image/png;base64," + + Base64.getEncoder().encodeToString(Files.readAllBytes(Path.of(testImagePath))); + assertThat(request.getBody()) + .extracting("model", "messages") + .containsExactly( + "gpt-4-vision-preview", + List.of( + Map.of("role", "system", "content", COMPLETION_SYSTEM_PROMPT), + Map.of("role", "user", "content", List.of( + Map.of( + "type", "image_url", + "image_url", Map.of("url", testImageUrl)), + Map.of("type", "text", "text", "TEST_MESSAGE") + )))); + } catch (IOException e) { + throw new RuntimeException(e); + } + return List.of( + jsonMapResponse("choices", jsonArray(jsonMap("delta", jsonMap("role", "assistant")))), + jsonMapResponse("choices", jsonArray(jsonMap("delta", jsonMap("content", "Hel")))), + jsonMapResponse("choices", jsonArray(jsonMap("delta", jsonMap("content", "lo")))), + jsonMapResponse("choices", jsonArray(jsonMap("delta", jsonMap("content", "!"))))); + }); + + panel.sendMessage(message); + + waitExpecting(() -> { + var messages = conversation.getMessages(); + return !messages.isEmpty() && "Hello!".equals(messages.get(0).getResponse()); + }); + var encodingManager = EncodingManager.getInstance(); + assertThat(panel.getTokenDetails()).extracting( + "systemPromptTokens", + "conversationTokens", + "userPromptTokens", + "highlightedTokens") + .containsExactly( + encodingManager.countTokens(COMPLETION_SYSTEM_PROMPT), + encodingManager.countTokens(message.getPrompt()), + 0, + 0); + assertThat(panel.getConversation()) + .isNotNull() + .extracting("id", "model", "clientCode", "discardTokenLimit") + .containsExactly( + conversation.getId(), + conversation.getModel(), + conversation.getClientCode(), + false); + var messages = panel.getConversation().getMessages(); + assertThat(messages.size()).isOne(); + assertThat(messages.get(0)) + .extracting("id", "prompt", "response", "imageFilePath") + .containsExactly( + message.getId(), + message.getPrompt(), + message.getResponse(), + message.getImageFilePath()); + } + public void testFixCompileErrorsWithOpenAIService() { getProject().putUserData(CodeGPTKeys.SELECTED_FILES, List.of( new ReferencedFile("TEST_FILE_NAME_1", "TEST_FILE_PATH_1", "TEST_FILE_CONTENT_1"), @@ -187,7 +265,7 @@ public class StandardChatToolWindowTabPanelTest extends IntegrationTest { message.setReferencedFilePaths( List.of("TEST_FILE_PATH_1", "TEST_FILE_PATH_2", "TEST_FILE_PATH_3")); var conversation = ConversationService.getInstance().startConversation(); - var panel = new StandardChatToolWindowTabPanel(getProject(), conversation); + var panel = new ChatToolWindowTabPanel(getProject(), conversation); expectOpenAI((StreamHttpExchange) request -> { assertThat(request.getUri().getPath()).isEqualTo("/v1/chat/completions"); assertThat(request.getMethod()).isEqualTo("POST"); @@ -201,23 +279,28 @@ public class StandardChatToolWindowTabPanelTest extends IntegrationTest { List.of( Map.of("role", "system", "content", FIX_COMPILE_ERRORS_SYSTEM_PROMPT), Map.of("role", "user", "content", - "Use the following context to answer question at the end:\n\n" - + "File Path: TEST_FILE_PATH_1\n" - + "File Content:\n" - + "```TEST_FILE_NAME_1\n" - + "TEST_FILE_CONTENT_1\n" - + "```\n\n" - + "File Path: TEST_FILE_PATH_2\n" - + "File Content:\n" - + "```TEST_FILE_NAME_2\n" - + "TEST_FILE_CONTENT_2\n" - + "```\n\n" - + "File Path: TEST_FILE_PATH_3\n" - + "File Content:\n" - + "```TEST_FILE_NAME_3\n" - + "TEST_FILE_CONTENT_3\n" - + "```\n\n" - + "Question: TEST_MESSAGE"))); + """ + Use the following context to answer question at the end: + + File Path: TEST_FILE_PATH_1 + File Content: + ```TEST_FILE_NAME_1 + TEST_FILE_CONTENT_1 + ``` + + File Path: TEST_FILE_PATH_2 + File Content: + ```TEST_FILE_NAME_2 + TEST_FILE_CONTENT_2 + ``` + + File Path: TEST_FILE_PATH_3 + File Content: + ```TEST_FILE_NAME_3 + TEST_FILE_CONTENT_3 + ``` + + Question: TEST_MESSAGE"""))); return List.of( jsonMapResponse("choices", jsonArray(jsonMap("delta", jsonMap("role", "assistant")))), jsonMapResponse("choices", jsonArray(jsonMap("delta", jsonMap("content", "Hel")))), @@ -227,11 +310,10 @@ public class StandardChatToolWindowTabPanelTest extends IntegrationTest { panel.sendMessage(message, ConversationType.FIX_COMPILE_ERRORS); - await().atMost(5, SECONDS) - .until(() -> { - var messages = conversation.getMessages(); - return !messages.isEmpty() && "Hello!".equals(messages.get(0).getResponse()); - }); + waitExpecting(() -> { + var messages = conversation.getMessages(); + return !messages.isEmpty() && "Hello!".equals(messages.get(0).getResponse()); + }); var encodingManager = EncodingManager.getInstance(); assertThat(panel.getTokenDetails()).extracting( "systemPromptTokens", @@ -277,7 +359,7 @@ public class StandardChatToolWindowTabPanelTest extends IntegrationTest { llamaSettings.setRepeatPenalty(1.3); var message = new Message("TEST_PROMPT"); var conversation = ConversationService.getInstance().startConversation(); - var panel = new StandardChatToolWindowTabPanel(getProject(), conversation); + var panel = new ChatToolWindowTabPanel(getProject(), conversation); expectLlama((StreamHttpExchange) request -> { assertThat(request.getUri().getPath()).isEqualTo("/completion"); assertThat(request.getBody()) @@ -312,11 +394,10 @@ public class StandardChatToolWindowTabPanelTest extends IntegrationTest { panel.sendMessage(message, ConversationType.DEFAULT); - await().atMost(5, SECONDS) - .until(() -> { - var messages = conversation.getMessages(); - return !messages.isEmpty() && "Hello!".equals(messages.get(0).getResponse()); - }); + waitExpecting(() -> { + var messages = conversation.getMessages(); + return !messages.isEmpty() && "Hello!".equals(messages.get(0).getResponse()); + }); assertThat(panel.getConversation()) .isNotNull() .extracting("id", "model", "clientCode", "discardTokenLimit") diff --git a/src/test/java/ee/carlrobert/codegpt/toolwindow/chat/StandardChatToolWindowTabbedPaneTest.java b/src/test/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabbedPaneTest.java similarity index 64% rename from src/test/java/ee/carlrobert/codegpt/toolwindow/chat/StandardChatToolWindowTabbedPaneTest.java rename to src/test/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabbedPaneTest.java index 17411989..0746f538 100644 --- a/src/test/java/ee/carlrobert/codegpt/toolwindow/chat/StandardChatToolWindowTabbedPaneTest.java +++ b/src/test/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabbedPaneTest.java @@ -6,13 +6,11 @@ import com.intellij.openapi.util.Disposer; import com.intellij.testFramework.fixtures.BasePlatformTestCase; import ee.carlrobert.codegpt.conversations.ConversationService; import ee.carlrobert.codegpt.conversations.message.Message; -import ee.carlrobert.codegpt.toolwindow.chat.standard.StandardChatToolWindowTabPanel; -import ee.carlrobert.codegpt.toolwindow.chat.standard.StandardChatToolWindowTabbedPane; -public class StandardChatToolWindowTabbedPaneTest extends BasePlatformTestCase { +public class ChatToolWindowTabbedPaneTest extends BasePlatformTestCase { public void testClearAllTabs() { - var tabbedPane = new StandardChatToolWindowTabbedPane(Disposer.newDisposable()); + var tabbedPane = new ChatToolWindowTabbedPane(Disposer.newDisposable()); tabbedPane.addNewTab(createNewTabPanel()); tabbedPane.clearAll(); @@ -22,7 +20,7 @@ public class StandardChatToolWindowTabbedPaneTest extends BasePlatformTestCase { public void testAddingNewTabs() { - var tabbedPane = new StandardChatToolWindowTabbedPane(Disposer.newDisposable()); + var tabbedPane = new ChatToolWindowTabbedPane(Disposer.newDisposable()); tabbedPane.addNewTab(createNewTabPanel()); tabbedPane.addNewTab(createNewTabPanel()); @@ -33,10 +31,10 @@ public class StandardChatToolWindowTabbedPaneTest extends BasePlatformTestCase { } public void testResetCurrentlyActiveTabPanel() { - var tabbedPane = new StandardChatToolWindowTabbedPane(Disposer.newDisposable()); + var tabbedPane = new ChatToolWindowTabbedPane(Disposer.newDisposable()); var conversation = ConversationService.getInstance().startConversation(); conversation.addMessage(new Message("TEST_PROMPT", "TEST_RESPONSE")); - tabbedPane.addNewTab(new StandardChatToolWindowTabPanel(getProject(), conversation)); + tabbedPane.addNewTab(new ChatToolWindowTabPanel(getProject(), conversation)); tabbedPane.resetCurrentlyActiveTabPanel(getProject()); @@ -44,8 +42,8 @@ public class StandardChatToolWindowTabbedPaneTest extends BasePlatformTestCase { assertThat(tabPanel.getConversation().getMessages()).isEmpty(); } - private StandardChatToolWindowTabPanel createNewTabPanel() { - return new StandardChatToolWindowTabPanel( + private ChatToolWindowTabPanel createNewTabPanel() { + return new ChatToolWindowTabPanel( getProject(), ConversationService.getInstance().startConversation()); } diff --git a/src/test/java/testsupport/IntegrationTest.java b/src/test/java/testsupport/IntegrationTest.java index 726d521b..f92be8e2 100644 --- a/src/test/java/testsupport/IntegrationTest.java +++ b/src/test/java/testsupport/IntegrationTest.java @@ -1,5 +1,6 @@ package testsupport; +import com.intellij.openapi.util.Key; import com.intellij.testFramework.fixtures.BasePlatformTestCase; import ee.carlrobert.codegpt.CodeGPTKeys; import ee.carlrobert.llm.client.mixin.ExternalServiceTestMixin; @@ -17,7 +18,17 @@ public class IntegrationTest extends BasePlatformTestCase implements @Override protected void tearDown() throws Exception { ExternalServiceTestMixin.clearAll(); - getProject().putUserData(CodeGPTKeys.SELECTED_FILES, Collections.emptyList()); + clearKeys(); super.tearDown(); } + + private void clearKeys() { + putUserData(CodeGPTKeys.SELECTED_FILES, Collections.emptyList()); + putUserData(CodeGPTKeys.PREVIOUS_INLAY_TEXT, ""); + putUserData(CodeGPTKeys.IMAGE_ATTACHMENT_FILE_PATH, ""); + } + + private void putUserData(Key key, T value) { + getProject().putUserData(key, value); + } } diff --git a/src/test/java/testsupport/mixin/ShortcutsTestMixin.java b/src/test/java/testsupport/mixin/ShortcutsTestMixin.java index c6f19be6..9c7d4aa2 100644 --- a/src/test/java/testsupport/mixin/ShortcutsTestMixin.java +++ b/src/test/java/testsupport/mixin/ShortcutsTestMixin.java @@ -3,19 +3,25 @@ package testsupport.mixin; import static ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.AZURE_OPENAI_API_KEY; import static ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.OPENAI_API_KEY; +import com.intellij.testFramework.PlatformTestUtil; import ee.carlrobert.codegpt.credentials.CredentialsStore; import ee.carlrobert.codegpt.settings.GeneralSettings; import ee.carlrobert.codegpt.settings.service.ServiceType; import ee.carlrobert.codegpt.settings.service.azure.AzureSettings; import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings; import ee.carlrobert.codegpt.settings.service.openai.OpenAISettings; +import java.util.function.BooleanSupplier; public interface ShortcutsTestMixin { default void useOpenAIService() { + useOpenAIService("gpt-4"); + } + + default void useOpenAIService(String model) { GeneralSettings.getCurrentState().setSelectedService(ServiceType.OPENAI); CredentialsStore.INSTANCE.setCredential(OPENAI_API_KEY, "TEST_API_KEY"); - OpenAISettings.getCurrentState().setModel("gpt-4"); + OpenAISettings.getCurrentState().setModel(model); } default void useAzureService() { @@ -35,4 +41,11 @@ public interface ShortcutsTestMixin { GeneralSettings.getCurrentState().setSelectedService(ServiceType.LLAMA_CPP); LlamaSettings.getCurrentState().setServerPort(null); } + + default void waitExpecting(BooleanSupplier condition) { + PlatformTestUtil.waitWithEventsDispatching( + "Waiting for message response timed out or did not meet expected conditions", + condition, + 5); + } } diff --git a/src/test/resources/images/test-image.png b/src/test/resources/images/test-image.png new file mode 100644 index 00000000..8ed28a16 Binary files /dev/null and b/src/test/resources/images/test-image.png differ