diff --git a/.github/workflows/build-plugin.yml b/.github/workflows/build-plugin.yml deleted file mode 100644 index da9cdd6..0000000 --- a/.github/workflows/build-plugin.yml +++ /dev/null @@ -1,76 +0,0 @@ -name: Build PyCharm Plugin - -on: - workflow_dispatch: - inputs: - version: - description: 'Version tag (e.g., 0.2.1)' - required: true - type: string - -jobs: - build: - runs-on: ubuntu-latest - permissions: - contents: write - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Set up JDK 17 - uses: actions/setup-java@v4 - with: - java-version: '17' - distribution: 'temurin' - - - name: Cache Gradle - uses: actions/cache@v4 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - ide-plugins/.gradle - key: gradle-17-${{ hashFiles('ide-plugins/**/*.gradle*','ide-plugins/**/gradle-wrapper.properties') }} - restore-keys: | - gradle-17- - - - name: Setup Gradle wrapper if missing - run: | - if [ ! -f gradlew ] || [ ! -f gradle/wrapper/gradle-wrapper.jar ]; then - echo "Setting up Gradle wrapper for Gradle 9.2.0..." - cd /tmp - curl -L -s -o gradle-9.2.0.zip https://services.gradle.org/distributions/gradle-9.2.0-bin.zip - unzip -q gradle-9.2.0.zip - cd $GITHUB_WORKSPACE/ide-plugins - /tmp/gradle-9.2.0/bin/gradle wrapper --gradle-version 9.2.0 - chmod +x gradlew - echo "Wrapper installed" - fi - working-directory: ide-plugins - - - name: Set version environment variable - run: echo "PLUGIN_VERSION=${{ github.event.inputs.version }}" >> $GITHUB_ENV - - - name: Build plugin - env: - GRADLE_OPTS: "-Dorg.gradle.jvmargs=-Xmx3g" - PLUGIN_VERSION: ${{ github.event.inputs.version }} - run: ./gradlew buildPlugin -Pversion=${{ github.event.inputs.version }} --no-daemon --stacktrace - working-directory: ide-plugins - - - name: Upload plugin artifact - uses: actions/upload-artifact@v4 - with: - name: intellij-plugin-${{ github.event.inputs.version }} - path: ide-plugins/build/distributions/*.zip - - - name: Upload to Release - if: github.event_name == 'release' - uses: softprops/action-gh-release@v1 - with: - files: ide-plugins/build/distributions/*.zip - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/kotlin-ci.yml b/.github/workflows/kotlin-ci.yml index 54ba426..2bd7a32 100644 --- a/.github/workflows/kotlin-ci.yml +++ b/.github/workflows/kotlin-ci.yml @@ -9,13 +9,15 @@ on: branches: [ "main" ] paths: - 'ide-plugins/**' + release: + types: [published] jobs: build-kotlin-plugin: name: 'Build (Java 17)' runs-on: ubuntu-latest permissions: - contents: read + contents: write steps: - name: Checkout repository @@ -82,8 +84,10 @@ jobs: mkdir -p plugin-extracted # Extract the plugin zip unzip -q "$PLUGIN_FILE" -d plugin-extracted/ - # Remove the original zip - rm "$PLUGIN_FILE" + # For non-release builds, remove the original zip + if [ "${{ github.event_name }}" != "release" ]; then + rm "$PLUGIN_FILE" + fi fi if: success() @@ -92,4 +96,12 @@ jobs: with: name: intellij-plugin-pr-${{ github.event.pull_request.number || github.run_number }} path: ide-plugins/build/distributions/plugin-extracted/ - if: success() + if: success() && github.event_name != 'release' + + - name: Upload to Release + if: github.event_name == 'release' + uses: softprops/action-gh-release@v1 + with: + files: ide-plugins/build/distributions/plugin-extracted/ + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/ide-plugins/src/main/kotlin/com/picocode/PicoCodeStatusBarWidget.kt b/ide-plugins/src/main/kotlin/com/picocode/PicoCodeStatusBarWidget.kt index 9f88e3a..e3d58b3 100644 --- a/ide-plugins/src/main/kotlin/com/picocode/PicoCodeStatusBarWidget.kt +++ b/ide-plugins/src/main/kotlin/com/picocode/PicoCodeStatusBarWidget.kt @@ -5,8 +5,6 @@ import com.intellij.openapi.project.Project import com.intellij.openapi.wm.StatusBar import com.intellij.openapi.wm.StatusBarWidget import com.intellij.openapi.wm.StatusBarWidgetFactory -import com.intellij.openapi.wm.ToolWindowManager -import com.intellij.openapi.ui.Messages import com.intellij.util.Consumer import com.google.gson.Gson import com.google.gson.JsonObject @@ -93,20 +91,24 @@ class PicoCodeStatusBarWidget(private val project: Project) : StatusBarWidget, override fun getClickConsumer(): Consumer? { return Consumer { - // Open the PicoCode RAG tool window on click + // Open the PicoCode chat dialog on click ApplicationManager.getApplication().invokeLater { - val toolWindowManager = ToolWindowManager.getInstance(project) - val toolWindow = toolWindowManager.getToolWindow("PicoCode RAG") - - if (toolWindow != null) { - toolWindow.show() - } else { - Messages.showInfoMessage( - project, - "PicoCode RAG tool window is not available. Please ensure the plugin is properly installed.", - "PicoCode RAG" - ) + val chatContent = PicoCodeToolWindowContent(project) + val dialog = object : com.intellij.openapi.ui.DialogWrapper(project) { + init { + init() + title = "PicoCode RAG Assistant" + } + + override fun createCenterPanel(): javax.swing.JComponent { + return chatContent.getContent() + } + + override fun createActions(): Array { + return emptyArray() // No default OK/Cancel buttons + } } + dialog.show() } } } diff --git a/ide-plugins/src/main/kotlin/com/picocode/PicoCodeToolWindowContent.kt b/ide-plugins/src/main/kotlin/com/picocode/PicoCodeToolWindowContent.kt index 1dee053..94bb050 100644 --- a/ide-plugins/src/main/kotlin/com/picocode/PicoCodeToolWindowContent.kt +++ b/ide-plugins/src/main/kotlin/com/picocode/PicoCodeToolWindowContent.kt @@ -5,11 +5,14 @@ import com.intellij.openapi.project.Project import com.intellij.ui.components.JBScrollPane import com.intellij.ui.components.JBTextArea import java.awt.BorderLayout +import java.awt.Dimension +import java.awt.FlowLayout import javax.swing.* import java.net.HttpURLConnection import java.net.URL import com.google.gson.Gson import com.google.gson.JsonObject +import com.google.gson.JsonArray /** * PicoCode RAG Chat Window @@ -18,18 +21,68 @@ import com.google.gson.JsonObject */ class PicoCodeToolWindowContent(private val project: Project) { // Chat components - private val chatArea = JBTextArea(25, 60) + private val chatPanel = JPanel() + private val chatScrollPane: JBScrollPane private val inputField = JBTextArea(3, 60) + private val projectComboBox = JComboBox() private val gson = Gson() - private val chatHistory = mutableListOf>() // (query, response) + private val chatHistory = mutableListOf() + + data class ChatMessage(val sender: String, val message: String, val contexts: List = emptyList()) + data class ContextInfo(val path: String, val score: Float) + data class ProjectItem(val id: String, val name: String) { + override fun toString(): String = name + } init { - chatArea.isEditable = false - chatArea.lineWrap = true - chatArea.wrapStyleWord = true + chatPanel.layout = BoxLayout(chatPanel, BoxLayout.Y_AXIS) + chatScrollPane = JBScrollPane(chatPanel) + chatScrollPane.preferredSize = Dimension(700, 500) inputField.lineWrap = true inputField.wrapStyleWord = true + + // Load available projects + loadProjects() + } + + private fun loadProjects() { + ApplicationManager.getApplication().executeOnPooledThread { + try { + val host = getServerHost() + val port = getServerPort() + val url = URL("http://$host:$port/api/projects") + val connection = url.openConnection() as HttpURLConnection + connection.requestMethod = "GET" + + val response = connection.inputStream.bufferedReader().readText() + val projects = gson.fromJson(response, JsonArray::class.java) + + SwingUtilities.invokeLater { + projectComboBox.removeAllItems() + projects.forEach { projectElement -> + val projectObj = projectElement.asJsonObject + val id = projectObj.get("id")?.asString ?: return@forEach + val name = projectObj.get("name")?.asString + ?: projectObj.get("path")?.asString?.split("/")?.lastOrNull() + ?: id + projectComboBox.addItem(ProjectItem(id, name)) + } + + // Try to select current project + val currentProjectPath = project.basePath + if (currentProjectPath != null) { + for (i in 0 until projectComboBox.itemCount) { + val item = projectComboBox.getItemAt(i) + // We'll need to check against the project path - for now just select first + break + } + } + } + } catch (e: Exception) { + // Silently fail + } + } } private fun getServerHost(): String { @@ -45,16 +98,24 @@ class PicoCodeToolWindowContent(private val project: Project) { fun getContent(): JComponent { val panel = JPanel(BorderLayout()) - // Add a re-index button at the top + // Top panel with project selector and re-index button + val topPanel = JPanel(FlowLayout(FlowLayout.LEFT)) + topPanel.add(JLabel("Project:")) + topPanel.add(projectComboBox) + + val refreshProjectsBtn = JButton("Refresh Projects") + refreshProjectsBtn.addActionListener { + loadProjects() + } + topPanel.add(refreshProjectsBtn) + val reindexBtn = JButton("Re-index Project") reindexBtn.addActionListener { reindexProject() } - val topPanel = JPanel(BorderLayout()) - topPanel.add(reindexBtn, BorderLayout.EAST) + topPanel.add(reindexBtn) // Chat display area - val chatScrollPane = JBScrollPane(chatArea) chatScrollPane.border = BorderFactory.createTitledBorder("Chat") // Input area with buttons @@ -96,6 +157,62 @@ class PicoCodeToolWindowContent(private val project: Project) { return panel } + private fun renderChatHistory() { + chatPanel.removeAll() + + for ((index, msg) in chatHistory.withIndex()) { + val messagePanel = JPanel(BorderLayout()) + messagePanel.border = BorderFactory.createCompoundBorder( + BorderFactory.createEmptyBorder(5, 5, 5, 5), + BorderFactory.createLineBorder(if (msg.sender == "You") java.awt.Color.BLUE else java.awt.Color.GRAY, 1) + ) + + val textArea = JBTextArea(msg.message) + textArea.isEditable = false + textArea.lineWrap = true + textArea.wrapStyleWord = true + textArea.background = if (msg.sender == "You") java.awt.Color(230, 240, 255) else java.awt.Color.WHITE + + val headerPanel = JPanel(BorderLayout()) + headerPanel.add(JLabel("[$msg.sender]"), BorderLayout.WEST) + + // Add delete button for each message + val deleteBtn = JButton("Ɨ") + deleteBtn.preferredSize = Dimension(30, 20) + deleteBtn.addActionListener { + chatHistory.removeAt(index) + renderChatHistory() + } + headerPanel.add(deleteBtn, BorderLayout.EAST) + + messagePanel.add(headerPanel, BorderLayout.NORTH) + messagePanel.add(textArea, BorderLayout.CENTER) + + // Add context information if available + if (msg.contexts.isNotEmpty()) { + val contextText = StringBuilder("\nšŸ“Ž Referenced files:\n") + msg.contexts.forEach { ctx -> + contextText.append(" • ${ctx.path} (${String.format("%.3f", ctx.score)})\n") + } + val contextArea = JBTextArea(contextText.toString()) + contextArea.isEditable = false + contextArea.background = java.awt.Color(250, 250, 250) + messagePanel.add(contextArea, BorderLayout.SOUTH) + } + + chatPanel.add(messagePanel) + } + + chatPanel.revalidate() + chatPanel.repaint() + + // Scroll to bottom + SwingUtilities.invokeLater { + val verticalScrollBar = chatScrollPane.verticalScrollBar + verticalScrollBar.value = verticalScrollBar.maximum + } + } + /** * Send a message to PicoCode backend */ @@ -105,33 +222,30 @@ class PicoCodeToolWindowContent(private val project: Project) { return } - val projectPath = project.basePath ?: return + val selectedProject = projectComboBox.selectedItem as? ProjectItem + if (selectedProject == null) { + SwingUtilities.invokeLater { + JOptionPane.showMessageDialog( + null, + "Please select a project first or refresh the project list", + "No Project Selected", + JOptionPane.WARNING_MESSAGE + ) + } + return + } + + val projectId = selectedProject.id val host = getServerHost() val port = getServerPort() // Add user message to chat - appendToChat("You", query) + chatHistory.add(ChatMessage("You", query)) + renderChatHistory() inputField.text = "" ApplicationManager.getApplication().executeOnPooledThread { try { - // Get or create project - val projectsUrl = URL("http://$host:$port/api/projects") - val createConnection = projectsUrl.openConnection() as HttpURLConnection - createConnection.requestMethod = "POST" - createConnection.setRequestProperty("Content-Type", "application/json") - createConnection.doOutput = true - - val createBody = gson.toJson(mapOf( - "path" to projectPath, - "name" to project.name - )) - createConnection.outputStream.use { it.write(createBody.toByteArray()) } - - val createResponse = createConnection.inputStream.bufferedReader().readText() - val projectData = gson.fromJson(createResponse, JsonObject::class.java) - val projectId = projectData.get("id").asString - // Send query to /code endpoint val queryUrl = URL("http://$host:$port/code") val queryConnection = queryUrl.openConnection() as HttpURLConnection @@ -148,36 +262,40 @@ class PicoCodeToolWindowContent(private val project: Project) { queryConnection.outputStream.use { it.write(queryBody.toByteArray()) } + if (queryConnection.responseCode != 200) { + val errorResponse = queryConnection.errorStream?.bufferedReader()?.readText() + ?: "Server returned ${queryConnection.responseCode}" + SwingUtilities.invokeLater { + chatHistory.add(ChatMessage("Error", "Failed to communicate with PicoCode: $errorResponse\n" + + "Make sure PicoCode server is running on http://$host:$port")) + renderChatHistory() + } + return@executeOnPooledThread + } + val queryResponse = queryConnection.inputStream.bufferedReader().readText() val jsonResponse = gson.fromJson(queryResponse, JsonObject::class.java) val answer = jsonResponse.get("response")?.asString ?: "No response" val usedContext = jsonResponse.getAsJsonArray("used_context") - // Add response to chat + val contexts = mutableListOf() + usedContext?.forEach { ctx -> + val ctxObj = ctx.asJsonObject + val filePath = ctxObj.get("path")?.asString ?: "" + val score = ctxObj.get("score")?.asFloat ?: 0f + contexts.add(ContextInfo(filePath, score)) + } + SwingUtilities.invokeLater { - appendToChat("PicoCode", answer) - - // Add file references if any - usedContext?.let { contexts -> - if (contexts.size() > 0) { - val fileRefs = StringBuilder("\nšŸ“Ž Referenced files:\n") - contexts.forEach { ctx -> - val ctxObj = ctx.asJsonObject - val filePath = ctxObj.get("path")?.asString ?: "" - val score = ctxObj.get("score")?.asFloat ?: 0f - fileRefs.append(" • $filePath (${String.format("%.3f", score)})\n") - } - chatArea.append(fileRefs.toString()) - } - } - - chatHistory.add(Pair(query, answer)) + chatHistory.add(ChatMessage("PicoCode", answer, contexts)) + renderChatHistory() } } catch (e: Exception) { SwingUtilities.invokeLater { - appendToChat("Error", "Failed to communicate with PicoCode: ${e.message}\n" + - "Make sure PicoCode server is running on http://$host:$port") + chatHistory.add(ChatMessage("Error", "Failed to communicate with PicoCode: ${e.message}\n" + + "Make sure PicoCode server is running on http://$host:$port")) + renderChatHistory() } } } @@ -187,29 +305,25 @@ class PicoCodeToolWindowContent(private val project: Project) { * Re-index the current project */ private fun reindexProject() { - val projectPath = project.basePath ?: return + val selectedProject = projectComboBox.selectedItem as? ProjectItem + if (selectedProject == null) { + SwingUtilities.invokeLater { + JOptionPane.showMessageDialog( + null, + "Please select a project first", + "No Project Selected", + JOptionPane.WARNING_MESSAGE + ) + } + return + } + + val projectId = selectedProject.id val host = getServerHost() val port = getServerPort() ApplicationManager.getApplication().executeOnPooledThread { try { - // Get or create project to get project ID - val projectsUrl = URL("http://$host:$port/api/projects") - val createConnection = projectsUrl.openConnection() as HttpURLConnection - createConnection.requestMethod = "POST" - createConnection.setRequestProperty("Content-Type", "application/json") - createConnection.doOutput = true - - val createBody = gson.toJson(mapOf( - "path" to projectPath, - "name" to project.name - )) - createConnection.outputStream.use { it.write(createBody.toByteArray()) } - - val createResponse = createConnection.inputStream.bufferedReader().readText() - val projectData = gson.fromJson(createResponse, JsonObject::class.java) - val projectId = projectData.get("id").asString - // Trigger re-indexing val indexUrl = URL("http://$host:$port/api/projects/index") val indexConnection = indexUrl.openConnection() as HttpURLConnection @@ -225,12 +339,14 @@ class PicoCodeToolWindowContent(private val project: Project) { SwingUtilities.invokeLater { val status = indexData.get("status")?.asString ?: "unknown" - appendToChat("System", "Re-indexing started. Status: $status") + chatHistory.add(ChatMessage("System", "Re-indexing started. Status: $status")) + renderChatHistory() } } catch (e: Exception) { SwingUtilities.invokeLater { - appendToChat("Error", "Failed to start re-indexing: ${e.message}\n" + - "Make sure PicoCode server is running on http://$host:$port") + chatHistory.add(ChatMessage("Error", "Failed to start re-indexing: ${e.message}\n" + + "Make sure PicoCode server is running on http://$host:$port")) + renderChatHistory() } } } @@ -241,19 +357,6 @@ class PicoCodeToolWindowContent(private val project: Project) { */ private fun clearHistory() { chatHistory.clear() - chatArea.text = "" - } - - /** - * Append a message to the chat area - */ - private fun appendToChat(sender: String, message: String) { - SwingUtilities.invokeLater { - if (chatArea.text.isNotEmpty()) { - chatArea.append("\n\n") - } - chatArea.append("[$sender]\n$message") - chatArea.caretPosition = chatArea.document.length - } + renderChatHistory() } } diff --git a/ide-plugins/src/main/kotlin/com/picocode/PicoCodeToolWindowFactory.kt b/ide-plugins/src/main/kotlin/com/picocode/PicoCodeToolWindowFactory.kt deleted file mode 100644 index 3706637..0000000 --- a/ide-plugins/src/main/kotlin/com/picocode/PicoCodeToolWindowFactory.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.picocode - -import com.intellij.openapi.project.Project -import com.intellij.openapi.wm.ToolWindow -import com.intellij.openapi.wm.ToolWindowFactory -import com.intellij.ui.content.ContentFactory - -/** - * Factory for creating the PicoCode RAG Assistant tool window - */ -class PicoCodeToolWindowFactory : ToolWindowFactory { - override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) { - val toolWindowContent = PicoCodeToolWindowContent(project) - val content = ContentFactory.getInstance().createContent( - toolWindowContent.getContent(), - "", - false - ) - toolWindow.contentManager.addContent(content) - } - - override fun shouldBeAvailable(project: Project): Boolean = true -} diff --git a/ide-plugins/src/main/resources/META-INF/plugin.xml b/ide-plugins/src/main/resources/META-INF/plugin.xml index 47824a1..4b323da 100644 --- a/ide-plugins/src/main/resources/META-INF/plugin.xml +++ b/ide-plugins/src/main/resources/META-INF/plugin.xml @@ -12,12 +12,8 @@ com.intellij.modules.platform - - PicoCode - Local Codebase Assistant + @@ -50,28 +51,36 @@

PicoCode — Local Codebase Assistant

-
Configuration
-

Embedding: {{ config.get("embedding_model") or "not set" }}

-

Coding: {{ config.get("coding_model") or "not set" }}

+
+ Configuration ā–¼ +
+
+

Embedding: {{ config.get("embedding_model") or "not set" }}

+

Coding: {{ config.get("coding_model") or "not set" }}

-
- -
+
+ +
+
-
Add New Project
-
- - -
-
- - +
+ Add New Project ā–¼ +
+
+
+ + +
+
+ + +
+
-
@@ -152,302 +161,325 @@
Chat