diff --git a/gradle.properties b/gradle.properties index 82f7984a..ef2dc526 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,11 +1,10 @@ pluginName=intellij-aicoder pluginRepositoryUrl=https://github.com/SimiaCryptus/intellij-aicoder -pluginVersion=1.8.3 +pluginVersion=1.8.4 jvmArgs=-Xmx8g org.gradle.jvmargs=-Xmx8g -XX:MaxMetaspaceSize=1g # Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html pluginSinceBuild=233 -pluginUntilBuild=242.* platformType=IU platformVersion=2024.2 gradleVersion=8.10.2 diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/AppServer.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/AppServer.kt index b72498a7..33bbe4e6 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/AppServer.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/AppServer.kt @@ -50,38 +50,43 @@ class AppServer( private val serverLock = Object() - private val progressThread = Thread { - try { - UITools.run( - project, "Running CodeChat Server on $port", false - ) { - while (isRunning(it)) { - Thread.sleep(1000) - } - synchronized(serverLock) { - if (it.isCanceled) { - log.info("Server cancelled") - server.stop() - } else { - log.info("Server stopped") - } - } - } - } finally { - log.info("Stopping Server") - server.stop() - } - } +// private val progressThread = Thread { +// try { +// UITools.run( +// project, "Running CodeChat Server on $port", false +// ) { +// while (isRunning(it)) { +// Thread.sleep(1000) +// } +// synchronized(serverLock) { +// if (it.isCanceled) { +// log.info("Server cancelled") +// server.stop() +// } else { +// log.info("Server stopped") +// } +// } +// } +// } finally { +// log.info("Stopping Server") +// server.stop() +// } +// } private fun isRunning(it: ProgressIndicator) = synchronized(serverLock) { !it.isCanceled && server.isRunning } + fun start() { server.start() - progressThread.start() +// progressThread.start() } companion object { @Transient private var server: AppServer? = null + fun isRunning(): Boolean { + return server?.server?.isRunning ?: false + } + fun getServer(project: Project?): AppServer { if (null == server || !server!!.server.isRunning) { server = AppServer( diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/ui/SettingsWidgetFactory.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/ui/SettingsWidgetFactory.kt index 44b17e58..54265080 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/ui/SettingsWidgetFactory.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/ui/SettingsWidgetFactory.kt @@ -1,10 +1,14 @@ package com.github.simiacryptus.aicoder.ui +import com.github.simiacryptus.aicoder.AppServer +import com.github.simiacryptus.aicoder.actions.generic.SessionProxyServer import com.github.simiacryptus.aicoder.config.AppSettingsState import com.github.simiacryptus.aicoder.util.BrowseUtil.browse import com.intellij.openapi.project.Project import com.intellij.openapi.ui.popup.JBPopup import com.intellij.openapi.ui.popup.JBPopupFactory +import com.intellij.openapi.ui.popup.JBPopupListener +import com.intellij.openapi.ui.popup.LightweightWindowEvent import com.intellij.openapi.wm.StatusBar import com.intellij.openapi.wm.StatusBarWidget import com.intellij.openapi.wm.StatusBarWidgetFactory @@ -12,11 +16,16 @@ import com.intellij.ui.CollectionListModel import com.intellij.ui.SimpleListCellRenderer import com.intellij.ui.components.JBList import com.simiacryptus.jopenai.models.ChatModel +import com.simiacryptus.skyenet.core.platform.Session import icons.MyIcons import kotlinx.coroutines.CoroutineScope import java.awt.BorderLayout +import java.awt.Component import java.awt.Cursor import java.awt.FlowLayout +import java.awt.GridLayout +import java.awt.Toolkit +import java.awt.datatransfer.StringSelection import java.awt.event.MouseAdapter import java.awt.event.MouseEvent import java.net.URI @@ -24,143 +33,293 @@ import javax.swing.* class SettingsWidgetFactory : StatusBarWidgetFactory { - class SettingsWidget : StatusBarWidget, StatusBarWidget.MultipleTextValuesPresentation { - - private var statusBar: StatusBar? = null - private val temperatureSlider by lazy { - val slider = JSlider(0, 100, (AppSettingsState.instance.temperature * 100).toInt()) - slider.addChangeListener { AppSettingsState.instance.temperature = slider.value / 100.0 } - val panel = JPanel(BorderLayout(5, 5)) // Add padding - panel.add(slider, BorderLayout.CENTER) - val label = JLabel(String.format("%.2f", AppSettingsState.instance.temperature)) - slider.addChangeListener { label.text = String.format("%.2f", slider.value / 100.0) } - panel.add(label, BorderLayout.EAST) - panel - } + class SettingsWidget : StatusBarWidget, StatusBarWidget.MultipleTextValuesPresentation { - init { - AppSettingsState.instance.addOnSettingsLoadedListener { - statusBar?.updateWidget(ID()) - } - } + private var statusBar: StatusBar? = null + private val smartModelList = createModelList() + private val fastModelList = createModelList() + private var project: Project? = null + private val sessionsList = JList() + private val sessionsListModel = DefaultListModel() + private val temperatureSlider by lazy { + val slider = JSlider(0, 100, (AppSettingsState.instance.temperature * 100).toInt()) + slider.addChangeListener { AppSettingsState.instance.temperature = slider.value / 100.0 } + val panel = JPanel(BorderLayout(5, 5)) // Add padding + panel.add(slider, BorderLayout.CENTER) + val label = JLabel(String.format("%.2f", AppSettingsState.instance.temperature)) + slider.addChangeListener { label.text = String.format("%.2f", slider.value / 100.0) } + panel.add(label, BorderLayout.EAST) + panel + } - fun models() = ChatModel.values().filter { it.value != null && isVisible(it.value!!) } - .entries.sortedBy { "${it.value!!.provider.name} - ${it.value!!.modelName}" }.map { it.value }.toList() + private fun createServerControlPanel(): JPanel { + val panel = JPanel(BorderLayout()) + // Server control buttons + val buttonPanel = JPanel(FlowLayout(FlowLayout.LEFT)) + val startButton = JButton("Start Server") + val stopButton = JButton("Stop Server") + // Set initial button states + startButton.isEnabled = !AppServer.isRunning() + stopButton.isEnabled = AppServer.isRunning() - override fun ID(): String { - return "AICodingAssistant.SettingsWidget" - } + startButton.addActionListener { + AppServer.getServer(project) + startButton.isEnabled = false + stopButton.isEnabled = true + updateSessionsList() + } + stopButton.addActionListener { + AppServer.getServer(project).server.stop() + startButton.isEnabled = true + stopButton.isEnabled = false + updateSessionsList() + } + buttonPanel.add(startButton) + buttonPanel.add(stopButton) + panel.add(buttonPanel, BorderLayout.NORTH) + // Sessions list + sessionsList.model = sessionsListModel + sessionsList.cellRenderer = SessionListRenderer() + val sessionPanel = JPanel(BorderLayout()) + sessionPanel.add(JLabel("Active Sessions:"), BorderLayout.NORTH) + sessionPanel.add(JScrollPane(sessionsList), BorderLayout.CENTER) + // Action buttons for sessions + val actionPanel = JPanel(GridLayout(1, 2)) + val copyButton = JButton("Copy Link") + val openButton = JButton("Open Link") + // Set initial button states for session actions + copyButton.isEnabled = false + openButton.isEnabled = false + // Add selection listener to enable/disable action buttons + sessionsList.addListSelectionListener { + val hasSelection = sessionsList.selectedValue != null + copyButton.isEnabled = hasSelection + openButton.isEnabled = hasSelection + } - override fun getPresentation(): StatusBarWidget.WidgetPresentation { - return this + copyButton.addActionListener { + val session = sessionsList.selectedValue + if (session != null) { + val link = getSessionLink(session) + val selection = StringSelection(link) + Toolkit.getDefaultToolkit().systemClipboard.setContents(selection, null) } - - override fun install(statusBar: StatusBar) { - this.statusBar = statusBar + } + openButton.addActionListener { + val session = sessionsList.selectedValue + if (session != null) { + browse(URI(getSessionLink(session))) } + } + actionPanel.add(copyButton) + actionPanel.add(openButton) + sessionPanel.add(actionPanel, BorderLayout.SOUTH) + panel.add(sessionPanel, BorderLayout.CENTER) + return panel + } - override fun dispose() { - //connection?.disconnect() - } + fun updateSessionsList() { + sessionsListModel.clear() + SessionProxyServer.chats.keys.forEach { sessionsListModel.addElement(it) } + SessionProxyServer.agents.keys.forEach { sessionsListModel.addElement(it) } + } - override fun getTooltipText(): String { - return "Current active model and temperature control" - } + private fun getSessionLink(session: Session): String { + val settings = AppSettingsState.instance + return "http://${settings.listeningEndpoint}:${settings.listeningPort}/?session=${session.sessionId}" + } - private fun createHeader(): JPanel { - val appname = JPanel(FlowLayout(FlowLayout.LEFT, 10, 10)) - appname.add(JLabel("AI Coding Assistant"), FlowLayout.LEFT) - appname.add(JLabel(MyIcons.icon), FlowLayout.LEFT) - - val header = JPanel(BorderLayout()) - header.add(appname, BorderLayout.WEST) - header.add(JLabel("Rate Us!").apply { - cursor = Cursor(Cursor.HAND_CURSOR) - addMouseListener(object : MouseAdapter() { - override fun mouseClicked(e: MouseEvent) = browse( - URI("https://plugins.jetbrains.com/plugin/20724-ai-coding-assistant/edit/reviews") - ) - }) - }, BorderLayout.EAST) - return header + private class SessionListRenderer : ListCellRenderer { + private val label = JLabel() + override fun getListCellRendererComponent( + list: JList?, + value: Session?, + index: Int, + isSelected: Boolean, + cellHasFocus: Boolean + ): Component { + label.text = "Session ${value?.sessionId?.take(8)}" + if (isSelected) { + label.background = list?.selectionBackground + label.foreground = list?.selectionForeground + } else { + label.background = list?.background + label.foreground = list?.foreground } + return label + } + } + + private fun createModelList(): JBList { + val list = JBList(CollectionListModel(models().map { it?.modelName ?: "" })) + list.cellRenderer = getRenderer() + list.visibleRowCount = 20 + return list + } + + + init { + AppSettingsState.instance.addOnSettingsLoadedListener { + statusBar?.updateWidget(ID()) + } + // Initialize selection for both lists + smartModelList.setSelectedValue(AppSettingsState.instance.smartModel, true) + fastModelList.setSelectedValue(AppSettingsState.instance.fastModel, true) + } + + fun models() = ChatModel.values().filter { it.value != null && isVisible(it.value!!) } + .entries.sortedBy { "${it.value!!.provider.name} - ${it.value!!.modelName}" }.map { it.value }.toList() + + override fun ID(): String { + return "AICodingAssistant.SettingsWidget" + } + + override fun getPresentation(): StatusBarWidget.WidgetPresentation { + return this + } + + override fun install(statusBar: StatusBar) { + this.statusBar = statusBar + } + + override fun dispose() { + //connection?.disconnect() + } - override fun getSelectedValue(): String { - return AppSettingsState.instance.smartModel + private fun createHeader(): JPanel { + val appname = JPanel(FlowLayout(FlowLayout.LEFT, 10, 10)) + appname.add(JLabel("AI Coding Assistant"), FlowLayout.LEFT) + appname.add(JLabel(MyIcons.icon), FlowLayout.LEFT) + + val header = JPanel(BorderLayout()) + header.add(appname, BorderLayout.WEST) + header.add(JLabel("Rate Us!").apply { + cursor = Cursor(Cursor.HAND_CURSOR) + addMouseListener(object : MouseAdapter() { + override fun mouseClicked(e: MouseEvent) = browse( + URI("https://plugins.jetbrains.com/plugin/20724-ai-coding-assistant/edit/reviews") + ) + }) + }, BorderLayout.EAST) + return header + } + + private fun getRenderer(): ListCellRenderer = object : SimpleListCellRenderer() { + override fun customize( + list: JList, + value: String?, + index: Int, + selected: Boolean, + hasFocus: Boolean + ) { + text = value // Here you can add more customization if needed + if (value != null) { + val model = models().find { it?.modelName == value } + text = "${model?.provider?.name} - $value" // Enhance label formatting } + } + } + + override fun getPopup(): JBPopup { - private fun getRenderer(): ListCellRenderer = object : SimpleListCellRenderer() { - override fun customize( - list: JList, - value: String?, - index: Int, - selected: Boolean, - hasFocus: Boolean - ) { - text = value // Here you can add more customization if needed - if (value != null) { - val model = models().find { it?.modelName == value } - text = "${model?.provider?.name} - $value" // Enhance label formatting - } - } + val panel = JPanel(BorderLayout()) + panel.add(createHeader(), BorderLayout.NORTH) + // Create tabbed pane + val tabbedPane = JTabbedPane() + // Smart model tab + val smartModelPanel = JPanel(BorderLayout()) + smartModelPanel.add(JScrollPane(smartModelList), BorderLayout.CENTER) + tabbedPane.addTab("Smart Model", smartModelPanel) + // Fast model tab + val fastModelPanel = JPanel(BorderLayout()) + fastModelPanel.add(JScrollPane(fastModelList), BorderLayout.CENTER) + tabbedPane.addTab("Fast Model", fastModelPanel) + // Add server control tab + tabbedPane.addTab("Server", createServerControlPanel()) + + panel.add(tabbedPane, BorderLayout.CENTER) + panel.add(temperatureSlider, BorderLayout.SOUTH) + + val popup = JBPopupFactory.getInstance().createComponentPopupBuilder(panel, tabbedPane) + .setRequestFocus(true) + .setCancelOnClickOutside(true) + .createPopup() + popup.addListener(object : JBPopupListener { + override fun onClosed(event: LightweightWindowEvent) { + updateSessionsList() } + }) - override fun getPopup(): JBPopup { - val listModel = CollectionListModel(models().map { it?.modelName ?: "" }) - val list = JBList(listModel) - list.cellRenderer = getRenderer() - list.visibleRowCount = 20 - - val panel = JPanel(BorderLayout()) - panel.add(createHeader(), BorderLayout.NORTH) - panel.add(JScrollPane(list), BorderLayout.CENTER) - panel.add(temperatureSlider, BorderLayout.SOUTH) - - val popup = JBPopupFactory.getInstance().createComponentPopupBuilder(panel, list) - .setRequestFocus(true) - .setCancelOnClickOutside(true) - .createPopup() - - list.addListSelectionListener { - val selectedValue = list.selectedValue - AppSettingsState.instance.smartModel = selectedValue - statusBar?.updateWidget(ID()) - popup.closeOk(null) - } - - return popup + smartModelList.addListSelectionListener { + val selectedValue = smartModelList.selectedValue + if (selectedValue != null) { + AppSettingsState.instance.smartModel = selectedValue + statusBar?.updateWidget(ID()) } + } - companion object { - fun isVisible(it: ChatModel): Boolean { - val hasApiKey = - AppSettingsState.instance.apiKey?.filter { it.value.isNotBlank() }?.keys?.contains(it.provider.name) - return false != hasApiKey - } + fastModelList.addListSelectionListener { + val selectedValue = fastModelList.selectedValue + if (selectedValue != null) { + AppSettingsState.instance.fastModel = selectedValue + statusBar?.updateWidget(ID()) } + } + return popup } - override fun getId(): String { - return "AICodingAssistant.SettingsWidgetFactory" + override fun getSelectedValue(): String { +// return "${AppSettingsState.instance.smartModel} / ${AppSettingsState.instance.fastModel}" + return "${AppSettingsState.instance.smartModel}" } - override fun getDisplayName(): String { - return "AI Coding Assistant Settings" + override fun getTooltipText(): String { + val serverStatus = if (AppServer.isRunning()) { + "Server running on ${AppSettingsState.instance.listeningEndpoint}:${AppSettingsState.instance.listeningPort}" + } else { + "Server stopped" + } + return "Smart Model: ${AppSettingsState.instance.smartModel}\n" + + "Fast Model: ${AppSettingsState.instance.fastModel}\n" + + "Temperature: ${AppSettingsState.instance.temperature}\n" + + serverStatus } - override fun createWidget(project: Project, scope: CoroutineScope): StatusBarWidget { - return SettingsWidget() + companion object { + fun isVisible(it: ChatModel): Boolean { + val hasApiKey = + AppSettingsState.instance.apiKey?.filter { it.value.isNotBlank() }?.keys?.contains(it.provider.name) + return false != hasApiKey + } } - override fun createWidget(project: Project): StatusBarWidget { - return SettingsWidget() - } + } - override fun isAvailable(project: Project): Boolean { - return true - } + override fun getId(): String { + return "AICodingAssistant.SettingsWidgetFactory" + } - override fun canBeEnabledOn(statusBar: StatusBar): Boolean { - return true - } + override fun getDisplayName(): String { + return "AI Coding Assistant Settings" + } + + companion object { + val settingsWidget = SettingsWidget() + } + + override fun createWidget(project: Project, scope: CoroutineScope): StatusBarWidget { + return settingsWidget + } + + override fun createWidget(project: Project): StatusBarWidget { + return settingsWidget + } + + override fun isAvailable(project: Project): Boolean { + return true + } + + override fun canBeEnabledOn(statusBar: StatusBar): Boolean { + return true + } } \ No newline at end of file diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/util/BrowseUtil.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/util/BrowseUtil.kt index 6a5332bd..57f752c7 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/util/BrowseUtil.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/util/BrowseUtil.kt @@ -1,6 +1,7 @@ package com.github.simiacryptus.aicoder.util import com.github.simiacryptus.aicoder.config.AppSettingsState +import com.github.simiacryptus.aicoder.ui.SettingsWidgetFactory import org.slf4j.LoggerFactory import java.awt.Desktop import java.net.DatagramPacket @@ -11,6 +12,7 @@ import java.net.URI object BrowseUtil { fun browse(uri: URI) { + SettingsWidgetFactory.settingsWidget?.updateSessionsList() sendUdpMessage(uri.toString()) if (!AppSettingsState.instance.disableAutoOpenUrls && Desktop.isDesktopSupported()) { val desktop = Desktop.getDesktop()