feat: introduce agent client protocol (acp)
Some checks failed
Build / Build (push) Failing after 4s
Build / Verify Plugin (push) Has been skipped

This commit is contained in:
Carl-Robert Linnupuu 2026-03-16 14:52:37 +00:00
parent 80b2490ca9
commit 9a79bd1610
64 changed files with 4510 additions and 573 deletions

View file

@ -17,7 +17,6 @@ public final class Icons {
IconLoader.getIcon("/icons/expandAll.svg", Icons.class);
public static final Icon Anthropic = IconLoader.getIcon("/icons/anthropic.svg", Icons.class);
public static final Icon DeepSeek = IconLoader.getIcon("/icons/deepseek.png", Icons.class);
public static final Icon Qwen = IconLoader.getIcon("/icons/qwen.png", Icons.class);
public static final Icon Google = IconLoader.getIcon("/icons/google.svg", Icons.class);
public static final Icon Llama = IconLoader.getIcon("/icons/llama.svg", Icons.class);
public static final Icon OpenAI = IconLoader.getIcon("/icons/openai.svg", Icons.class);

View file

@ -8,21 +8,23 @@ import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.project.Project
import ee.carlrobert.codegpt.agent.external.ExternalAcpAgentService
import ee.carlrobert.codegpt.agent.history.AgentCheckpointHistoryService
import ee.carlrobert.codegpt.agent.history.CheckpointRef
import ee.carlrobert.codegpt.settings.models.ModelSettings
import ee.carlrobert.codegpt.settings.service.FeatureType
import ee.carlrobert.codegpt.settings.service.ServiceType
import ee.carlrobert.codegpt.toolwindow.agent.AgentSession
import ee.carlrobert.codegpt.toolwindow.agent.AgentToolWindowContentManager
import ee.carlrobert.codegpt.ui.textarea.header.tag.McpTagDetails
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlin.time.Clock
import kotlinx.serialization.json.JsonNull
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import kotlin.io.path.Path
import kotlin.time.Clock
import ai.koog.prompt.message.Message as PromptMessage
internal fun interface AgentRuntimeFactory {
@ -112,6 +114,12 @@ class AgentService(private val project: Project) {
val provider = service<ModelSettings>().getServiceForFeature(FeatureType.AGENT)
val contentManager = project.service<AgentToolWindowContentManager>()
val session = contentManager.getSession(sessionId) ?: return
if (!session.externalAgentId.isNullOrBlank()) {
submitExternalMessage(session, message, events, provider)
return
}
val runtimeAgentId = contentManager.getSession(sessionId)?.runtimeAgentId
val runtime = runCatching {
ensureSessionRuntime(sessionId, provider, events)
@ -156,6 +164,17 @@ class AgentService(private val project: Project) {
}
fun cancelCurrentRun(sessionId: String) {
val session = project.service<AgentToolWindowContentManager>().getSession(sessionId)
if (session?.externalAgentId != null) {
runCatching {
runBlocking {
project.service<ExternalAcpAgentService>()
.cancelSession(sessionId, session.externalAgentSessionId)
}
}.onFailure { ex ->
logger.warn("Failed cancelling external ACP session for session=$sessionId", ex)
}
}
sessionJobs[sessionId]?.cancel()
sessionJobs.remove(sessionId)
}
@ -171,6 +190,9 @@ class AgentService(private val project: Project) {
logger.warn("Failed closing managed agent service for session=$sessionId", ex)
}
}
project.service<ExternalAcpAgentService>().closeSession(sessionId)
project.service<AgentToolWindowContentManager>()
.getSession(sessionId)?.externalAgentSessionId = null
project.service<AgentMcpContextService>().clear(sessionId)
}
@ -190,6 +212,40 @@ class AgentService(private val project: Project) {
return sessionAgents[sessionId]
}
private fun submitExternalMessage(
session: AgentSession,
message: MessageWithContext,
events: AgentEvents,
provider: ServiceType
) {
val externalAgentService = project.service<ExternalAcpAgentService>()
sessionJobs[session.sessionId] = CoroutineScope(Dispatchers.IO).launch {
try {
externalAgentService.runPromptLoop(
session = session,
firstMessage = message,
events = events,
pollNextQueued = {
val queue = pendingMessages[session.sessionId] ?: return@runPromptLoop null
if (queue.isEmpty()) {
null
} else {
queue.removeFirst()
}
}
)
} catch (_: CancellationException) {
return@launch
} catch (ex: Throwable) {
logger.error(ex)
events.onAgentException(provider, ex)
} finally {
events.onRunCheckpointUpdated(message.id, null)
sessionJobs.remove(session.sessionId)
}
}
}
private fun updateMcpContext(sessionId: String, message: MessageWithContext) {
val selectedServerIds = message.tags
.filterIsInstance<McpTagDetails>()

View file

@ -0,0 +1,264 @@
package ee.carlrobert.codegpt.agent.external
data class ExternalAcpAgentPreset(
val id: String,
val displayName: String,
val vendor: String,
val command: String,
val args: List<String>,
val env: Map<String, String> = emptyMap(),
val enabledByDefault: Boolean = false,
val description: String? = null,
) {
fun fullCommand(): String = buildString {
append(command)
if (args.isNotEmpty()) {
append(' ')
append(args.joinToString(" "))
}
}
}
object ExternalAcpAgents {
private val presets = listOf(
ExternalAcpAgentPreset(
id = "codex",
displayName = "Codex",
vendor = "OpenAI",
command = "npx",
args = listOf("-y", "@zed-industries/codex-acp"),
enabledByDefault = true,
description = "OpenAI Codex via the Zed ACP adapter."
),
ExternalAcpAgentPreset(
id = "opencode",
displayName = "OpenCode",
vendor = "OpenCode",
command = "opencode",
args = listOf("acp"),
enabledByDefault = true,
description = "OpenCode CLI running its ACP server."
),
ExternalAcpAgentPreset(
id = "cursor",
displayName = "Cursor",
vendor = "Cursor",
command = "agent",
args = listOf("acp"),
description = "Cursor Agent in ACP mode."
),
ExternalAcpAgentPreset(
id = "claude-code",
displayName = "Claude Code",
vendor = "Anthropic",
command = "npx",
args = listOf("-y", "@zed-industries/claude-code-acp"),
enabledByDefault = true,
description = "Anthropic Claude Code via the Zed ACP adapter."
),
ExternalAcpAgentPreset(
id = "gemini-cli",
displayName = "Gemini CLI",
vendor = "Google",
command = "gemini",
args = listOf("--experimental-acp"),
description = "Google Gemini CLI in experimental ACP mode."
),
ExternalAcpAgentPreset(
id = "goose",
displayName = "Goose",
vendor = "Block",
command = "goose",
args = listOf("acp"),
description = "Block Goose running as an ACP server."
),
ExternalAcpAgentPreset(
id = "github-copilot",
displayName = "GitHub Copilot",
vendor = "GitHub",
command = "copilot",
args = listOf("--acp"),
description = "GitHub Copilot CLI ACP server."
),
ExternalAcpAgentPreset(
id = "qwen-code",
displayName = "Qwen Code",
vendor = "Qwen",
command = "qwen",
args = listOf("--acp"),
description = "Qwen Code running its ACP server."
),
ExternalAcpAgentPreset(
id = "auggie",
displayName = "Auggie CLI",
vendor = "Augment",
command = "auggie",
args = listOf("--acp"),
description = "Augment's Auggie CLI in ACP mode."
),
ExternalAcpAgentPreset(
id = "agentpool",
displayName = "AgentPool",
vendor = "AgentPool",
command = "agentpool",
args = listOf("serve-acp", "agents.yml"),
description = "AgentPool serving ACP from a local agents.yml configuration."
),
ExternalAcpAgentPreset(
id = "blackbox-ai",
displayName = "Blackbox AI",
vendor = "Blackbox AI",
command = "blackbox",
args = listOf("--experimental-acp"),
description = "Blackbox CLI running in experimental ACP mode."
),
ExternalAcpAgentPreset(
id = "claude-agent",
displayName = "Claude Agent",
vendor = "Anthropic",
command = "npx",
args = listOf("-y", "@zed-industries/claude-agent-acp"),
description = "Anthropic Claude Agent via the Zed ACP adapter."
),
ExternalAcpAgentPreset(
id = "cline",
displayName = "Cline",
vendor = "Cline",
command = "npx",
args = listOf("-y", "cline", "--acp"),
description = "Cline running as an ACP server."
),
ExternalAcpAgentPreset(
id = "code-assistant",
displayName = "Code Assistant",
vendor = "stippi",
command = "code-assistant",
args = listOf("acp"),
description = "Code Assistant running in ACP agent mode."
),
ExternalAcpAgentPreset(
id = "docker-cagent",
displayName = "Docker's cagent",
vendor = "Docker",
command = "cagent",
args = listOf("acp"),
description = "Docker cagent serving ACP; project agent configuration may still be required."
),
ExternalAcpAgentPreset(
id = "fast-agent",
displayName = "fast-agent",
vendor = "fast-agent",
command = "uvx",
args = listOf("fast-agent-acp", "-x"),
description = "fast-agent's ACP bridge via uvx."
),
ExternalAcpAgentPreset(
id = "factory-droid",
displayName = "Factory Droid",
vendor = "Factory AI",
command = "npx",
args = listOf("-y", "droid", "exec", "--output-format", "acp"),
env = mapOf(
"DROID_DISABLE_AUTO_UPDATE" to "true",
"FACTORY_DROID_AUTO_UPDATE_ENABLED" to "false",
),
description = "Factory Droid running in ACP mode."
),
ExternalAcpAgentPreset(
id = "junie",
displayName = "Junie",
vendor = "JetBrains",
command = "junie",
args = listOf("--acp=true"),
description = "JetBrains Junie running as an ACP agent."
),
ExternalAcpAgentPreset(
id = "kimi-cli",
displayName = "Kimi CLI",
vendor = "Moonshot AI",
command = "kimi",
args = listOf("acp"),
description = "Moonshot AI's Kimi CLI in ACP mode."
),
ExternalAcpAgentPreset(
id = "kiro-cli",
displayName = "Kiro CLI",
vendor = "Kiro",
command = "kiro",
args = listOf("--acp"),
description = "Kiro CLI running as an ACP-compliant agent."
),
ExternalAcpAgentPreset(
id = "minion-code",
displayName = "Minion Code",
vendor = "Minion",
command = "uvx",
args = listOf("minion-code", "acp"),
description = "Minion Code running its ACP server."
),
ExternalAcpAgentPreset(
id = "mistral-vibe",
displayName = "Mistral Vibe",
vendor = "Mistral AI",
command = "vibe-acp",
args = emptyList(),
description = "Mistral Vibe's ACP bridge."
),
ExternalAcpAgentPreset(
id = "openclaw",
displayName = "OpenClaw",
vendor = "OpenClaw",
command = "openclaw",
args = listOf("acp"),
description = "OpenClaw running as an ACP server."
),
ExternalAcpAgentPreset(
id = "openhands",
displayName = "OpenHands",
vendor = "All Hands AI",
command = "openhands",
args = listOf("acp"),
description = "OpenHands in ACP mode."
),
ExternalAcpAgentPreset(
id = "pi",
displayName = "Pi",
vendor = "pi",
command = "npx",
args = listOf("-y", "pi-acp"),
description = "Pi via the pi ACP adapter."
),
ExternalAcpAgentPreset(
id = "qoder-cli",
displayName = "Qoder CLI",
vendor = "Qoder AI",
command = "npx",
args = listOf("-y", "@qoder-ai/qodercli", "--acp"),
description = "Qoder CLI running its ACP server."
),
ExternalAcpAgentPreset(
id = "stakpak",
displayName = "Stakpak",
vendor = "Stakpak",
command = "stakpak",
args = listOf("acp"),
description = "Stakpak running in ACP mode."
),
ExternalAcpAgentPreset(
id = "vt-code",
displayName = "VT Code",
vendor = "VT Code",
command = "vtcode",
args = listOf("acp"),
description = "VT Code running its ACP bridge."
)
)
fun all(): List<ExternalAcpAgentPreset> = presets.sortedBy { it.displayName.lowercase() }
fun find(id: String?): ExternalAcpAgentPreset? = presets.firstOrNull { it.id == id }
fun enabledByDefaultIds(): List<String> =
presets.filter { it.enabledByDefault }.map { it.id }
}

View file

@ -0,0 +1,739 @@
package ee.carlrobert.codegpt.agent.external
import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.VfsUtil
import ee.carlrobert.codegpt.agent.AgentEvents
import ee.carlrobert.codegpt.agent.MessageWithContext
import ee.carlrobert.codegpt.agent.ToolSpecs
import ee.carlrobert.codegpt.agent.tools.BashTool
import ee.carlrobert.codegpt.agent.tools.EditTool
import ee.carlrobert.codegpt.agent.tools.WriteTool
import ee.carlrobert.codegpt.settings.mcp.McpSettings
import ee.carlrobert.codegpt.toolwindow.agent.AgentSession
import ee.carlrobert.codegpt.toolwindow.agent.AgentToolWindowContentManager
import ee.carlrobert.codegpt.toolwindow.agent.ui.approval.*
import ee.carlrobert.codegpt.ui.textarea.header.tag.*
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.json.*
import java.io.File
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.util.concurrent.ConcurrentHashMap
import kotlin.io.path.createDirectories
import kotlin.io.path.notExists
private data class ExternalToolCall(
val toolName: String,
val args: Any?
)
@Service(Service.Level.PROJECT)
class ExternalAcpAgentService(private val project: Project) {
private companion object {
const val PROTOCOL_VERSION = 1
const val FULL_ACCESS_MODE_ID = "full-access"
val NO_OP_EVENTS = object : AgentEvents {
override fun onQueuedMessagesResolved() = Unit
override fun onAgentException(
provider: ee.carlrobert.codegpt.settings.service.ServiceType,
throwable: Throwable
) = Unit
}
}
private val logger = thisLogger()
private val json = Json { ignoreUnknownKeys = true }
private val sessionConfigAdapter = AcpSessionConfigAdapter(json)
private val toolCallDecoder = AcpToolCallDecoder(json)
private val sessionUpdateParser = AcpSessionUpdateParser(toolCallDecoder)
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val states = ConcurrentHashMap<String, AcpProcessState>()
private val sessionSetupMutexes = ConcurrentHashMap<String, Mutex>()
suspend fun runPromptLoop(
session: AgentSession,
firstMessage: MessageWithContext,
events: AgentEvents,
pollNextQueued: () -> MessageWithContext?
) {
val preset = ExternalAcpAgents.find(session.externalAgentId)
?: error("Unsupported external agent: ${session.externalAgentId}")
val state = ensureSessionReady(session, preset, events)
var current: MessageWithContext? = firstMessage
while (current != null && scope.isActive) {
val promptMessage = current
val externalSessionId = session.externalAgentSessionId
?: error("Missing ACP session id for ${session.sessionId}")
try {
sendPrompt(state, externalSessionId, promptMessage)
} catch (cancelled: CancellationException) {
cancelSession(state, externalSessionId)
throw cancelled
}
val nextMessage = pollNextQueued()
if (nextMessage == null) {
events.onAgentCompleted(preset.displayName)
return
}
events.onQueuedMessagesResolved()
current = nextMessage
delay(50)
}
}
fun closeSession(sessionId: String) {
states.remove(sessionId)?.close()
sessionSetupMutexes.remove(sessionId)
}
suspend fun cancelSession(sessionId: String, externalSessionId: String?) {
val state = states[sessionId] ?: return
val activeSessionId = externalSessionId ?: return
cancelSession(state, activeSessionId)
}
suspend fun warmUpSession(session: AgentSession) {
val preset = ExternalAcpAgents.find(session.externalAgentId) ?: return
ensureSessionReady(session, preset, NO_OP_EVENTS)
}
suspend fun setSessionConfigOption(
session: AgentSession,
optionId: String,
value: String
) {
val preset = ExternalAcpAgents.find(session.externalAgentId)
?: error("Unsupported external agent: ${session.externalAgentId}")
val state = ensureSessionReady(session, preset, NO_OP_EVENTS)
val externalSessionId = session.externalAgentSessionId
?: error("Missing ACP session id for ${session.sessionId}")
val result = sessionConfigAdapter.updateOption(
request = AcpConfigUpdateRequest(
sessionId = externalSessionId,
optionId = optionId,
value = value
),
support = state.configUpdateSupport,
sendRequest = state::sendRequest
)
when (result) {
AcpConfigUpdateResult.Unsupported -> {
state.configUpdateSupport = AcpConfigUpdateSupport.Unsupported
throw IllegalStateException("${preset.displayName} does not support runtime option changes")
}
is AcpConfigUpdateResult.Applied -> {
state.configUpdateSupport = result.support
updateSessionConfigOptions(session, result.response)
}
}
}
private suspend fun ensureSessionReady(
session: AgentSession,
preset: ExternalAcpAgentPreset,
events: AgentEvents
): AcpProcessState {
val mutex = sessionSetupMutexes.computeIfAbsent(session.sessionId) { Mutex() }
return mutex.withLock {
val state = ensureState(session, preset, events)
if (session.externalAgentSessionId.isNullOrBlank()) {
session.externalAgentConfigLoading = true
session.externalAgentSessionId = createSession(state, session)
}
state
}
}
private suspend fun ensureState(
session: AgentSession,
preset: ExternalAcpAgentPreset,
events: AgentEvents
): AcpProcessState {
val existing = states[session.sessionId]
if (existing != null && existing.preset.id == preset.id && existing.isAlive()) {
existing.events = events
return existing
}
existing?.close()
val resolvedCommand = AcpProcessHelper.resolveCommand(
command = preset.command,
extraEnvironment = preset.env
)
?: throw IllegalStateException(
AcpProcessHelper.getCommandNotFoundMessage(preset.command)
)
val enhancedEnv = AcpProcessHelper.createEnvironment(
extraEnvironment = preset.env,
resolvedCommand = resolvedCommand
)
val process = withContext(Dispatchers.IO) {
GeneralCommandLine().apply {
withExePath(resolvedCommand)
withParameters(preset.args)
withWorkDirectory(File(project.basePath ?: System.getProperty("user.dir")))
withEnvironment(enhancedEnv)
withParentEnvironmentType(GeneralCommandLine.ParentEnvironmentType.NONE)
withRedirectErrorStream(false)
}.createProcess()
}
val state = AcpProcessState(
proxySessionId = session.sessionId,
preset = preset,
process = process,
events = events
)
states[session.sessionId] = state
state.startReader()
state.startStderrLogger()
initialize(state)
return state
}
private suspend fun initialize(state: AcpProcessState) {
val response = state.sendRequest(
method = "initialize",
params = buildJsonObject {
put("protocolVersion", PROTOCOL_VERSION)
putJsonObject("clientCapabilities") {
putJsonObject("fs") {
put("readTextFile", true)
put("writeTextFile", true)
}
}
putJsonObject("clientInfo") {
put("name", "proxyai")
put("title", "ProxyAI")
put("version", "dev")
}
}
)
state.authMethodIds =
response["authMethods"]?.jsonArray?.mapNotNull { it.jsonObject.string("id") }.orEmpty()
}
private suspend fun createSession(state: AcpProcessState, session: AgentSession): String {
return try {
val response = runCatching {
state.sendRequest(
method = "session/new",
params = buildJsonObject {
put("cwd", project.basePath ?: System.getProperty("user.dir"))
put("mcpServers", buildMcpServers(session.sessionId))
}
)
}.recoverCatching { ex ->
if (ex.isAuthenticationRequiredError() && state.authMethodIds.isNotEmpty()) {
authenticate(state, state.authMethodIds.first())
state.sendRequest(
method = "session/new",
params = buildJsonObject {
put("cwd", project.basePath ?: System.getProperty("user.dir"))
put("mcpServers", buildMcpServers(session.sessionId))
}
)
} else {
throw ex
}
}.getOrThrow()
updateSessionConfigOptions(session, response)
response["sessionId"]?.jsonPrimitive?.content
?: error("ACP agent did not return a sessionId")
} finally {
session.externalAgentConfigLoading = false
}
}
private suspend fun authenticate(state: AcpProcessState, methodId: String) {
state.sendRequest(
method = "authenticate",
params = buildJsonObject {
put("methodId", methodId)
}
)
}
private fun updateSessionConfigOptions(session: AgentSession, response: JsonObject) {
session.externalAgentConfigOptions = sessionConfigAdapter.merge(
existing = session.externalAgentConfigOptions,
response = response
)
session.externalAgentConfigLoading = false
}
private suspend fun sendPrompt(
state: AcpProcessState,
externalSessionId: String,
message: MessageWithContext
) {
state.sendRequest(
method = "session/prompt",
params = buildJsonObject {
put("sessionId", externalSessionId)
put("prompt", buildPromptBlocks(message))
}
)
}
private suspend fun cancelSession(state: AcpProcessState, externalSessionId: String) {
runCatching {
state.sendNotification(
method = "session/cancel",
params = buildJsonObject {
put("sessionId", externalSessionId)
}
)
}.onFailure {
logger.debug("Failed to cancel ACP session $externalSessionId", it)
}
}
private fun buildMcpServers(proxySessionId: String): JsonArray {
val selectedServerIds =
project.service<ee.carlrobert.codegpt.agent.AgentMcpContextService>()
.get(proxySessionId)
?.selectedServerIds
.orEmpty()
if (selectedServerIds.isEmpty()) {
return JsonArray(emptyList())
}
val serversById =
project.service<McpSettings>().state.servers.associateBy { it.id.toString() }
return JsonArray(selectedServerIds.mapNotNull { serverId ->
val server = serversById[serverId] ?: return@mapNotNull null
buildJsonObject {
put("type", "stdio")
put("name", server.name ?: serverId)
put("command", server.command ?: "npx")
putJsonArray("args") {
server.arguments.forEach { add(JsonPrimitive(it)) }
}
putJsonArray("env") {
server.environmentVariables.forEach { (key, value) ->
add(
buildJsonObject {
put("name", key)
put("value", value)
}
)
}
}
}
})
}
private fun buildPromptBlocks(message: MessageWithContext): JsonArray {
val blocks = mutableListOf<JsonElement>()
val selectedTags = message.tags.filter { it.selected }
if (selectedTags.isNotEmpty()) {
val tagSummary = buildTagSummary(selectedTags)
if (tagSummary.isNotBlank()) {
blocks += buildJsonObject {
put("type", "text")
put("text", tagSummary)
}
}
}
blocks += buildJsonObject {
put("type", "text")
put("text", message.text)
}
selectedTags.forEach { tag ->
when (tag) {
is FileTagDetails -> blocks += resourceLinkBlock(tag.virtualFile.path)
is EditorTagDetails -> blocks += resourceLinkBlock(tag.virtualFile.path)
else -> Unit
}
}
return JsonArray(blocks)
}
private fun resourceLinkBlock(path: String): JsonObject {
val filePath = Paths.get(path)
return buildJsonObject {
put("type", "resource_link")
put("uri", Paths.get(path).toUri().toString())
put("name", filePath.fileName?.toString() ?: path)
put("mimeType", "text/plain")
}
}
private fun buildTagSummary(tags: List<TagDetails>): String {
if (tags.isEmpty()) return ""
return buildString {
appendLine("Additional ProxyAI context:")
tags.forEach { tag ->
when (tag) {
is FileTagDetails -> appendLine("- File: ${tag.virtualFile.path}")
is EditorTagDetails -> appendLine("- Open file: ${tag.virtualFile.path}")
is FolderTagDetails -> appendLine("- Folder: ${tag.folder.path}")
is SelectionTagDetails -> {
appendLine("- Selection from ${tag.virtualFile.path}:")
appendLine(tag.selectedText.orEmpty())
}
is EditorSelectionTagDetails -> {
appendLine("- Selection from ${tag.virtualFile.path}:")
appendLine(tag.selectedText.orEmpty())
}
else -> appendLine("- ${tag.name}")
}
}
}.trim()
}
private inner class AcpProcessState(
val proxySessionId: String,
val preset: ExternalAcpAgentPreset,
val process: Process,
@Volatile var events: AgentEvents
) {
private val toolCallsById = ConcurrentHashMap<String, ExternalToolCall>()
private val rpcConnection = AcpJsonRpcConnection(
json = json,
process = process,
scope = scope,
logger = logger,
processName = preset.displayName,
onRequest = ::handleRequest,
onNotification = { notification -> handleNotification(notification, events) }
)
@Volatile
var authMethodIds: List<String> = emptyList()
@Volatile
var configUpdateSupport: AcpConfigUpdateSupport = AcpConfigUpdateSupport.Unknown
fun isAlive(): Boolean = rpcConnection.isAlive()
fun startReader() = rpcConnection.startReader()
fun startStderrLogger() = rpcConnection.startStderrLogger()
suspend fun sendRequest(method: String, params: JsonObject): JsonObject =
rpcConnection.request(method, params)
suspend fun sendNotification(method: String, params: JsonObject) =
rpcConnection.notify(method, params)
private suspend fun handleRequest(request: AcpJsonRpcRequest): JsonElement? {
return when (request.method) {
"session/request_permission", "requestPermission" ->
handleRequestPermission(request.params)
"fs/read_text_file", "readTextFile" -> handleReadTextFile(request.params)
"fs/write_text_file", "writeTextFile" -> handleWriteTextFile(request.params)
else -> null
}
}
private fun handleNotification(notification: AcpJsonRpcNotification, events: AgentEvents) {
when (val update = sessionUpdateParser.parse(notification)) {
null -> Unit
is AcpSessionUpdate.TextChunk -> events.onTextReceived(update.text)
is AcpSessionUpdate.ThoughtChunk -> events.onTextReceived("<think>${update.text}</think>")
is AcpSessionUpdate.ToolCall -> handleToolCall(update, events)
is AcpSessionUpdate.ToolCallUpdate -> handleToolCallUpdate(update, events)
is AcpSessionUpdate.ConfigOptionUpdate -> handleConfigOptionUpdate(update.update)
}
}
private suspend fun handleRequestPermission(params: JsonObject): JsonObject {
val requestData = toolCallDecoder.decodePermissionRequest(params)
val session = currentSession()
val mode = session.currentAcpMode()
if (mode == FULL_ACCESS_MODE_ID) {
logger.debug(
"Auto-approving ${preset.displayName} ACP permission in mode=$mode for tool=${requestData.toolName} title=${requestData.rawTitle}"
)
return permissionResponse(selectApprovedPermissionOptionId(requestData.options))
}
val request = buildApprovalRequest(
requestData.rawTitle,
requestData.details,
requestData.toolName,
requestData.parsedArgs
)
val approved = runCatching { events.approveToolCall(request) }.getOrDefault(false)
val selectedOptionId = if (approved) {
selectApprovedPermissionOptionId(requestData.options)
} else {
selectRejectedPermissionOptionId(requestData.options)
}
logger.debug(
"Resolved ${preset.displayName} ACP permission in mode=$mode for tool=${requestData.toolName} approved=$approved title=${requestData.rawTitle}"
)
return permissionResponse(selectedOptionId)
}
private fun handleToolCall(update: AcpSessionUpdate.ToolCall, events: AgentEvents) {
val toolCall = update.toolCall
toolCallsById[toolCall.id] = ExternalToolCall(toolCall.toolName, toolCall.args)
events.onToolStarting(toolCall.id, toolCall.toolName, toolCall.args)
if (update.status?.isTerminal == true) {
completeToolCall(
toolCallId = toolCall.id,
toolName = toolCall.toolName,
args = toolCall.args,
status = update.status,
rawOutput = update.rawOutput,
events = events
)
}
}
private fun handleToolCallUpdate(
update: AcpSessionUpdate.ToolCallUpdate,
events: AgentEvents
) {
if (!update.status.isTerminal) {
return
}
val toolCall = toolCallsById[update.toolCallId]
completeToolCall(
toolCallId = update.toolCallId,
toolName = toolCall?.toolName ?: "Tool",
args = toolCall?.args,
status = update.status,
rawOutput = update.rawOutput,
events = events
)
}
private fun completeToolCall(
toolCallId: String,
toolName: String,
args: Any?,
status: AcpToolCallStatus,
rawOutput: JsonElement?,
events: AgentEvents
) {
val result = toolCallDecoder.decodeResult(
toolName = toolName,
args = args,
status = status,
rawOutput = rawOutput
)
toolCallsById.remove(toolCallId)
events.onToolCompleted(toolCallId, toolName, result)
}
private fun handleConfigOptionUpdate(update: JsonObject) {
project.service<AgentToolWindowContentManager>()
.getSession(proxySessionId)
?.let { session ->
updateSessionConfigOptions(session, update)
}
}
private fun handleReadTextFile(params: JsonObject): JsonObject {
val path = requestPath(params)
val raw = Files.readString(path)
val line = params["line"]?.jsonPrimitive?.intOrNull
val limit = params["limit"]?.jsonPrimitive?.intOrNull
val content = if (line != null && limit != null && line > 0 && limit > 0) {
raw.lineSequence()
.drop(line - 1)
.take(limit)
.joinToString("\n")
} else {
raw
}
return buildJsonObject {
put("content", content)
}
}
private fun handleWriteTextFile(params: JsonObject): JsonElement {
val path = requestPath(params)
val content = params["content"]?.jsonPrimitive?.content ?: ""
if (path.parent != null && path.parent.notExists()) {
path.parent.createDirectories()
}
Files.writeString(path, content)
val virtualFile =
LocalFileSystem.getInstance().refreshAndFindFileByIoFile(path.toFile())
if (virtualFile != null) {
VfsUtil.markDirtyAndRefresh(false, false, false, virtualFile)
} else {
val parent = path.parent?.toFile()
if (parent != null) {
LocalFileSystem.getInstance().refreshAndFindFileByIoFile(parent)
?.let { parentVf ->
VfsUtil.markDirtyAndRefresh(false, false, true, parentVf)
}
}
}
return JsonNull
}
private fun selectApprovedPermissionOptionId(options: JsonArray): String {
return selectPermissionOptionId(
options = options,
preferredKinds = listOf("allow_once", "allow_always", "trust"),
fallback = "allow"
)
}
private fun selectRejectedPermissionOptionId(options: JsonArray): String {
return selectPermissionOptionId(
options = options,
preferredKinds = listOf("reject_once", "reject_always", "deny", "abort", "cancel"),
fallback = "abort",
predicate = { value ->
value.contains("reject") || value.contains("deny") ||
value.contains("abort") || value.contains("cancel")
}
)
}
private fun selectPermissionOptionId(
options: JsonArray,
preferredKinds: List<String>,
fallback: String,
predicate: (String) -> Boolean = { false }
): String {
preferredKinds.forEach { preferred ->
options.firstOrNull { optionMatches(it.jsonObject, preferred) }?.let {
return optionIdOf(it.jsonObject) ?: preferred
}
}
options.firstOrNull { option ->
optionValues(option.jsonObject).any(predicate)
}?.let { option ->
return optionIdOf(option.jsonObject) ?: fallback
}
return options.firstOrNull()?.jsonObject?.let(::optionIdOf) ?: fallback
}
private fun optionMatches(option: JsonObject, expected: String): Boolean {
return optionValues(option).any { it == expected }
}
private fun optionValues(option: JsonObject): List<String> {
return listOfNotNull(
option["kind"]?.jsonPrimitive?.contentOrNull?.lowercase(),
option["optionId"]?.jsonPrimitive?.contentOrNull?.lowercase(),
option["id"]?.jsonPrimitive?.contentOrNull?.lowercase()
)
}
private fun optionIdOf(option: JsonObject): String? {
return option["optionId"]?.jsonPrimitive?.contentOrNull
?: option["id"]?.jsonPrimitive?.contentOrNull
}
private fun buildApprovalRequest(
rawTitle: String,
details: String,
toolName: String,
parsedArgs: Any?
): ToolApprovalRequest {
val approvalType = when (parsedArgs) {
is WriteTool.Args -> ToolApprovalType.WRITE
is EditTool.Args -> ToolApprovalType.EDIT
is BashTool.Args -> ToolApprovalType.BASH
else -> ToolSpecs.approvalTypeFor(toolName)
}
val payload = approvalPayload(parsedArgs)
val title = rawTitle.ifBlank {
when (approvalType) {
ToolApprovalType.BASH -> "Run shell command?"
ToolApprovalType.WRITE -> "Write file?"
ToolApprovalType.EDIT -> "Edit file?"
ToolApprovalType.GENERIC -> "Allow action?"
}
}
return ToolApprovalRequest(
type = approvalType,
title = title,
details = details,
payload = payload
)
}
private fun approvalPayload(parsedArgs: Any?): ToolApprovalPayload? {
return when (parsedArgs) {
is WriteTool.Args -> WritePayload(parsedArgs.filePath, parsedArgs.content)
is EditTool.Args -> EditPayload(
filePath = parsedArgs.filePath,
oldString = parsedArgs.oldString,
newString = parsedArgs.newString,
replaceAll = parsedArgs.replaceAll
)
is BashTool.Args -> BashPayload(parsedArgs.command, parsedArgs.description)
else -> null
}
}
private fun permissionResponse(optionId: String): JsonObject {
return buildJsonObject {
putJsonObject("outcome") {
put("outcome", "selected")
put("optionId", optionId)
}
}
}
private fun currentSession(): AgentSession? {
return project.service<AgentToolWindowContentManager>().getSession(proxySessionId)
}
fun close() = rpcConnection.close()
}
private fun AgentSession?.currentAcpMode(): String? {
return this?.externalAgentConfigOptions
?.firstOrNull(AcpSessionConfigId.MODE::matches)
?.currentValue
}
private fun requestPath(params: JsonObject): Path {
val rawPath = params["path"]?.jsonPrimitive?.contentOrNull
?: params["uri"]?.jsonPrimitive?.contentOrNull
?: error("Missing path")
return uriToPath(rawPath)
}
private fun uriToPath(uri: String): Path {
return when {
uri.startsWith("file://") -> Paths.get(java.net.URI.create(uri))
else -> Paths.get(uri)
}
}
private fun Throwable.isAuthenticationRequiredError(): Boolean {
return message?.contains("Authentication required", ignoreCase = true) == true
}
}

View file

@ -0,0 +1,47 @@
package ee.carlrobert.codegpt.agent.external
import com.intellij.openapi.util.IconLoader
import com.intellij.util.IconUtil
import ee.carlrobert.codegpt.Icons
import javax.swing.Icon
object AcpIcons {
private const val ACP_ICON_ROOT = "/icons/agents"
private const val TARGET_ICON_SIZE = 16
private val iconFileOverrides = mapOf(
"agentpool" to "agentpool.png",
"auggie" to "auggie.png",
"blackbox-ai" to "blackbox.svg",
"claude-code" to "claude.svg",
"gemini-cli" to "gemini-agent.svg",
"kimi-cli" to "kimi.svg",
"kiro-cli" to "kiro.svg",
"pi" to "pi-acp.svg",
"qoder-cli" to "qoder.svg",
"qwen-code" to "qwen.png",
"vt-code" to "vtcode.svg"
)
fun iconFor(agentId: String?): Icon {
val resolvedAgentId = agentId ?: return Icons.DefaultSmall
return acpIconOrNull(iconFileOverrides[resolvedAgentId] ?: "$resolvedAgentId.svg")
?: Icons.DefaultSmall
}
private fun acpIconOrNull(fileName: String): Icon? {
val icon =
IconLoader.findIcon("$ACP_ICON_ROOT/$fileName", AcpIcons::class.java)
?: return null
return normalize(icon)
}
private fun normalize(icon: Icon): Icon {
val maxDimension = maxOf(icon.iconWidth, icon.iconHeight)
if (maxDimension <= TARGET_ICON_SIZE) {
return icon
}
return IconUtil.scale(icon, null, TARGET_ICON_SIZE.toFloat() / maxDimension.toFloat())
}
}

View file

@ -0,0 +1,88 @@
package ee.carlrobert.codegpt.agent.external
import kotlinx.serialization.json.*
internal fun JsonObject.string(vararg keys: String): String? {
return keys.firstNotNullOfOrNull { key ->
when (val element = this[key]) {
is JsonPrimitive -> element.contentOrNull?.takeIf { it.isNotBlank() }
else -> null
}
}
}
internal fun JsonObject.commandString(): String? {
return listOf("command", "cmd").firstNotNullOfOrNull { key ->
when (val element = this[key]) {
is JsonPrimitive -> element.contentOrNull?.takeIf { it.isNotBlank() }
is JsonArray -> element.mapNotNull { item ->
(item as? JsonPrimitive)?.contentOrNull
}.takeIf { it.isNotEmpty() }?.joinToString(" ")
else -> null
}
}
}
internal fun JsonObject.boolean(vararg keys: String): Boolean? {
return keys.firstNotNullOfOrNull { key ->
(this[key] as? JsonPrimitive)?.booleanOrNull
}
}
internal fun JsonObject.int(vararg keys: String): Int? {
return keys.firstNotNullOfOrNull { key ->
(this[key] as? JsonPrimitive)?.intOrNull
}
}
internal fun JsonObject.firstLocationPath(): String? {
return (this["locations"] as? JsonArray)
?.firstNotNullOfOrNull { (it as? JsonObject)?.string("path") }
}
internal fun JsonObject.titlePath(): String? {
val title = string("title") ?: return null
val firstSpaceIndex = title.indexOf(' ')
if (firstSpaceIndex < 0 || firstSpaceIndex >= title.lastIndex) {
return null
}
val path = title.substring(firstSpaceIndex + 1).trim()
return path.takeIf { it.startsWith("/") }
}
internal fun JsonObject.firstChangePath(): String? {
return (this["changes"] as? JsonObject)?.keys?.firstOrNull()
}
internal fun JsonObject.firstChangeContent(): String? {
return (this["changes"] as? JsonObject)?.values?.firstNotNullOfOrNull { change ->
(change as? JsonObject)?.string("content")
}
}
internal fun JsonElement?.asJsonArrayOrEmpty(): JsonArray {
return this as? JsonArray ?: JsonArray(emptyList())
}
internal fun JsonElement?.asJsonObjectOrNull(json: Json): JsonObject? {
return when (this) {
is JsonObject -> this
is JsonPrimitive -> {
if (!isString) {
return null
}
runCatching { json.parseToJsonElement(content) }.getOrNull() as? JsonObject
}
else -> null
}
}
internal fun JsonElement?.toPayloadString(): String {
return when (this) {
null -> ""
is JsonPrimitive -> if (isString) content else toString()
else -> toString()
}
}

View file

@ -0,0 +1,257 @@
package ee.carlrobert.codegpt.agent.external
import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.Logger
import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.json.*
import java.nio.charset.StandardCharsets
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicLong
internal data class AcpJsonRpcRequest(
val id: JsonElement,
val method: String,
val params: JsonObject
)
internal data class AcpJsonRpcNotification(
val method: String,
val params: JsonObject
)
internal data class AcpJsonRpcError(
val code: Int,
val message: String,
val data: JsonElement? = null
) {
companion object {
const val METHOD_NOT_FOUND_CODE = -32601
}
}
internal class AcpJsonRpcException(
val error: AcpJsonRpcError
) : IllegalStateException(error.message)
private sealed interface AcpJsonRpcIncomingMessage {
data class Response(
val id: String,
val result: JsonElement,
val error: AcpJsonRpcError?
) : AcpJsonRpcIncomingMessage
data class Request(val value: AcpJsonRpcRequest) : AcpJsonRpcIncomingMessage
data class Notification(val value: AcpJsonRpcNotification) : AcpJsonRpcIncomingMessage
}
internal class AcpJsonRpcConnection(
private val json: Json,
private val process: Process,
private val scope: CoroutineScope,
private val logger: Logger,
private val processName: String,
private val onRequest: suspend (AcpJsonRpcRequest) -> JsonElement?,
private val onNotification: suspend (AcpJsonRpcNotification) -> Unit
) {
private val requestCounter = AtomicLong(0)
private val pendingResponses = ConcurrentHashMap<String, CompletableDeferred<JsonElement>>()
private val writeMutex = Mutex()
fun isAlive(): Boolean = process.isAlive
fun startReader() {
scope.launch {
val reader = process.inputStream.bufferedReader(StandardCharsets.UTF_8)
try {
while (isActive && process.isAlive) {
val line = reader.readLine() ?: break
if (line.isBlank()) continue
if (service<ConfigurationSettings>().state.debugModeEnabled) {
logger.info("[$processName] $line")
}
when (val message = parseIncomingMessage(line)) {
null -> Unit
is AcpJsonRpcIncomingMessage.Response -> handleResponse(message)
is AcpJsonRpcIncomingMessage.Request -> reply(message.value)
is AcpJsonRpcIncomingMessage.Notification -> onNotification(message.value)
}
}
} catch (cancelled: CancellationException) {
throw cancelled
} catch (t: Throwable) {
logger.warn("ACP reader loop failed for $processName", t)
} finally {
closePendingResponses(IllegalStateException("$processName ACP process exited"))
}
}
}
fun startStderrLogger() {
scope.launch {
process.errorStream.bufferedReader(StandardCharsets.UTF_8).useLines { lines ->
lines.forEach { line ->
if (line.isNotBlank()) {
logger.info("[$processName] $line")
}
}
}
}
}
suspend fun request(method: String, params: JsonObject): JsonObject {
val id = requestCounter.incrementAndGet().toString()
val response = CompletableDeferred<JsonElement>()
pendingResponses[id] = response
writePayload(requestPayload(id, method, params))
return response.await().jsonObject
}
suspend fun notify(method: String, params: JsonObject) {
writePayload(notificationPayload(method, params))
}
fun close() {
closePendingResponses(CancellationException("ACP session closed"))
process.destroy()
}
private suspend fun reply(request: AcpJsonRpcRequest) {
val response = runCatching { onRequest(request) }.fold(
onSuccess = { body ->
if (body == null) {
errorResponse(request.id, -32601, "Method not found: ${request.method}")
} else {
successResponse(request.id, body)
}
},
onFailure = { error ->
errorResponse(request.id, -32603, error.message ?: "Internal error")
}
)
writePayload(response)
}
private fun handleResponse(response: AcpJsonRpcIncomingMessage.Response) {
val pending = pendingResponses.remove(response.id) ?: return
if (response.error != null) {
pending.completeExceptionally(AcpJsonRpcException(response.error))
return
}
pending.complete(response.result)
}
private fun parseIncomingMessage(line: String): AcpJsonRpcIncomingMessage? {
val element = runCatching { json.parseToJsonElement(line) }
.onFailure { logger.warn("Ignoring non-JSON ACP output from $processName: $line") }
.getOrNull() ?: return null
val obj = element.jsonObject
return when {
obj["result"] != null || obj["error"] != null -> {
val responseId = obj["id"]?.jsonPrimitive?.content ?: return null
AcpJsonRpcIncomingMessage.Response(
id = responseId,
result = obj["result"] ?: JsonObject(emptyMap()),
error = parseError(obj["error"])
)
}
obj["method"] != null && obj["id"] != null -> {
val method = obj["method"]?.jsonPrimitive?.content ?: return null
AcpJsonRpcIncomingMessage.Request(
AcpJsonRpcRequest(
id = obj["id"] ?: return null,
method = method,
params = obj["params"]?.jsonObject ?: JsonObject(emptyMap())
)
)
}
obj["method"] != null -> {
val method = obj["method"]?.jsonPrimitive?.content ?: return null
AcpJsonRpcIncomingMessage.Notification(
AcpJsonRpcNotification(
method = method,
params = obj["params"]?.jsonObject ?: JsonObject(emptyMap())
)
)
}
else -> null
}
}
private fun parseError(element: JsonElement?): AcpJsonRpcError? {
val error = element as? JsonObject ?: return null
return AcpJsonRpcError(
code = error.int("code") ?: 0,
message = error.string("message") ?: "Unknown JSON-RPC error",
data = error["data"]
)
}
private suspend fun writePayload(payload: JsonObject) {
writeMutex.withLock {
val serializedPayload = json.encodeToString(JsonObject.serializer(), payload)
if (service<ConfigurationSettings>().state.debugModeEnabled) {
logger.info("[$processName] $serializedPayload")
}
process.outputStream.write(
(serializedPayload + "\n").toByteArray(StandardCharsets.UTF_8)
)
process.outputStream.flush()
}
}
private fun closePendingResponses(error: Throwable) {
pendingResponses.values.forEach { pending ->
pending.completeExceptionally(error)
}
pendingResponses.clear()
}
private fun requestPayload(id: String, method: String, params: JsonObject): JsonObject {
return buildJsonObject {
put("jsonrpc", JsonPrimitive("2.0"))
put("id", JsonPrimitive(id))
put("method", JsonPrimitive(method))
put("params", params)
}
}
private fun notificationPayload(method: String, params: JsonObject): JsonObject {
return buildJsonObject {
put("jsonrpc", JsonPrimitive("2.0"))
put("method", JsonPrimitive(method))
put("params", params)
}
}
private fun successResponse(id: JsonElement, result: JsonElement): JsonObject {
return buildJsonObject {
put("jsonrpc", JsonPrimitive("2.0"))
put("id", id)
put("result", result)
}
}
private fun errorResponse(id: JsonElement, code: Int, message: String): JsonObject {
return buildJsonObject {
put("jsonrpc", JsonPrimitive("2.0"))
put("id", id)
put(
"error",
buildJsonObject {
put("code", JsonPrimitive(code))
put("message", JsonPrimitive(message))
}
)
}
}
}

View file

@ -0,0 +1,41 @@
package ee.carlrobert.codegpt.agent.external
import ee.carlrobert.codegpt.util.CommandRuntimeHelper
object AcpProcessHelper {
fun resolveCommand(
command: String,
extraEnvironment: Map<String, String> = emptyMap()
): String? {
return CommandRuntimeHelper.resolveCommand(command, extraEnvironment)
}
fun createEnvironment(
extraEnvironment: Map<String, String>,
resolvedCommand: String
): MutableMap<String, String> {
return CommandRuntimeHelper.createEnvironment(
extraEnvironment = extraEnvironment,
resolvedCommand = resolvedCommand
)
}
fun getCommandNotFoundMessage(command: String): String {
return buildString {
append("Command '$command' not found. ")
when (command) {
"npx", "node" -> {
append("Node.js/npm is required for this ACP runtime. ")
append("Ensure it is installed and available to the IDE process. ")
append("You can also point the runtime to an absolute executable path.")
}
else -> {
append("Ensure it is installed and available to the IDE process. ")
append("You can also point the runtime to an absolute executable path.")
}
}
}
}
}

View file

@ -0,0 +1,266 @@
package ee.carlrobert.codegpt.agent.external
import ee.carlrobert.codegpt.toolwindow.agent.AcpConfigOption
import ee.carlrobert.codegpt.toolwindow.agent.AcpConfigOptionChoice
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.*
internal enum class AcpSessionConfigId(
val value: String,
val displayName: String
) {
MODEL("model", "Model"),
MODE("mode", "Mode");
fun matches(option: AcpConfigOption): Boolean {
return option.id == value || option.category == value
}
}
internal data class AcpConfigUpdateRequest(
val sessionId: String,
val optionId: String,
val value: String
) {
fun toJsonObject(): JsonObject {
return buildJsonObject {
put("sessionId", sessionId)
put("configId", optionId)
put("value", value)
}
}
}
internal enum class AcpConfigUpdateMethod(val wireName: String) {
SNAKE_CASE("session/set_config_option"),
CAMEL_CASE("session/setConfigOption")
}
internal sealed interface AcpConfigUpdateSupport {
fun candidateMethods(): List<AcpConfigUpdateMethod>
data object Unknown : AcpConfigUpdateSupport {
override fun candidateMethods(): List<AcpConfigUpdateMethod> =
AcpConfigUpdateMethod.entries
}
data object Unsupported : AcpConfigUpdateSupport {
override fun candidateMethods(): List<AcpConfigUpdateMethod> = emptyList()
}
data class Supported(val method: AcpConfigUpdateMethod) : AcpConfigUpdateSupport {
override fun candidateMethods(): List<AcpConfigUpdateMethod> = listOf(method)
}
}
internal sealed interface AcpConfigUpdateResult {
data class Applied(
val response: JsonObject,
val support: AcpConfigUpdateSupport.Supported
) : AcpConfigUpdateResult
data object Unsupported : AcpConfigUpdateResult
}
internal class AcpSessionConfigAdapter(
private val json: Json
) {
fun merge(
existing: List<AcpConfigOption>,
response: JsonObject
): List<AcpConfigOption> {
val updates = decode(response)
if (updates.isEmpty()) {
return existing
}
val merged = existing.associateByTo(linkedMapOf()) { it.id }
updates.forEach { option ->
merged[option.id] = option
}
return merged.values.toList()
}
suspend fun updateOption(
request: AcpConfigUpdateRequest,
support: AcpConfigUpdateSupport,
sendRequest: suspend (String, JsonObject) -> JsonObject
): AcpConfigUpdateResult {
val candidateMethods = support.candidateMethods()
if (candidateMethods.isEmpty()) {
return AcpConfigUpdateResult.Unsupported
}
val params = request.toJsonObject()
candidateMethods.forEach { method ->
try {
return AcpConfigUpdateResult.Applied(
response = sendRequest(method.wireName, params),
support = AcpConfigUpdateSupport.Supported(method)
)
} catch (error: Throwable) {
if (!error.isMethodNotFoundJsonRpcError()) {
throw error
}
}
}
return AcpConfigUpdateResult.Unsupported
}
private fun decode(response: JsonObject): List<AcpConfigOption> {
val directOptions = buildList {
addAll(
response.decodeField<List<AcpStandardConfigOptionPayload>>("configOptions")
.orEmpty()
.mapNotNull(AcpStandardConfigOptionPayload::toConfigOption)
)
response.decodeField<AcpStandardConfigOptionPayload>("configOption")
?.toConfigOption()
?.let(::add)
}
val hasDirectModelOption = directOptions.any(AcpSessionConfigId.MODEL::matches)
val hasDirectModeOption = directOptions.any(AcpSessionConfigId.MODE::matches)
return buildList {
addAll(directOptions)
if (!hasDirectModelOption) {
response.decodeField<AcpModelsPayload>("models")
?.toConfigOption()
?.let(::add)
}
if (!hasDirectModeOption) {
response.decodeField<AcpModesPayload>("modes")
?.toConfigOption()
?.let(::add)
}
}
}
private inline fun <reified T> JsonObject.decodeField(key: String): T? {
val element = this[key] ?: return null
return runCatching { json.decodeFromJsonElement<T>(element) }.getOrNull()
}
}
@Serializable
private data class AcpStandardConfigOptionPayload(
val id: String? = null,
val name: String? = null,
val description: String? = null,
val category: String? = null,
val type: String? = null,
val currentValue: String? = null,
val current_value: String? = null,
val value: String? = null,
val options: List<AcpConfigChoicePayload> = emptyList()
) {
fun toConfigOption(): AcpConfigOption? {
val resolvedId = id.nullIfBlank() ?: return null
return AcpConfigOption(
id = resolvedId,
name = name.nullIfBlank() ?: resolvedId,
description = description.nullIfBlank(),
category = category.nullIfBlank(),
type = type.nullIfBlank(),
currentValue = firstNotBlank(currentValue, current_value, value),
options = options.mapNotNull(AcpConfigChoicePayload::toChoice)
)
}
}
@Serializable
private data class AcpConfigChoicePayload(
val value: String? = null,
val id: String? = null,
val name: String? = null,
val description: String? = null
) {
fun toChoice(): AcpConfigOptionChoice? {
val resolvedValue = firstNotBlank(value, id) ?: return null
return AcpConfigOptionChoice(
value = resolvedValue,
name = name.nullIfBlank() ?: resolvedValue,
description = description.nullIfBlank()
)
}
}
@Serializable
private data class AcpModelsPayload(
val currentModelId: String? = null,
val availableModels: List<AcpAlternativeChoicePayload> = emptyList()
) {
fun toConfigOption(): AcpConfigOption? {
return toAlternativeConfigOption(
id = AcpSessionConfigId.MODEL,
currentValue = currentModelId,
entries = availableModels
)
}
}
@Serializable
private data class AcpModesPayload(
val currentModeId: String? = null,
val availableModes: List<AcpAlternativeChoicePayload> = emptyList()
) {
fun toConfigOption(): AcpConfigOption? {
return toAlternativeConfigOption(
id = AcpSessionConfigId.MODE,
currentValue = currentModeId,
entries = availableModes
)
}
}
@Serializable
private data class AcpAlternativeChoicePayload(
val modelId: String? = null,
val modeId: String? = null,
val value: String? = null,
val id: String? = null,
val name: String? = null,
val description: String? = null
) {
fun toChoice(): AcpConfigOptionChoice? {
val resolvedValue = firstNotBlank(modelId, modeId, value, id) ?: return null
return AcpConfigOptionChoice(
value = resolvedValue,
name = name.nullIfBlank() ?: resolvedValue,
description = description.nullIfBlank()
)
}
}
private fun toAlternativeConfigOption(
id: AcpSessionConfigId,
currentValue: String?,
entries: List<AcpAlternativeChoicePayload>
): AcpConfigOption? {
val options = entries.mapNotNull(AcpAlternativeChoicePayload::toChoice)
val resolvedCurrentValue = currentValue.nullIfBlank()
if (options.isEmpty() && resolvedCurrentValue == null) {
return null
}
return AcpConfigOption(
id = id.value,
name = id.displayName,
category = id.value,
type = "select",
currentValue = resolvedCurrentValue,
options = options
)
}
private fun firstNotBlank(vararg values: String?): String? {
return values.firstNotNullOfOrNull(String?::nullIfBlank)
}
private fun String?.nullIfBlank(): String? = this?.takeIf { it.isNotBlank() }
private fun Throwable.isMethodNotFoundJsonRpcError(): Boolean {
return (this as? AcpJsonRpcException)?.error?.code == AcpJsonRpcError.METHOD_NOT_FOUND_CODE
}

View file

@ -0,0 +1,119 @@
package ee.carlrobert.codegpt.agent.external
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
internal sealed interface AcpSessionUpdate {
data class TextChunk(val text: String) : AcpSessionUpdate
data class ThoughtChunk(val text: String) : AcpSessionUpdate
data class ToolCall(
val toolCall: AcpDecodedToolCall,
val status: AcpToolCallStatus?,
val rawOutput: JsonElement?
) : AcpSessionUpdate
data class ToolCallUpdate(
val toolCallId: String,
val status: AcpToolCallStatus,
val rawOutput: JsonElement?
) : AcpSessionUpdate
data class ConfigOptionUpdate(val update: JsonObject) : AcpSessionUpdate
}
internal enum class AcpToolCallStatus(val wireValue: String) {
COMPLETED("completed"),
FAILED("failed"),
CANCELLED("cancelled");
val isTerminal: Boolean
get() = true
companion object {
fun fromWireValue(value: String?): AcpToolCallStatus? {
return entries.firstOrNull { it.wireValue == value }
}
}
}
internal class AcpSessionUpdateParser(
private val toolCallDecoder: AcpToolCallDecoder
) {
fun parse(notification: AcpJsonRpcNotification): AcpSessionUpdate? {
if (notification.method != SESSION_UPDATE_METHOD) {
return null
}
val update = notification.params["update"] as? JsonObject ?: return null
return when (SessionUpdateKind.fromWireValue(update.string("sessionUpdate"))) {
SessionUpdateKind.AGENT_MESSAGE_CHUNK -> parseAgentMessageChunk(update)
SessionUpdateKind.TOOL_CALL -> parseToolCall(update)
SessionUpdateKind.TOOL_CALL_UPDATE -> parseToolCallUpdate(update)
SessionUpdateKind.CONFIG_OPTION_UPDATE -> AcpSessionUpdate.ConfigOptionUpdate(update)
null -> null
}
}
private fun parseAgentMessageChunk(update: JsonObject): AcpSessionUpdate? {
val content = update["content"] as? JsonObject ?: return null
return when (MessageChunkType.fromWireValue(content.string("type"))) {
MessageChunkType.TEXT ->
AcpSessionUpdate.TextChunk(content.string("text").orEmpty())
MessageChunkType.THOUGHT -> {
val text = content.string("thought").orEmpty()
text.takeIf { it.isNotBlank() }?.let(AcpSessionUpdate::ThoughtChunk)
}
null -> null
}
}
private fun parseToolCall(update: JsonObject): AcpSessionUpdate? {
val toolCall = toolCallDecoder.decodeToolCall(update) ?: return null
return AcpSessionUpdate.ToolCall(
toolCall = toolCall,
status = AcpToolCallStatus.fromWireValue(update.string("status")),
rawOutput = update["rawOutput"] ?: update["content"]
)
}
private fun parseToolCallUpdate(update: JsonObject): AcpSessionUpdate? {
val toolCallId = update.string("toolCallId") ?: return null
val status = AcpToolCallStatus.fromWireValue(update.string("status")) ?: return null
return AcpSessionUpdate.ToolCallUpdate(
toolCallId = toolCallId,
status = status,
rawOutput = update["rawOutput"] ?: update["content"]
)
}
private enum class SessionUpdateKind(val wireValue: String) {
AGENT_MESSAGE_CHUNK("agent_message_chunk"),
TOOL_CALL("tool_call"),
TOOL_CALL_UPDATE("tool_call_update"),
CONFIG_OPTION_UPDATE("config_option_update");
companion object {
fun fromWireValue(value: String?): SessionUpdateKind? {
return entries.firstOrNull { it.wireValue == value }
}
}
}
private enum class MessageChunkType(val wireValue: String) {
TEXT("text"),
THOUGHT("thought");
companion object {
fun fromWireValue(value: String?): MessageChunkType? {
return entries.firstOrNull { it.wireValue == value }
}
}
}
private companion object {
const val SESSION_UPDATE_METHOD = "session/update"
}
}

View file

@ -0,0 +1,336 @@
package ee.carlrobert.codegpt.agent.external
import ee.carlrobert.codegpt.agent.ToolSpecs
import ee.carlrobert.codegpt.agent.tools.*
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import java.nio.charset.StandardCharsets
internal data class AcpDecodedToolCall(
val id: String,
val toolName: String,
val args: Any?
)
internal data class AcpPermissionRequestData(
val rawTitle: String,
val toolName: String,
val parsedArgs: Any?,
val details: String,
val options: JsonArray
)
private data class DiffContent(
val path: String,
val oldText: String?,
val newText: String
)
private data class ResolvedToolCall(
val rawTitle: String,
val toolName: String,
val args: Any?
)
internal class AcpToolCallDecoder(
private val json: Json
) {
fun decodeToolCall(metadata: JsonObject): AcpDecodedToolCall? {
val toolCallId = metadata.string("toolCallId") ?: return null
val tool = resolveToolCall(metadata)
return AcpDecodedToolCall(
id = toolCallId,
toolName = tool.toolName,
args = tool.args
)
}
fun decodePermissionRequest(params: JsonObject): AcpPermissionRequestData {
val toolCall = params["toolCall"] as? JsonObject ?: JsonObject(emptyMap())
val tool = resolveToolCall(toolCall, defaultTitle = "Allow action?")
return AcpPermissionRequestData(
rawTitle = tool.rawTitle,
toolName = tool.toolName,
parsedArgs = tool.args,
details = permissionDetails(toolCall),
options = params["options"].asJsonArrayOrEmpty()
)
}
fun decodeResult(
toolName: String,
args: Any?,
status: AcpToolCallStatus,
rawOutput: JsonElement?
): Any? {
val payload = rawOutput.toPayloadString()
ToolSpecs.decodeResultOrNull(json, toolName, payload)?.let { return it }
if (status == AcpToolCallStatus.COMPLETED) {
when (args) {
is McpTool.Args -> return McpTool.Result(
serverId = args.serverId,
serverName = args.serverName,
toolName = args.toolName,
success = true,
output = payload.ifBlank { "MCP tool completed" }
)
is EditTool.Args -> return EditTool.Result.Success(
filePath = args.filePath,
replacementsMade = 1,
message = "Edit completed"
)
is WriteTool.Args -> return WriteTool.Result.Success(
filePath = args.filePath,
bytesWritten = args.content.toByteArray(StandardCharsets.UTF_8).size,
isNewFile = false,
message = "Write completed"
)
}
}
if (status == AcpToolCallStatus.FAILED || status == AcpToolCallStatus.CANCELLED) {
val message = payload.ifBlank { "Tool ${status.wireValue}" }
when (args) {
is McpTool.Args -> return McpTool.Result.error(
toolName = args.toolName,
output = message,
serverId = args.serverId,
serverName = args.serverName
)
is EditTool.Args -> return EditTool.Result.Error(args.filePath, message)
is WriteTool.Args -> return WriteTool.Result.Error(args.filePath, message)
}
}
return payload.ifBlank { null }
}
private fun resolveToolCall(
metadata: JsonObject,
defaultTitle: String = "Tool"
): ResolvedToolCall {
val rawTitle = metadata.string("title") ?: defaultTitle
val rawKind = metadata.string("kind")
val rawInput = metadata["rawInput"]
val initialToolName = normalizeToolName(rawTitle, rawKind, rawInput)
val parsedArgs = if (initialToolName == "MCP") {
decodeMcpArgs(rawTitle, rawInput)
} else {
decodeToolArgs(initialToolName, rawInput, metadata)
}
return ResolvedToolCall(
rawTitle = rawTitle,
toolName = resolveToolName(initialToolName, parsedArgs),
args = parsedArgs
)
}
private fun permissionDetails(toolCall: JsonObject): String {
return buildString {
(toolCall["locations"] as? JsonArray)
?.mapNotNull { (it as? JsonObject)?.string("path") }
?.takeIf { it.isNotEmpty() }
?.let { paths ->
appendLine("Locations:")
paths.forEach { path -> appendLine(path) }
}
toolCall["rawInput"]?.let { rawInput ->
if (isNotBlank()) {
appendLine()
}
appendLine("Input:")
append(rawInput.toString())
}
}.ifBlank { toolCall.toString() }
}
private fun normalizeToolName(
rawTitle: String,
kind: String?,
rawInput: JsonElement?
): String {
val normalizedKind = kind?.lowercase().orEmpty()
val rawInputObject = rawInput.asJsonObjectOrNull(json)
return when {
looksLikeMcpToolName(rawTitle) -> "MCP"
rawInputObject?.get("command") != null || rawInputObject?.get("cmd") != null -> "Bash"
normalizedKind == "execute" || normalizedKind == "terminal" || normalizedKind == "bash" -> "Bash"
normalizedKind == "edit" -> "Edit"
normalizedKind == "read" -> "Read"
normalizedKind == "search" -> "IntelliJSearch"
normalizedKind == "fetch" -> "WebFetch"
else -> rawTitle.ifBlank { kind ?: "Tool" }
}
}
private fun resolveToolName(initialToolName: String, args: Any?): String {
return when (args) {
is McpTool.Args -> "MCP"
is WriteTool.Args -> "Write"
is EditTool.Args -> "Edit"
is ReadTool.Args -> "Read"
is IntelliJSearchTool.Args -> "IntelliJSearch"
is BashTool.Args -> "Bash"
else -> initialToolName
}
}
private fun decodeToolArgs(
toolName: String,
rawInput: JsonElement?,
metadata: JsonObject? = null
): Any? {
val payload = rawInput.toPayloadString()
ToolSpecs.decodeArgsOrNull(json, toolName, payload)?.let { return it }
val obj = rawInput.asJsonObjectOrNull(json) ?: JsonObject(emptyMap())
return when (toolName) {
"Edit" -> decodeEditOrWriteArgs(obj, metadata) ?: payload.ifBlank { null }
"Write" -> decodeWriteArgs(obj, metadata) ?: payload.ifBlank { null }
"Read" -> decodeReadArgs(obj, metadata) ?: payload.ifBlank { null }
"IntelliJSearch" -> decodeSearchArgs(obj) ?: payload.ifBlank { null }
"Bash" -> decodeBashArgs(obj) ?: payload.ifBlank { null }
else -> payload.ifBlank { null }
}
}
private fun decodeEditOrWriteArgs(obj: JsonObject, metadata: JsonObject?): Any? {
decodeDiffContent(metadata)?.let { diff ->
return if (diff.oldText == null) {
WriteTool.Args(
filePath = diff.path,
content = diff.newText
)
} else {
EditTool.Args(
filePath = diff.path,
oldString = diff.oldText,
newString = diff.newText,
shortDescription = metadata?.string("title") ?: "ACP edit",
replaceAll = false
)
}
}
decodeWriteArgs(obj, metadata)?.let { return it }
return decodeEditArgs(obj, metadata)
}
private fun decodeEditArgs(obj: JsonObject, metadata: JsonObject? = null): EditTool.Args? {
val filePath = obj.string("file_path", "filePath", "path") ?: return null
val oldString = obj.string("old_string", "oldString", "old_text", "oldText") ?: return null
val newString = obj.string("new_string", "newString", "new_text", "newText") ?: return null
val shortDescription = obj.string("short_description", "shortDescription", "description")
?: metadata?.string("title")
?: "ACP edit"
val replaceAll = obj.boolean("replace_all", "replaceAll") ?: false
return EditTool.Args(filePath, oldString, newString, shortDescription, replaceAll)
}
private fun decodeWriteArgs(
obj: JsonObject,
metadata: JsonObject? = null
): WriteTool.Args? {
val filePath = obj.string("file_path", "filePath", "path")
?: metadata?.firstLocationPath()
?: metadata?.titlePath()
?: obj.firstChangePath()
?: return null
val content = obj.string("content", "text")
?: obj.firstChangeContent()
?: decodeDiffContent(metadata)?.takeIf { it.oldText == null }?.newText
?: return null
return WriteTool.Args(filePath, content)
}
private fun decodeReadArgs(obj: JsonObject, metadata: JsonObject? = null): ReadTool.Args? {
val filePath = obj.string("file_path", "filePath", "path")
?: metadata?.firstLocationPath()
?: metadata?.titlePath()
?: return null
return ReadTool.Args(
filePath = filePath,
offset = obj.int("offset", "line") ?: metadata?.int("line"),
limit = obj.int("limit", "maxLinesCount")
)
}
private fun decodeSearchArgs(obj: JsonObject): IntelliJSearchTool.Args? {
val pattern = obj.string("pattern", "searchText", "query", "nameKeyword") ?: return null
return IntelliJSearchTool.Args(
pattern = pattern,
scope = obj.string("scope"),
path = obj.string("path", "directoryToSearch"),
fileType = obj.string("fileType", "fileMask"),
context = null,
caseSensitive = obj.boolean("caseSensitive"),
regex = obj.boolean("regex"),
wholeWords = null,
outputMode = null,
limit = obj.int("limit", "maxUsageCount", "fileCountLimit")
)
}
private fun decodeBashArgs(obj: JsonObject): BashTool.Args? {
val command = obj.commandString() ?: return null
return BashTool.Args(
command = command,
workingDirectory = obj.string("workingDirectory", "workdir", "cwd"),
timeout = obj.int("timeout") ?: 60_000,
description = obj.string("description", "title"),
runInBackground = obj.boolean("run_in_background", "runInBackground")
)
}
private fun decodeMcpArgs(rawTitle: String, rawInput: JsonElement?): McpTool.Args {
val obj = rawInput.asJsonObjectOrNull(json) ?: JsonObject(emptyMap())
val callName = rawTitle.ifBlank { obj.string("tool_name", "toolName") ?: "unknown" }
val slashIndex = callName.indexOf('/')
val serverName = if (slashIndex > 0) callName.substring(0, slashIndex) else obj.string(
"server_name",
"serverName"
)
val toolName = if (slashIndex > 0 && slashIndex < callName.length - 1) {
callName.substring(slashIndex + 1)
} else {
obj.string("tool_name", "toolName") ?: callName.ifBlank { "unknown" }
}
val arguments = (obj["arguments"] as? JsonObject)?.toMap() ?: obj.toMap()
return McpTool.Args(
toolName = toolName,
serverId = obj.string("server_id", "serverId"),
serverName = serverName,
arguments = arguments
)
}
private fun decodeDiffContent(metadata: JsonObject?): DiffContent? {
val diff = (metadata?.get("content") as? JsonArray)
?.firstOrNull { entry ->
(entry as? JsonObject)?.string("type") == "diff"
} as? JsonObject
?: return null
val path = diff.string("path") ?: return null
val newText = diff.string("newText", "new_text") ?: return null
val oldText = diff.string("oldText", "old_text")
return DiffContent(path, oldText, newText)
}
private fun looksLikeMcpToolName(rawTitle: String): Boolean {
val candidate = rawTitle.trim()
if (candidate.isBlank() || ' ' in candidate || candidate.startsWith("/")) {
return false
}
val slashIndex = candidate.indexOf('/')
return slashIndex > 0 && slashIndex < candidate.length - 1
}
}

View file

@ -6,6 +6,7 @@ import ai.koog.agents.snapshot.providers.file.JVMFilePersistenceStorageProvider
import com.intellij.openapi.components.Service
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.project.Project
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
@ -203,9 +204,14 @@ class AgentCheckpointHistoryService(project: Project) {
}
return agentIds.mapNotNull { agentId ->
runCatching { buildSummary(agentId) }
.onFailure { logger.warn("Failed to load checkpoints for agentId=$agentId", it) }
.getOrNull()
try {
buildSummary(agentId)
} catch (cancelled: CancellationException) {
throw cancelled
} catch (throwable: Throwable) {
logger.warn("Failed to load checkpoints for agentId=$agentId", throwable)
null
}
}.sortedByDescending { it.latestCreatedAt }
}

View file

@ -1,118 +1,15 @@
package ee.carlrobert.codegpt.mcp
import com.intellij.execution.configurations.PathEnvironmentVariableUtil
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.util.SystemInfo
import java.io.File
import ee.carlrobert.codegpt.util.CommandRuntimeHelper
object McpCommandValidator {
private val logger = thisLogger()
fun resolveCommand(command: String): String? {
val commandFile = File(command)
if (commandFile.isAbsolute && commandFile.exists() && commandFile.canExecute()) {
return command
}
return when {
command == "npx" || command == "node" -> findNodeExecutable(command)
else -> PathEnvironmentVariableUtil.findInPath(command)?.absolutePath
}
}
private fun findNodeExecutable(command: String): String? {
PathEnvironmentVariableUtil.findInPath(command)?.let {
return it.absolutePath
}
findInCommonMacOSLocations(command)?.let {
return it
}
findViaNodeVersionManager(command)?.let {
return it
}
findViaEnvironmentHints(command)?.let {
return it
}
logger.warn("$command not found in any location")
return null
}
private fun findInCommonMacOSLocations(command: String): String? {
if (!SystemInfo.isMac) {
return null
}
val commonLocations = listOf(
"/usr/local/bin", // Homebrew (Intel Mac)
"/opt/homebrew/bin", // Homebrew (Apple Silicon)
"/usr/bin", // System Node.js
"/usr/local/share/npm/bin", // npm global bin
"/opt/homebrew/share/npm/bin" // npm global bin (Apple Silicon)
)
for (location in commonLocations) {
findExecutableInDirectory(File(location), command)?.let { return it }
}
return null
}
private fun findViaNodeVersionManager(command: String): String? {
val userHome = System.getProperty("user.home") ?: return null
System.getenv("NVM_DIR")?.let { nvmDir ->
findExecutableInDirectory(File(nvmDir, "current/bin"), command)?.let { return it }
}
findExecutableInDirectory(File(userHome, ".nvm/current/bin"), command)?.let { return it }
System.getenv("VOLTA_HOME")?.let { voltaHome ->
findExecutableInDirectory(File(voltaHome, "bin"), command)?.let { return it }
}
findExecutableInDirectory(File(userHome, ".volta/bin"), command)?.let { return it }
System.getenv("FNM_DIR")?.let { fnmDir ->
findExecutableInDirectory(File(fnmDir), command)?.let { return it }
}
return null
}
private fun findViaEnvironmentHints(command: String): String? {
System.getenv("NODE_PATH")?.let { nodePath ->
File(nodePath).parent?.let { binDir ->
findExecutableInDirectory(File(binDir), command)?.let { return it }
}
}
System.getenv("NPM_CONFIG_PREFIX")?.let { prefix ->
findExecutableInDirectory(File(prefix, "bin"), command)?.let { return it }
}
return null
}
private fun findExecutableInDirectory(directory: File, command: String): String? {
if (!directory.exists() || !directory.isDirectory) {
return null
}
val executable = File(directory, command)
if (executable.exists() && executable.canExecute()) {
return executable.absolutePath
}
if (SystemInfo.isWindows) {
for (ext in listOf(".exe", ".cmd", ".bat")) {
val executableWithExt = File(directory, command + ext)
if (executableWithExt.exists() && executableWithExt.canExecute()) {
return executableWithExt.absolutePath
}
}
}
return null
fun resolveCommand(
command: String,
extraEnvironment: Map<String, String> = emptyMap()
): String? {
return CommandRuntimeHelper.resolveCommand(command, extraEnvironment)
}
fun getCommandNotFoundMessage(command: String): String {
@ -145,4 +42,4 @@ object McpCommandValidator {
}
}
}
}
}

View file

@ -1,99 +1,17 @@
package ee.carlrobert.codegpt.mcp
import com.intellij.openapi.diagnostic.thisLogger
import java.io.File
import ee.carlrobert.codegpt.util.CommandRuntimeHelper
object McpPathHelper {
private val logger = thisLogger()
fun getAdditionalNodePaths(): List<String> {
val osName = System.getProperty("os.name").lowercase()
val userHome = System.getProperty("user.home")
val additionalPaths = mutableListOf<String>()
when {
osName.contains("mac") -> {
additionalPaths.addAll(listOf(
"/usr/local/bin",
"/opt/homebrew/bin",
"/usr/bin",
"/usr/local/share/npm/bin",
"/opt/homebrew/share/npm/bin",
"$userHome/.nvm/current/bin",
"$userHome/.nvm/versions/node/current/bin",
"$userHome/.volta/bin"
))
additionalPaths.addAll(getShellProfilePaths())
}
osName.contains("windows") -> additionalPaths.addAll(listOf(
"C:\\Program Files\\nodejs",
"${System.getenv("APPDATA")}\\npm"
))
else -> additionalPaths.addAll(listOf(
"/usr/local/bin",
"/usr/bin",
"$userHome/.nvm/current/bin",
"$userHome/.nvm/versions/node/current/bin"
))
}
return additionalPaths.distinct()
}
private fun getShellProfilePaths(): List<String> {
val userHome = System.getProperty("user.home") ?: return emptyList()
val shellProfiles = listOf(
File(userHome, ".zshrc"),
File(userHome, ".bash_profile"),
File(userHome, ".bashrc"),
File(userHome, ".profile"),
File(userHome, ".zprofile")
fun createEnvironment(
serverEnvironmentVariables: Map<String, String>,
resolvedCommand: String? = null
): MutableMap<String, String> {
return CommandRuntimeHelper.createEnvironment(
extraEnvironment = serverEnvironmentVariables,
resolvedCommand = resolvedCommand,
includeResolvedCommandParent = false
)
val pathEntries = mutableSetOf<String>()
for (profile in shellProfiles) {
if (profile.exists()) {
try {
val content = profile.readText()
val pathRegex = Regex("""(?:export\s+)?PATH\s*=\s*["']?([^"':]+)(?::[^"']*)?["']?""")
pathRegex.findAll(content).forEach { match ->
val pathValue = match.groupValues[1]
pathValue.split(":").forEach { path ->
if (path.isNotBlank()) {
pathEntries.add(path)
}
}
}
} catch (e: Exception) {
logger.error("Failed to read shell profile ${profile.absolutePath}: ${e.message}")
}
}
}
return pathEntries.toList()
}
fun createEnvironmentPath(environment: MutableMap<String, String>): MutableMap<String, String> {
val currentPath = environment["PATH"] ?: ""
val additionalPaths = getAdditionalNodePaths()
val pathSeparator = File.pathSeparator
val enhancedPath = (listOf(currentPath) + additionalPaths)
.filter { it.isNotEmpty() }
.joinToString(pathSeparator)
environment["PATH"] = enhancedPath
return environment
}
fun createEnvironment(serverEnvironmentVariables: Map<String, String>): MutableMap<String, String> {
val mergedEnv = mutableMapOf<String, String>()
mergedEnv.putAll(System.getenv())
mergedEnv.putAll(serverEnvironmentVariables)
return createEnvironmentPath(mergedEnv)
}
}

View file

@ -37,7 +37,10 @@ class McpSessionManager {
?: throw IllegalArgumentException("Server with ID $serverId not found")
val command = serverDetails.command ?: "npx"
val resolvedCommand = McpCommandValidator.resolveCommand(command)
val resolvedCommand = McpCommandValidator.resolveCommand(
command = command,
extraEnvironment = serverDetails.environmentVariables
)
if (resolvedCommand == null) {
val errorMsg = McpCommandValidator.getCommandNotFoundMessage(command)
logger.error(
@ -47,7 +50,7 @@ class McpSessionManager {
}
val mergedEnv =
McpPathHelper.createEnvironment(serverDetails.environmentVariables)
McpPathHelper.createEnvironment(serverDetails.environmentVariables, resolvedCommand)
val serverParameters = ServerParameters.builder(resolvedCommand)
.args(*serverDetails.arguments.toTypedArray())

View file

@ -0,0 +1,27 @@
package ee.carlrobert.codegpt.settings.agents.acp
import com.intellij.openapi.options.Configurable
import com.intellij.openapi.project.Project
import javax.swing.JComponent
class AcpAgentConfigurable(private val project: Project) : Configurable {
private lateinit var form: AcpAgentSettingsForm
override fun getDisplayName(): String = "ProxyAI: ACP"
override fun createComponent(): JComponent {
form = AcpAgentSettingsForm(project)
return form.createPanel()
}
override fun isModified(): Boolean = form.isModified()
override fun apply() {
form.applyChanges()
}
override fun reset() {
form.resetChanges()
}
}

View file

@ -0,0 +1,34 @@
package ee.carlrobert.codegpt.settings.agents.acp
import com.intellij.openapi.components.*
import ee.carlrobert.codegpt.agent.external.ExternalAcpAgentPreset
import ee.carlrobert.codegpt.agent.external.ExternalAcpAgents
@Service(Service.Level.PROJECT)
@State(
name = "CodeGPT_AcpAgentSettings",
storages = [Storage("CodeGPT_AcpAgentSettings.xml")]
)
class AcpAgentSettings :
SimplePersistentStateComponent<AcpAgentSettingsState>(
AcpAgentSettingsState().apply {
enabledAgentIds = ExternalAcpAgents.enabledByDefaultIds().toMutableList()
}
) {
fun getEnabledPresetIds(): List<String> = state.enabledAgentIds
fun getVisiblePresets(currentPresetId: String? = null): List<ExternalAcpAgentPreset> {
val visibleIds = state.enabledAgentIds.toMutableSet()
currentPresetId?.takeIf { it.isNotBlank() }?.let(visibleIds::add)
return ExternalAcpAgents.all().filter { it.id in visibleIds }
}
fun setEnabledPresetIds(ids: Collection<String>) {
state.enabledAgentIds = ids.distinct().toMutableList()
}
}
class AcpAgentSettingsState : BaseState() {
var enabledAgentIds by list<String>()
}

View file

@ -0,0 +1,238 @@
package ee.carlrobert.codegpt.settings.agents.acp
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.ui.DocumentAdapter
import com.intellij.ui.SearchTextField
import com.intellij.ui.components.JBLabel
import com.intellij.ui.components.JBScrollPane
import com.intellij.ui.table.JBTable
import com.intellij.util.ui.JBFont
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.UIUtil
import com.intellij.util.ui.components.BorderLayoutPanel
import ee.carlrobert.codegpt.agent.external.AcpIcons
import ee.carlrobert.codegpt.agent.external.ExternalAcpAgents
import java.awt.Component
import java.awt.Dimension
import javax.swing.JComponent
import javax.swing.JTable
import javax.swing.event.DocumentEvent
import javax.swing.table.AbstractTableModel
import javax.swing.table.DefaultTableCellRenderer
import javax.swing.table.TableRowSorter
class AcpAgentSettingsForm(project: Project) {
private val settings = project.service<AcpAgentSettings>()
private val selectedPresetIds = linkedSetOf<String>()
private val presets = ExternalAcpAgents.all()
private val root = BorderLayoutPanel()
private val searchField = SearchTextField().apply {
textEditor.emptyText.text = "Search ACP runtimes..."
border = JBUI.Borders.compound(
JBUI.Borders.customLine(UIUtil.getTooltipSeparatorColor(), 1),
JBUI.Borders.empty(3, 8)
)
textEditor.border = JBUI.Borders.empty()
}
private val tableModel = AcpAgentTableModel()
private val rowSorter = TableRowSorter(tableModel)
private val helperLabel = JBLabel(
"ACP runtimes are external agents that can appear in the Agent runtime dropdown."
).apply {
font = JBFont.small()
foreground = UIUtil.getContextHelpForeground()
border = JBUI.Borders.emptyTop(6)
}
private val table = JBTable(tableModel).apply {
rowSorter = this@AcpAgentSettingsForm.rowSorter
emptyText.text = "No ACP runtimes match your search."
setShowGrid(false)
intercellSpacing = Dimension(0, 0)
rowHeight = JBUI.scale(28)
fillsViewportHeight = true
tableHeader.reorderingAllowed = false
tableHeader.resizingAllowed = true
autoResizeMode = JTable.AUTO_RESIZE_LAST_COLUMN
setSelectionMode(javax.swing.ListSelectionModel.SINGLE_SELECTION)
setDefaultRenderer(String::class.java, AcpTableCellRenderer())
}
init {
buildUi()
configureTable()
installSearch()
resetChanges()
}
fun createPanel(): JComponent = root
fun isModified(): Boolean {
return selectedPresetIds != settings.getEnabledPresetIds().toSet()
}
fun applyChanges() {
settings.setEnabledPresetIds(selectedPresetIds)
}
fun resetChanges() {
selectedPresetIds.clear()
selectedPresetIds += settings.getEnabledPresetIds()
tableModel.fireTableDataChanged()
refreshFilter()
}
private fun buildUi() {
val scrollPane = JBScrollPane(table).apply {
border = JBUI.Borders.customLine(UIUtil.getTooltipSeparatorColor(), 1)
horizontalScrollBarPolicy = JBScrollPane.HORIZONTAL_SCROLLBAR_NEVER
}
root.border = JBUI.Borders.empty(8)
root.addToTop(
BorderLayoutPanel().apply {
border = JBUI.Borders.emptyBottom(8)
addToTop(searchField)
addToCenter(helperLabel)
}
)
root.addToCenter(scrollPane)
}
private fun configureTable() {
rowSorter.setSortable(COL_ENABLED, false)
rowSorter.setSortable(COL_NAME, false)
rowSorter.setSortable(COL_VENDOR, false)
rowSorter.setSortable(COL_COMMAND, false)
table.columnModel.getColumn(COL_ENABLED).apply {
minWidth = JBUI.scale(36)
maxWidth = JBUI.scale(36)
preferredWidth = JBUI.scale(36)
}
table.columnModel.getColumn(COL_NAME).preferredWidth = JBUI.scale(170)
table.columnModel.getColumn(COL_VENDOR).preferredWidth = JBUI.scale(90)
table.columnModel.getColumn(COL_COMMAND).preferredWidth = JBUI.scale(360)
}
private fun installSearch() {
searchField.addDocumentListener(object : DocumentAdapter() {
override fun textChanged(e: DocumentEvent) {
refreshFilter()
}
})
}
private fun refreshFilter() {
val query = searchField.text.trim().lowercase()
rowSorter.rowFilter = if (query.isBlank()) {
null
} else {
object : javax.swing.RowFilter<AcpAgentTableModel, Int>() {
override fun include(entry: Entry<out AcpAgentTableModel, out Int>): Boolean {
val preset = presets[entry.identifier]
return listOf(
preset.displayName,
preset.vendor,
preset.description.orEmpty(),
preset.fullCommand()
).joinToString(" ").lowercase().contains(query)
}
}
}
}
private inner class AcpAgentTableModel : AbstractTableModel() {
override fun getRowCount(): Int = presets.size
override fun getColumnCount(): Int = 4
override fun getColumnName(column: Int): String {
return when (column) {
COL_ENABLED -> ""
COL_NAME -> "Agent"
COL_VENDOR -> "Vendor"
COL_COMMAND -> "Command"
else -> ""
}
}
override fun getColumnClass(columnIndex: Int): Class<*> {
return if (columnIndex == COL_ENABLED) {
java.lang.Boolean::class.java
} else {
String::class.java
}
}
override fun isCellEditable(rowIndex: Int, columnIndex: Int): Boolean {
return columnIndex == COL_ENABLED
}
override fun getValueAt(rowIndex: Int, columnIndex: Int): Any {
val preset = presets[rowIndex]
return when (columnIndex) {
COL_ENABLED -> preset.id in selectedPresetIds
COL_NAME -> preset.displayName
COL_VENDOR -> preset.vendor
COL_COMMAND -> preset.fullCommand()
else -> ""
}
}
override fun setValueAt(aValue: Any?, rowIndex: Int, columnIndex: Int) {
if (columnIndex != COL_ENABLED) {
return
}
val preset = presets[rowIndex]
val enabled = aValue as? Boolean ?: false
if (enabled) {
selectedPresetIds.add(preset.id)
} else {
selectedPresetIds.remove(preset.id)
}
fireTableCellUpdated(rowIndex, columnIndex)
}
}
private inner class AcpTableCellRenderer : DefaultTableCellRenderer() {
override fun getTableCellRendererComponent(
table: JTable,
value: Any?,
isSelected: Boolean,
hasFocus: Boolean,
row: Int,
column: Int,
): Component {
val component =
super.getTableCellRendererComponent(table, value, isSelected, false, row, column)
val modelRow = table.convertRowIndexToModel(row)
val preset = presets[modelRow]
horizontalAlignment = LEFT
border = JBUI.Borders.empty(0, 6)
icon = if (column == COL_NAME) AcpIcons.iconFor(preset.id) else null
iconTextGap = JBUI.scale(8)
font = if (column == COL_NAME) {
JBFont.label().asBold()
} else {
JBFont.small()
}
foreground = when {
isSelected -> table.selectionForeground
column == COL_NAME -> UIUtil.getLabelForeground()
else -> UIUtil.getContextHelpForeground()
}
return component
}
}
private companion object {
const val COL_ENABLED = 0
const val COL_NAME = 1
const val COL_VENDOR = 2
const val COL_COMMAND = 3
}
}

View file

@ -19,10 +19,14 @@ class McpClientManager {
fun createClient(serverDetails: McpServerDetailsState): McpSyncClient? {
return try {
val command = serverDetails.command ?: "npx"
val resolvedCommand = McpCommandValidator.resolveCommand(command)
val resolvedCommand = McpCommandValidator.resolveCommand(
command = command,
extraEnvironment = serverDetails.environmentVariables
)
?: throw IllegalStateException(McpCommandValidator.getCommandNotFoundMessage(command))
val enhancedEnv = McpPathHelper.createEnvironment(serverDetails.environmentVariables)
val enhancedEnv =
McpPathHelper.createEnvironment(serverDetails.environmentVariables, resolvedCommand)
val connectionParams = ServerParameters.builder(resolvedCommand)
.args(*serverDetails.arguments.toTypedArray())
.env(enhancedEnv)
@ -57,7 +61,10 @@ class McpClientManager {
return try {
val command = serverDetails.command ?: "npx"
val resolvedCommand = McpCommandValidator.resolveCommand(command)
val resolvedCommand = McpCommandValidator.resolveCommand(
command = command,
extraEnvironment = serverDetails.environmentVariables
)
if (resolvedCommand == null) {
logger.warn("Command not found for '${serverDetails.name}': $command")
return ConnectionTestResult(

View file

@ -18,6 +18,7 @@ import ee.carlrobert.codegpt.settings.service.FeatureType
import ee.carlrobert.codegpt.settings.service.ServiceType
import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings
import ee.carlrobert.codegpt.toolwindow.ui.ModelListPopup
import ee.carlrobert.codegpt.toolwindow.ui.ModelListPopups
import java.awt.Color
import javax.swing.Icon
import javax.swing.JComponent
@ -190,18 +191,7 @@ class SettingsModelComboBoxAction(
group: DefaultActionGroup,
context: DataContext,
disposeCallback: Runnable?
): JBPopup {
val popup = ModelListPopup(group, context)
if (disposeCallback != null) {
popup.addListener(object : JBPopupListener {
override fun onClosed(event: LightweightWindowEvent) {
disposeCallback.run()
}
})
}
popup.isShowSubmenuOnHover = true
return popup
}
): JBPopup = ModelListPopups.createPopup(group, context, disposeCallback)
private fun getModelsForFeature(featureType: FeatureType): List<ModelSelection> {
val allModels = service<ModelSettings>().getAvailableModels(featureType)

View file

@ -5,6 +5,22 @@ import ee.carlrobert.codegpt.agent.history.CheckpointRef
import ee.carlrobert.codegpt.conversations.Conversation
import ee.carlrobert.codegpt.settings.service.ServiceType
data class AcpConfigOptionChoice(
val value: String,
val name: String,
val description: String? = null
)
data class AcpConfigOption(
val id: String,
val name: String,
val description: String? = null,
val category: String? = null,
val type: String? = null,
val currentValue: String? = null,
val options: List<AcpConfigOptionChoice> = emptyList()
)
/**
* Represents a single Agent session with its own conversation state and metadata.
* Each tab in the Agent tool window corresponds to one AgentSession.
@ -14,9 +30,15 @@ data class AgentSession(
val conversation: Conversation,
var displayName: String = "",
var serviceType: ServiceType? = null,
var externalAgentId: String? = null,
var externalAgentSessionId: String? = null,
var externalAgentConfigOptions: List<AcpConfigOption> = emptyList(),
var externalAgentConfigLoading: Boolean = false,
var runtimeAgentId: String? = null,
var resumeCheckpointRef: CheckpointRef? = null,
val referencedFiles: List<VirtualFile> = emptyList(),
val createdAt: Long = System.currentTimeMillis(),
var lastActiveAt: Long = System.currentTimeMillis()
)
) {
var externalAgentErrorMessage: String? = null
}

View file

@ -85,6 +85,7 @@ class AgentToolWindowPanel(
fun getTabbedPane(): AgentToolWindowTabbedPane = tabbedPane
private fun showTabsView() {
disposeLandingPanel()
centerLayout.show(centerPanel, TABS_CARD)
}
@ -107,7 +108,8 @@ class AgentToolWindowPanel(
project = project,
agentSession = draftSession,
draftSubmitHandler = { message ->
val panel = contentManager.createNewAgentTab()
disposeLandingPanel()
val panel = contentManager.createNewAgentTab(draftSession)
panel.submitMessage(message)
}
)

View file

@ -1,5 +1,7 @@
package ee.carlrobert.codegpt.toolwindow.agent
import com.intellij.notification.NotificationType
import com.intellij.openapi.actionSystem.ActionPlaces
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.EDT
@ -15,6 +17,8 @@ import com.intellij.util.ui.JBUI
import com.intellij.util.ui.components.BorderLayoutPanel
import ee.carlrobert.codegpt.CodeGPTBundle
import ee.carlrobert.codegpt.agent.*
import ee.carlrobert.codegpt.agent.external.ExternalAcpAgents
import ee.carlrobert.codegpt.agent.external.ExternalAcpAgentService
import ee.carlrobert.codegpt.agent.ProxyAIAgent.loadProjectInstructions
import ee.carlrobert.codegpt.agent.history.AgentCheckpointHistoryService
import ee.carlrobert.codegpt.agent.history.AgentCheckpointTurnSequencer
@ -27,6 +31,8 @@ import ee.carlrobert.codegpt.psistructure.PsiStructureProvider
import ee.carlrobert.codegpt.settings.service.FeatureType
import ee.carlrobert.codegpt.settings.models.ModelSettings
import ee.carlrobert.codegpt.toolwindow.agent.ui.AgentToolWindowLandingPanel
import ee.carlrobert.codegpt.toolwindow.agent.ui.AgentModelComboBoxAction
import ee.carlrobert.codegpt.toolwindow.agent.ui.AgentRuntimeOptionsComboBoxAction
import ee.carlrobert.codegpt.toolwindow.agent.ui.RollbackPanel
import ee.carlrobert.codegpt.toolwindow.agent.ui.TodoListPanel
import ee.carlrobert.codegpt.toolwindow.agent.ui.ToolCallCard
@ -43,6 +49,7 @@ import ee.carlrobert.codegpt.ui.components.TokenUsageCounterPanel
import ee.carlrobert.codegpt.ui.queue.QueuedMessagePanel
import ee.carlrobert.codegpt.ui.textarea.UserInputPanel
import ee.carlrobert.codegpt.ui.textarea.header.tag.TagManager
import ee.carlrobert.codegpt.ui.OverlayUtil
import ee.carlrobert.codegpt.util.EditorUtil
import ee.carlrobert.codegpt.util.StringUtil.stripThinkingBlocks
import ee.carlrobert.codegpt.util.coroutines.CoroutineDispatchers
@ -122,9 +129,15 @@ class AgentToolWindowTabPanel(
onStop = ::handleCancel,
withRemovableSelectedEditorTag = true,
agentTokenCounterPanel = TokenUsageCounterPanel(project, sessionId),
agentTokenCounterVisibilityProvider = { agentSession.externalAgentId.isNullOrBlank() },
sessionIdProvider = { sessionId },
conversationIdProvider = { conversation.id },
onStartSessionTimeline = ::showSessionStartTimelineDialog
onStartSessionTimeline = ::showSessionStartTimelineDialog,
modelSelectorComponentFactory = ::createAgentModelSelector,
secondaryFooterComponentFactory = ::createAgentRuntimeOptionsSelector,
secondaryFooterComponentVisibilityProvider = { !agentSession.externalAgentId.isNullOrBlank() },
promptEnhancerVisibilityProvider = { agentSession.externalAgentId.isNullOrBlank() },
sessionTimelineVisibilityProvider = { agentSession.externalAgentId.isNullOrBlank() }
)
private var rollbackPanel: RollbackPanel
private val todoListPanel = TodoListPanel()
@ -382,6 +395,105 @@ class AgentToolWindowTabPanel(
}
}
private fun createAgentModelSelector(inputPanel: UserInputPanel): JComponent {
val modelSettings = ModelSettings.getInstance()
return AgentModelComboBoxAction(
project,
agentSession,
{
inputPanel.refreshModelDependentState()
},
{ externalAgentId ->
project.service<ExternalAcpAgentService>().closeSession(sessionId)
agentSession.externalAgentId = externalAgentId
agentSession.externalAgentSessionId = null
agentSession.externalAgentConfigOptions = emptyList()
agentSession.externalAgentErrorMessage = null
agentSession.externalAgentConfigLoading = !externalAgentId.isNullOrBlank()
if (!externalAgentId.isNullOrBlank()) {
backgroundScope.launch {
runCatching {
project.service<ExternalAcpAgentService>().warmUpSession(agentSession)
}.onFailure { ex ->
agentSession.externalAgentConfigLoading = false
project.service<ExternalAcpAgentService>().closeSession(sessionId)
agentSession.externalAgentErrorMessage =
buildExternalAgentFailureMessage(externalAgentId, ex)
OverlayUtil.showNotification(
"${displayExternalAgentName(externalAgentId)} unavailable. ${agentSession.externalAgentErrorMessage}",
NotificationType.ERROR
)
}
withContext(Dispatchers.EDT) {
inputPanel.refreshModelDependentState()
}
}
}
},
modelSettings.getServiceForFeature(FeatureType.AGENT),
modelSettings.getAvailableProviders(FeatureType.AGENT),
true
).createCustomComponent(com.intellij.openapi.actionSystem.ActionPlaces.UNKNOWN)
}
private fun displayExternalAgentName(externalAgentId: String): String {
return ExternalAcpAgents.find(externalAgentId)?.displayName ?: externalAgentId
}
private fun buildExternalAgentFailureMessage(
externalAgentId: String,
throwable: Throwable
): String {
val command = ExternalAcpAgents.find(externalAgentId)?.command ?: externalAgentId
val message = throwable.message.orEmpty()
return when {
message.contains("Cannot run program", ignoreCase = true) &&
message.contains("No such file or directory", ignoreCase = true) ->
"Command not found: $command"
message.isNotBlank() -> message
else -> "Failed to start ${displayExternalAgentName(externalAgentId)}"
}
}
private fun buildExternalAgentConfigFailureMessage(throwable: Throwable): String {
val message = throwable.message.orEmpty()
return when {
message.contains("does not support runtime option changes", ignoreCase = true) ->
"This agent exposes runtime info but does not support changing it over ACP."
message.isNotBlank() -> message
else -> "Failed to update runtime option"
}
}
private fun createAgentRuntimeOptionsSelector(inputPanel: UserInputPanel): JComponent {
return AgentRuntimeOptionsComboBoxAction(
agentSession,
{ optionId, value ->
backgroundScope.launch {
agentSession.externalAgentConfigLoading = true
withContext(Dispatchers.EDT) {
inputPanel.refreshModelDependentState()
}
runCatching {
project.service<ExternalAcpAgentService>()
.setSessionConfigOption(agentSession, optionId, value)
}.onFailure { ex ->
agentSession.externalAgentConfigLoading = false
OverlayUtil.showNotification(
"${displayExternalAgentName(agentSession.externalAgentId ?: "agent")} option update failed. ${buildExternalAgentConfigFailureMessage(ex)}",
NotificationType.ERROR
)
}
withContext(Dispatchers.EDT) {
inputPanel.refreshModelDependentState()
}
}
}
).createCustomComponent(com.intellij.openapi.actionSystem.ActionPlaces.UNKNOWN)
}
private fun displayLandingView() {
disposeLandingPanelIfPresent()
val landingPanel = createLandingView()

View file

@ -330,12 +330,15 @@ class AgentToolWindowTabbedPane(private val project: Project) : JBTabbedPane(),
fun resetCurrentlyActiveTabPanel() {
tryFindActiveTabPanel().ifPresent { tabPanel ->
val oldDisplayName = tabPanel.getAgentSession().displayName
val oldSession = tabPanel.getAgentSession()
val oldDisplayName = oldSession.displayName
closeTabAt(selectedIndex)
val newSession = AgentSession(
UUID.randomUUID().toString(),
Conversation(),
displayName = oldDisplayName
displayName = oldDisplayName,
serviceType = oldSession.serviceType,
externalAgentId = oldSession.externalAgentId
)
project.service<AgentToolWindowContentManager>().createNewAgentTab(newSession)
repaint()
@ -423,7 +426,8 @@ class AgentToolWindowTabbedPane(private val project: Project) : JBTabbedPane(),
activeTabMapping.entries
.filter { it.key != selectedPopupTabTitle }
.forEach { entry ->
project.service<AgentToolWindowContentManager>().removeSession(entry.value.getSessionId())
project.service<AgentToolWindowContentManager>()
.removeSession(entry.value.getSessionId())
Disposer.dispose(entry.value)
}

View file

@ -0,0 +1,509 @@
package ee.carlrobert.codegpt.toolwindow.agent.ui
import com.intellij.icons.AllIcons
import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.actionSystem.DefaultActionGroup
import com.intellij.openapi.actionSystem.Presentation
import com.intellij.openapi.actionSystem.ex.ComboBoxAction
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.service
import com.intellij.openapi.editor.colors.EditorColorsManager
import com.intellij.openapi.options.ShowSettingsUtil
import com.intellij.openapi.project.DumbAwareAction
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.popup.JBPopup
import ee.carlrobert.codegpt.Icons
import ee.carlrobert.codegpt.agent.external.AcpIcons
import ee.carlrobert.codegpt.agent.external.ExternalAcpAgentPreset
import ee.carlrobert.codegpt.agent.external.ExternalAcpAgents
import ee.carlrobert.codegpt.completions.llama.LlamaModel
import ee.carlrobert.codegpt.settings.GeneralSettingsConfigurable
import ee.carlrobert.codegpt.settings.agents.acp.AcpAgentSettings
import ee.carlrobert.codegpt.settings.models.ModelSelection
import ee.carlrobert.codegpt.settings.models.ModelSettings
import ee.carlrobert.codegpt.settings.service.FeatureType
import ee.carlrobert.codegpt.settings.service.ModelChangeNotifier
import ee.carlrobert.codegpt.settings.service.ModelChangeNotifierAdapter
import ee.carlrobert.codegpt.settings.service.ServiceType
import ee.carlrobert.codegpt.settings.service.ServiceType.ANTHROPIC
import ee.carlrobert.codegpt.settings.service.ServiceType.CUSTOM_OPENAI
import ee.carlrobert.codegpt.settings.service.ServiceType.GOOGLE
import ee.carlrobert.codegpt.settings.service.ServiceType.INCEPTION
import ee.carlrobert.codegpt.settings.service.ServiceType.LLAMA_CPP
import ee.carlrobert.codegpt.settings.service.ServiceType.MISTRAL
import ee.carlrobert.codegpt.settings.service.ServiceType.OLLAMA
import ee.carlrobert.codegpt.settings.service.ServiceType.OPENAI
import ee.carlrobert.codegpt.settings.service.ServiceType.PROXYAI
import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings
import ee.carlrobert.codegpt.settings.service.ollama.OllamaSettings
import ee.carlrobert.codegpt.toolwindow.agent.AgentSession
import ee.carlrobert.codegpt.toolwindow.ui.CodeGPTModelsListPopupAction
import ee.carlrobert.codegpt.toolwindow.ui.ModelListPopups
import java.awt.Color
import javax.swing.Icon
import javax.swing.JComponent
class AgentModelComboBoxAction(
private val project: Project,
private val agentSession: AgentSession,
private val onModelChange: (ServiceType) -> Unit,
private val onAgentRuntimeChanged: (String?) -> Unit,
selectedProvider: ServiceType,
private val availableProviders: List<ServiceType>,
private val showConfigureModels: Boolean
) : ComboBoxAction() {
private data class TemplateState(
val icon: Icon,
val text: String
)
private val modelSettings = ModelSettings.getInstance()
init {
isSmallVariant = true
updateTemplatePresentation(selectedProvider)
ApplicationManager.getApplication().messageBus.connect().subscribe(
ModelChangeNotifier.getTopic(),
object : ModelChangeNotifierAdapter() {
override fun modelChanged(
featureType: FeatureType,
newModel: String,
serviceType: ServiceType
) {
if (featureType == FeatureType.AGENT && agentSession.externalAgentId == null) {
updateTemplatePresentation(serviceType)
}
}
}
)
}
fun createCustomComponent(place: String): JComponent {
return createCustomComponent(templatePresentation, place)
}
override fun createCustomComponent(
presentation: Presentation,
place: String
): JComponent {
val button = createComboBoxButton(presentation)
button.foreground = EditorColorsManager.getInstance().globalScheme.defaultForeground
button.border = null
button.putClientProperty("JButton.backgroundColor", Color(0, 0, 0, 0))
button.putClientProperty(
"proxyai.refreshPresentation",
Runnable {
updateTemplatePresentation(modelSettings.getServiceForFeature(FeatureType.AGENT))
}
)
return button
}
override fun createActionPopup(
group: DefaultActionGroup,
context: DataContext,
disposeCallback: Runnable?
): JBPopup {
return ModelListPopups.createPopup(group, context, disposeCallback)
}
override fun createPopupActionGroup(
button: JComponent,
context: DataContext
): DefaultActionGroup {
return buildPopupActionGroup((button as ComboBoxButton).presentation)
}
override fun shouldShowDisabledActions(): Boolean = true
private fun buildPopupActionGroup(presentation: Presentation): DefaultActionGroup {
return DefaultActionGroup().apply {
addSeparator("Cloud")
addProxyAIGroup()
addModelGroup(presentation, ANTHROPIC, "Anthropic", Icons.Anthropic)
addModelGroup(presentation, OPENAI, "OpenAI", Icons.OpenAI)
addModelGroup(presentation, CUSTOM_OPENAI, "Custom OpenAI", Icons.OpenAI)
addModelGroup(presentation, GOOGLE, "Google", Icons.Google)
addModelGroup(presentation, MISTRAL, "Mistral", Icons.Mistral)
addInceptionGroup(presentation)
addOfflineGroups(presentation)
addExternalAgentsSection()
}
}
private fun DefaultActionGroup.addProxyAIGroup() {
if (PROXYAI !in availableProviders) {
return
}
val group = DefaultActionGroup.createPopupGroup { "ProxyAI" }
group.templatePresentation.icon = Icons.DefaultSmall
group.addAll(proxyAIModelActions().toList())
add(group)
}
private fun proxyAIModelActions(): Array<AnAction> {
return availableModelsForProvider(PROXYAI)
.map(::createCodeGPTModelAction)
.toTypedArray()
}
private fun DefaultActionGroup.addModelGroup(
presentation: Presentation,
provider: ServiceType,
label: String,
icon: Icon
) {
if (provider !in availableProviders) {
return
}
val group = DefaultActionGroup.createPopupGroup { label }
group.templatePresentation.icon = icon
availableModelsForProvider(provider).forEach { model ->
group.add(
createModelAction(
serviceType = provider,
label = model.displayName,
icon = icon,
comboBoxPresentation = presentation
) {
modelSettings.setModel(FeatureType.AGENT, model.model, provider)
}
)
}
add(group)
}
private fun DefaultActionGroup.addInceptionGroup(presentation: Presentation) {
if (INCEPTION !in availableProviders) {
return
}
val group = DefaultActionGroup.createPopupGroup { "Inception" }
group.templatePresentation.icon = Icons.Inception
group.add(createInceptionModelAction(presentation))
add(group)
}
private fun DefaultActionGroup.addOfflineGroups(presentation: Presentation) {
if (LLAMA_CPP !in availableProviders && OLLAMA !in availableProviders) {
return
}
addSeparator("Offline")
if (LLAMA_CPP in availableProviders) {
add(createLlamaModelAction(presentation))
}
if (OLLAMA in availableProviders) {
add(createOllamaGroup(presentation))
}
}
private fun createOllamaGroup(presentation: Presentation): DefaultActionGroup {
val group = DefaultActionGroup.createPopupGroup { "Ollama" }
group.templatePresentation.icon = Icons.Ollama
ApplicationManager.getApplication()
.getService(OllamaSettings::class.java)
.state
.availableModels
.forEach { model ->
group.add(createOllamaModelAction(model, presentation))
}
return group
}
private fun DefaultActionGroup.addExternalAgentsSection() {
val externalAgents = availableExternalAgents()
if (externalAgents.isEmpty() && !showConfigureModels) {
return
}
addSeparator("Agents")
if (externalAgents.isEmpty()) {
if (showConfigureModels) {
add(createNoAgentRuntimesAction())
}
} else {
externalAgents.forEach { preset ->
add(createAgentRuntimeAction(preset))
}
}
if (showConfigureModels) {
addSeparator()
add(createGoToSettingsAction())
}
}
private fun updateTemplatePresentation(selectedService: ServiceType) {
val externalAgentId = agentSession.externalAgentId
if (!externalAgentId.isNullOrBlank()) {
updateExternalAgentPresentation(externalAgentId)
return
}
val templateState = templateState(selectedService)
templatePresentation.icon = templateState.icon
templatePresentation.text = templateState.text
}
private fun templateState(selectedService: ServiceType): TemplateState {
val modelCode = selectedAgentModelCode()
return when (selectedService) {
PROXYAI -> proxyAITemplateState(modelCode)
OPENAI -> providerTemplateState(OPENAI, Icons.OpenAI, modelCode)
CUSTOM_OPENAI -> TemplateState(
Icons.OpenAI,
customOpenAIModelDisplayName(modelCode)
)
ANTHROPIC -> providerTemplateState(ANTHROPIC, Icons.Anthropic, modelCode)
LLAMA_CPP -> providerTemplateState(LLAMA_CPP, Icons.Llama, modelCode)
OLLAMA -> providerTemplateState(OLLAMA, Icons.Ollama, modelCode)
GOOGLE -> providerTemplateState(
GOOGLE,
Icons.Google,
resolvedGoogleModelCode(modelCode)
)
MISTRAL -> providerTemplateState(MISTRAL, Icons.Mistral, modelCode)
INCEPTION -> providerTemplateState(INCEPTION, Icons.Inception, modelCode)
}
}
private fun proxyAITemplateState(modelCode: String?): TemplateState {
val proxyAIModel = availableModelsForProvider(PROXYAI)
.firstOrNull { it.model == modelCode }
return TemplateState(
icon = proxyAIModel?.icon ?: Icons.DefaultSmall,
text = proxyAIModel?.displayName ?: "Unknown"
)
}
private fun providerTemplateState(
serviceType: ServiceType,
icon: Icon,
modelCode: String?
): TemplateState {
return TemplateState(icon, modelSettings.getModelDisplayName(serviceType, modelCode))
}
private fun customOpenAIModelDisplayName(modelCode: String?): String {
return availableModelsForProvider(CUSTOM_OPENAI)
.firstOrNull { it.model == modelCode }
?.displayName
?: modelSettings.getModelDisplayName(CUSTOM_OPENAI, modelCode)
}
private fun selectedAgentModelCode(): String? {
return modelSettings.getStoredModelForFeature(FeatureType.AGENT)
}
private fun updateExternalAgentPresentation(externalAgentId: String) {
val preset = ExternalAcpAgents.find(externalAgentId)
if (preset != null) {
templatePresentation.icon = externalAgentIcon(preset.id)
templatePresentation.text = preset.displayName
return
}
templatePresentation.icon = Icons.DefaultSmall
templatePresentation.text = "ACP"
}
private fun createAgentRuntimeAction(preset: ExternalAcpAgentPreset): AnAction {
return object : DumbAwareAction(
preset.displayName,
preset.description,
externalAgentIcon(preset.id)
) {
override fun update(event: AnActionEvent) {
event.presentation.isEnabled = preset.id != agentSession.externalAgentId
}
override fun actionPerformed(event: AnActionEvent) {
agentSession.externalAgentId = preset.id
agentSession.externalAgentSessionId = null
onAgentRuntimeChanged(preset.id)
updateExternalAgentPresentation(preset.id)
onModelChange(modelSettings.getServiceForFeature(FeatureType.AGENT))
}
override fun getActionUpdateThread(): ActionUpdateThread {
return ActionUpdateThread.BGT
}
}
}
private fun resolvedGoogleModelCode(modelCode: String?): String {
if (!modelCode.isNullOrBlank()) {
return modelCode
}
return availableModelsForProvider(GOOGLE)
.firstOrNull()
?.model
.orEmpty()
}
private fun llamaCppPresentationText(): String {
val huggingFaceModel = LlamaSettings.getCurrentState().huggingFaceModel
val llamaModel = LlamaModel.findByHuggingFaceModel(huggingFaceModel)
return "%s (%dB)".format(llamaModel.label, huggingFaceModel.parameterSize)
}
private fun createModelAction(
serviceType: ServiceType,
label: String,
icon: Icon,
comboBoxPresentation: Presentation,
onModelChanged: (() -> Unit)? = null
): AnAction {
return object : DumbAwareAction(label, "", icon) {
override fun update(event: AnActionEvent) {
val currentExternalAgent = agentSession.externalAgentId
event.presentation.isEnabled = when {
!currentExternalAgent.isNullOrBlank() -> true
else -> event.presentation.text != comboBoxPresentation.text
}
}
override fun actionPerformed(event: AnActionEvent) {
clearExternalAgentSelection()
onModelChanged?.invoke()
handleModelChange(serviceType)
}
override fun getActionUpdateThread(): ActionUpdateThread {
return ActionUpdateThread.BGT
}
}
}
private fun handleModelChange(serviceType: ServiceType) {
updateTemplatePresentation(serviceType)
onModelChange(serviceType)
}
private fun createCodeGPTModelAction(model: ModelSelection): AnAction {
val selected = isProxyAIModelSelected(model.model) && agentSession.externalAgentId == null
return CodeGPTModelsListPopupAction(
model.displayName,
model.model,
model.icon ?: Icons.DefaultSmall,
false,
selected,
Runnable {
clearExternalAgentSelection()
setAgentModel(PROXYAI, model.model)
handleModelChange(PROXYAI)
}
)
}
private fun createOllamaModelAction(model: String, presentation: Presentation): AnAction {
return createModelAction(
serviceType = OLLAMA,
label = model,
icon = Icons.Ollama,
comboBoxPresentation = presentation
) {
ApplicationManager.getApplication()
.getService(OllamaSettings::class.java)
.state
.model = model
setAgentModel(OLLAMA, model)
}
}
private fun createLlamaModelAction(presentation: Presentation): AnAction {
return createModelAction(
serviceType = LLAMA_CPP,
label = llamaCppPresentationText(),
icon = Icons.Llama,
comboBoxPresentation = presentation
) {
setAgentModel(
LLAMA_CPP,
LlamaSettings.getCurrentState().huggingFaceModel.code
)
}
}
private fun createInceptionModelAction(presentation: Presentation): AnAction {
val modelCode = availableModelsForProvider(INCEPTION)
.firstOrNull()
?.model
?: "mercury"
return createModelAction(
serviceType = INCEPTION,
label = modelSettings.getModelDisplayName(INCEPTION, modelCode),
icon = Icons.Inception,
comboBoxPresentation = presentation
) {
setAgentModel(INCEPTION, modelCode)
}
}
private fun availableExternalAgents(): List<ExternalAcpAgentPreset> {
return project.service<AcpAgentSettings>().getVisiblePresets(agentSession.externalAgentId)
}
private fun createNoAgentRuntimesAction(): DumbAwareAction {
return object : DumbAwareAction("No Agent Runtimes") {
override fun actionPerformed(event: AnActionEvent) = Unit
override fun update(event: AnActionEvent) {
event.presentation.isEnabled = false
}
override fun getActionUpdateThread(): ActionUpdateThread {
return ActionUpdateThread.BGT
}
}
}
private fun createGoToSettingsAction(): DumbAwareAction {
return object : DumbAwareAction("Go to Settings", "", AllIcons.General.Settings) {
override fun actionPerformed(event: AnActionEvent) {
ShowSettingsUtil.getInstance()
.showSettingsDialog(project, GeneralSettingsConfigurable::class.java)
}
override fun getActionUpdateThread(): ActionUpdateThread {
return ActionUpdateThread.BGT
}
}
}
private fun externalAgentIcon(externalAgentId: String): Icon {
return AcpIcons.iconFor(externalAgentId)
}
private fun availableModelsForFeature(): List<ModelSelection> {
return modelSettings.getAvailableModels(FeatureType.AGENT)
}
private fun availableModelsForProvider(provider: ServiceType): List<ModelSelection> {
return availableModelsForFeature().filter { it.provider == provider }
}
private fun clearExternalAgentSelection() {
agentSession.externalAgentId = null
agentSession.externalAgentSessionId = null
onAgentRuntimeChanged(null)
}
private fun setAgentModel(serviceType: ServiceType, modelCode: String) {
modelSettings.setModel(FeatureType.AGENT, modelCode, serviceType)
}
private fun isProxyAIModelSelected(modelCode: String?): Boolean {
val current = modelSettings.getModelSelection(FeatureType.AGENT) ?: return false
return current.provider == PROXYAI && modelCode == current.model
}
}

View file

@ -0,0 +1,205 @@
package ee.carlrobert.codegpt.toolwindow.agent.ui
import com.intellij.icons.AllIcons
import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.actionSystem.DefaultActionGroup
import com.intellij.openapi.actionSystem.Presentation
import com.intellij.openapi.actionSystem.ex.ComboBoxAction
import com.intellij.openapi.editor.colors.EditorColorsManager
import com.intellij.openapi.project.DumbAwareAction
import com.intellij.openapi.ui.popup.JBPopup
import com.intellij.ui.AnimatedIcon
import ee.carlrobert.codegpt.toolwindow.agent.AcpConfigOption
import ee.carlrobert.codegpt.toolwindow.agent.AgentSession
import ee.carlrobert.codegpt.toolwindow.ui.ModelListPopups
import java.awt.Color
import javax.swing.Icon
import javax.swing.JComponent
class AgentRuntimeOptionsComboBoxAction(
private val agentSession: AgentSession,
private val onAcpConfigChanged: (String, String) -> Unit
) : ComboBoxAction() {
init {
isSmallVariant = true
refreshPresentation(templatePresentation)
}
fun createCustomComponent(place: String): JComponent {
return createCustomComponent(templatePresentation, place)
}
override fun createCustomComponent(
presentation: Presentation,
place: String
): JComponent {
val button = createComboBoxButton(presentation)
val livePresentation = button.presentation
refreshPresentation(livePresentation)
button.isEnabled = hasExternalAgentSelected()
button.foreground = EditorColorsManager.getInstance().globalScheme.defaultForeground
button.border = null
button.putClientProperty("JButton.backgroundColor", Color(0, 0, 0, 0))
button.putClientProperty(
"proxyai.refreshPresentation",
Runnable {
refreshPresentation(livePresentation)
button.isEnabled = hasExternalAgentSelected()
}
)
return button
}
override fun createActionPopup(
group: DefaultActionGroup,
context: DataContext,
disposeCallback: Runnable?
): JBPopup {
return ModelListPopups.createPopup(group, context, disposeCallback)
}
override fun createPopupActionGroup(
button: JComponent,
context: DataContext
): DefaultActionGroup {
val actionGroup = DefaultActionGroup()
val errorMessage = agentSession.externalAgentErrorMessage
when {
!errorMessage.isNullOrBlank() -> {
actionGroup.add(createDisabledInfoAction(errorMessage))
}
agentSession.externalAgentConfigLoading -> {
actionGroup.add(createDisabledInfoAction("Loading...", AnimatedIcon.Default()))
}
selectableOptions.isEmpty() -> {
actionGroup.add(createDisabledInfoAction("No options available"))
}
else -> {
selectableOptions.forEach { option ->
actionGroup.add(createOptionGroup(option))
}
}
}
return actionGroup
}
override fun shouldShowDisabledActions(): Boolean = true
private fun createOptionGroup(option: AcpConfigOption): DefaultActionGroup {
val group = DefaultActionGroup.createPopupGroup { optionLabel(option) }
option.options.forEach { choice ->
val selected = choice.value == option.currentValue
group.add(
object : DumbAwareAction(
choice.name,
choice.description,
if (selected) AllIcons.Actions.Checked else null
) {
override fun update(event: AnActionEvent) {
event.presentation.isEnabled =
!selected && !agentSession.externalAgentConfigLoading
}
override fun actionPerformed(event: AnActionEvent) {
onAcpConfigChanged(option.id, choice.value)
}
override fun getActionUpdateThread(): ActionUpdateThread {
return ActionUpdateThread.BGT
}
}
)
}
return group
}
private fun refreshPresentation(presentation: Presentation) {
presentation.icon = if (agentSession.externalAgentConfigLoading) {
AnimatedIcon.Default()
} else {
null
}
presentation.text = buildSummaryText()
}
private fun hasExternalAgentSelected(): Boolean {
return !agentSession.externalAgentId.isNullOrBlank()
}
private fun buildSummaryText(): String {
if (agentSession.externalAgentConfigLoading) {
return "Loading..."
}
agentSession.externalAgentErrorMessage
?.takeIf(String::isNotBlank)
?.let { return it }
val parts = listOfNotNull(
selectedOptionValue("model"),
selectedOptionValue("thought_level")
).ifEmpty {
listOfNotNull(selectedOptionValue("mode"))
}
return parts.takeIf { it.isNotEmpty() }?.joinToString(" · ") ?: "Options"
}
private fun selectedOptionValue(category: String): String? {
val option = selectableOptions.firstOrNull { it.category == category } ?: return null
return option.options
.firstOrNull { it.value == option.currentValue }
?.name
?: option.currentValue
}
private val selectableOptions: List<AcpConfigOption>
get() = agentSession.externalAgentConfigOptions
.asSequence()
.filter { it.type == "select" && it.options.isNotEmpty() }
.sortedBy { categoryOrder(it.category) }
.toList()
private fun optionLabel(option: AcpConfigOption): String {
return when (option.category.orEmpty()) {
"model" -> "Model"
"mode" -> "Mode"
"thought_level" -> "Reasoning"
else -> option.name
}
}
private fun createDisabledInfoAction(
text: String,
icon: Icon? = null
): DumbAwareAction {
return object : DumbAwareAction(text, "", icon) {
override fun actionPerformed(event: AnActionEvent) = Unit
override fun update(event: AnActionEvent) {
event.presentation.isEnabled = false
}
override fun getActionUpdateThread(): ActionUpdateThread {
return ActionUpdateThread.BGT
}
}
}
private fun categoryOrder(category: String?): Int {
return when (category.orEmpty()) {
"model" -> 0
"mode" -> 1
"thought_level" -> 2
else -> 3
}
}
}

View file

@ -40,6 +40,7 @@ import ee.carlrobert.codegpt.toolwindow.agent.history.AgentHistoryListPanel
import ee.carlrobert.codegpt.toolwindow.ui.ResponseMessagePanel
import ee.carlrobert.codegpt.ui.UIUtil
import ee.carlrobert.codegpt.ui.UIUtil.createTextPane
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import ee.carlrobert.codegpt.util.coroutines.DisposableCoroutineScope
import com.intellij.openapi.Disposable
@ -336,16 +337,19 @@ class AgentToolWindowLandingPanel(private val project: Project) : BorderLayoutPa
if (disposed || project.isDisposed) return
val shouldRefresh = offset == 0 && refreshHistory
backgroundScope.launch {
val page = runCatching {
val page = try {
historyService.listThreadsPage(
query = query,
offset = offset,
limit = limit,
refresh = shouldRefresh
)
} catch (cancelled: CancellationException) {
throw cancelled
} catch (throwable: Throwable) {
logger.warn("Failed to load checkpoint history", throwable)
null
}
.onFailure { logger.warn("Failed to load checkpoint history", it) }
.getOrNull()
if (disposed || project.isDisposed) return@launch
runInEdt {
@ -361,20 +365,26 @@ class AgentToolWindowLandingPanel(private val project: Project) : BorderLayoutPa
private fun openCheckpointThread(thread: AgentHistoryThreadSummary) {
if (disposed || project.isDisposed) return
backgroundScope.launch {
val checkpoint = runCatching {
val checkpoint = try {
historyService.loadCheckpoint(thread.latest)
}.onFailure {
logger.warn("Failed to open checkpoint thread ${thread.agentId}", it)
}.getOrNull() ?: return@launch
} catch (cancelled: CancellationException) {
throw cancelled
} catch (throwable: Throwable) {
logger.warn("Failed to open checkpoint thread ${thread.agentId}", throwable)
null
} ?: return@launch
val conversation = runCatching {
val conversation = try {
AgentCheckpointConversationMapper.toConversation(
checkpoint = checkpoint,
projectInstructions = loadProjectInstructions(project.basePath)
)
}.onFailure {
logger.warn("Failed to open checkpoint thread ${thread.agentId}", it)
}.getOrNull() ?: return@launch
} catch (cancelled: CancellationException) {
throw cancelled
} catch (throwable: Throwable) {
logger.warn("Failed to open checkpoint thread ${thread.agentId}", throwable)
null
} ?: return@launch
if (disposed || project.isDisposed) return@launch
runInEdt {

View file

@ -93,8 +93,16 @@ object ToolCallDescriptorFactory {
val mcpArgs = args as? McpTool.Args
val mcpResult = result as? McpTool.Result
val resolvedToolName = mcpResult?.toolName ?: mcpArgs?.toolName ?: toolName
val server = mcpResult?.serverName ?: mcpResult?.serverId ?: mcpArgs?.serverName ?: mcpArgs?.serverId
val server =
mcpResult?.serverName ?: mcpResult?.serverId ?: mcpArgs?.serverName ?: mcpArgs?.serverId
val titleMain = resolvedToolName
val summary = mcpArgs?.arguments
?.entries
?.take(2)
?.joinToString(" · ") { (key, value) ->
val valueText = value.toString().trim('"')
"$key=${truncateQuery(valueText)}"
}
val actions = if (mcpResult != null) {
listOf(
@ -117,7 +125,8 @@ object ToolCallDescriptorFactory {
result = result,
projectId = projectId,
secondaryBadges = listOfNotNull(serverBadge),
actions = actions
actions = actions,
summary = summary
)
}
@ -133,6 +142,7 @@ object ToolCallDescriptorFactory {
showTextDialog(result.loadedContent, "Skill Content: ${result.name}")
}
)
else -> emptyList()
}
return ToolCallDescriptor(
@ -474,7 +484,8 @@ object ToolCallDescriptorFactory {
private fun buildSearchBadges(result: Any?): List<Badge> {
return if (result is IntelliJSearchTool.Result) {
listOf(Badge(
listOf(
Badge(
"[${result.totalMatches} matches]",
JBColor.BLUE,
action = { showTextDialog(result.output, "Search Results") }
@ -769,10 +780,10 @@ object ToolCallDescriptorFactory {
): ToolCallDescriptor {
return ToolCallDescriptor(
kind = ToolKind.OTHER,
icon = AllIcons.Actions.Help,
titlePrefix = "Tool:",
icon = AllIcons.Actions.Execute,
titlePrefix = "",
titleMain = toolName,
tooltip = "Tool: $toolName",
tooltip = toolName,
args = args,
result = result,
projectId = projectId
@ -817,7 +828,8 @@ object ToolCallDescriptorFactory {
private fun buildDocsBadges(result: Any?): List<Badge> {
return if (result is GetLibraryDocsTool.Result.Success) {
listOf(Badge(
listOf(
Badge(
"[View Results]",
JBColor.BLUE,
action = {
@ -836,7 +848,8 @@ object ToolCallDescriptorFactory {
return when (result) {
is WebSearchTool.Result -> {
val argsObj = args as? WebSearchTool.Args
val badges = mutableListOf(Badge(
val badges = mutableListOf(
Badge(
"[${result.results.size} results]",
JBColor.BLUE,
action = { showWebResultsDialog(result) }

View file

@ -0,0 +1,27 @@
package ee.carlrobert.codegpt.toolwindow.ui
import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.actionSystem.DefaultActionGroup
import com.intellij.openapi.ui.popup.JBPopup
import com.intellij.openapi.ui.popup.JBPopupListener
import com.intellij.openapi.ui.popup.LightweightWindowEvent
object ModelListPopups {
fun createPopup(
group: DefaultActionGroup,
context: DataContext,
disposeCallback: Runnable?
): JBPopup {
val popup = ModelListPopup(group, context)
if (disposeCallback != null) {
popup.addListener(object : JBPopupListener {
override fun onClosed(event: LightweightWindowEvent) {
disposeCallback.run()
}
})
}
popup.isShowSubmenuOnHover = true
return popup
}
}

View file

@ -9,6 +9,7 @@ import com.intellij.openapi.application.ReadAction
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.popup.JBPopup
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiElement
import com.intellij.ui.ScrollPaneFactory
import com.intellij.ui.awt.RelativePoint
@ -18,6 +19,7 @@ import com.intellij.util.ui.JBFont
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.UIUtil
import ee.carlrobert.codegpt.util.NavigationResolverFactory
import ee.carlrobert.codegpt.util.EditorUtil
import java.awt.Dimension
import java.awt.Image
import java.awt.MouseInfo
@ -42,6 +44,8 @@ object PsiLinkHoverPreview {
private const val FILE_PREFIX = "file://"
private const val HOVER_DELAY_MS = 250L
private const val EXIT_CLOSE_DELAY_MS = 120L
private const val FILE_PREVIEW_MAX_LINES = 120
private const val FILE_PREVIEW_MAX_CHARS = 6000
@JvmStatic
fun install(project: Project, textPane: JTextPane) {
@ -202,16 +206,12 @@ object PsiLinkHoverPreview {
val maxW = JBUI.scale(600)
val maxH = JBUI.scale(400)
editor.size = Dimension(maxW, Int.MAX_VALUE)
val pref = editor.preferredSize
val w = min(pref.width.coerceAtLeast(200), maxW)
val h = min(pref.height.coerceAtLeast(50), maxH)
scroll.preferredSize = Dimension(w, h)
scroll.preferredSize = computeHoverSize(editor, maxW, maxH)
val newPopup = PopupFactoryImpl.getInstance()
.createComponentPopupBuilder(scroll, null)
.setRequestFocus(false)
.setResizable(true)
.setResizable(false)
.setMovable(false)
.setShowShadow(true)
.setCancelOnClickOutside(true)
@ -262,6 +262,9 @@ object PsiLinkHoverPreview {
text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
private fun buildPsiHtmlOffEdt(element: Any?): String? {
if (element is PsiFile) {
return buildFilePreviewHtml(element)
}
if (element !is PsiElement) return null
val provider = DocumentationManager.getProviderFromElement(element)
@ -272,5 +275,49 @@ object PsiLinkHoverPreview {
null
}
}
private fun buildFilePreviewHtml(file: PsiFile): String? {
val virtualFile = file.virtualFile ?: return null
val rawContent = EditorUtil.getFileContent(virtualFile)
val excerpt = rawContent.lineSequence()
.take(FILE_PREVIEW_MAX_LINES)
.joinToString("\n")
.take(FILE_PREVIEW_MAX_CHARS)
val truncated = excerpt.length < rawContent.length ||
rawContent.lineSequence().drop(FILE_PREVIEW_MAX_LINES).any()
val renderedContent = buildString {
append(escape(excerpt.ifBlank { "<empty file>" }))
if (truncated) {
append("\n...")
}
}
return """
<html>
<body>
<div style="font-weight:bold;margin-bottom:4px;">${escape(virtualFile.name)}</div>
<div style="color:#808080;margin-bottom:8px;">${escape(virtualFile.path)}</div>
<pre style="white-space:pre-wrap;margin:0;font-family:monospace;">$renderedContent</pre>
</body>
</html>
""".trimIndent()
}
private fun computeHoverSize(
editor: DocumentationHintEditorPane,
maxW: Int,
maxH: Int
): Dimension {
return runCatching {
editor.size = Dimension(maxW, maxH)
val preferredSize = editor.preferredSize
Dimension(
min(preferredSize.width.coerceAtLeast(200), maxW),
min(preferredSize.height.coerceAtLeast(50), maxH)
)
}.getOrElse { error ->
logger.warn("Failed to measure hover preview; using fallback size", error)
Dimension(JBUI.scale(360), JBUI.scale(220))
}
}
}
}

View file

@ -66,44 +66,23 @@ class UserInputPanel @JvmOverloads constructor(
val tagManager: TagManager,
private val onSubmit: (String) -> Unit,
private val onStop: () -> Unit,
withRemovableSelectedEditorTag: Boolean = true,
private val onAcceptAll: (() -> Unit)? = null,
private val onRejectAll: (() -> Unit)? = null,
onApply: (() -> Unit)? = null,
getMarkdownContent: (() -> String)? = null,
withRemovableSelectedEditorTag: Boolean = true,
private val agentTokenCounterPanel: JComponent? = null,
private val agentTokenCounterVisibilityProvider: (() -> Boolean)? = null,
private val sessionIdProvider: (() -> String?)? = null,
private val conversationIdProvider: (() -> UUID?)? = null,
private val onStartSessionTimeline: (() -> Unit)? = null,
private val modelSelectorComponentFactory: ((UserInputPanel) -> JComponent)? = null,
private val secondaryFooterComponentFactory: ((UserInputPanel) -> JComponent)? = null,
private val secondaryFooterComponentVisibilityProvider: (() -> Boolean)? = null,
private val promptEnhancerVisibilityProvider: (() -> Boolean)? = null,
private val sessionTimelineVisibilityProvider: (() -> Boolean)? = null,
) : BorderLayoutPanel() {
constructor(
project: Project,
totalTokensPanel: TotalTokensPanel,
parentDisposable: Disposable,
featureType: FeatureType,
tagManager: TagManager,
onSubmit: (String) -> Unit,
onStop: () -> Unit,
withRemovableSelectedEditorTag: Boolean
) : this(
project,
totalTokensPanel,
parentDisposable,
featureType,
tagManager,
onSubmit,
onStop,
null,
null,
null,
null,
withRemovableSelectedEditorTag,
null,
null,
null
)
companion object {
private const val CORNER_RADIUS = 16
}
@ -145,6 +124,9 @@ class UserInputPanel @JvmOverloads constructor(
)
private var footerPanelRef: JPanel? = null
private var modelSelectorComponentRef: JComponent? = null
private var secondaryFooterComponentRef: JComponent? = null
private var secondaryFooterSeparatorRef: JComponent? = null
private val tokenUsageCounterPanel = agentTokenCounterPanel as? TokenUsageCounterPanel
private val applyChip =
@ -592,17 +574,39 @@ class UserInputPanel @JvmOverloads constructor(
private fun createFooterPanel(featureType: FeatureType): JPanel {
val modelSettings = ModelSettings.getInstance()
val currentService = modelSettings.getServiceForFeature(featureType)
val availableProviders = modelSettings.getAvailableProviders(featureType)
val modelComboBox = ModelComboBoxAction(
{ imageActionSupported.set(isImageActionSupported()) },
currentService,
availableProviders,
true,
featureType
).createCustomComponent(ActionPlaces.UNKNOWN).apply {
val modelComboBox = modelSelectorComponentFactory?.invoke(this) ?: run {
val currentService = modelSettings.getServiceForFeature(featureType)
val availableProviders = modelSettings.getAvailableProviders(featureType)
ModelComboBoxAction(
{ imageActionSupported.set(isImageActionSupported()) },
currentService,
availableProviders,
true,
featureType
).createCustomComponent(ActionPlaces.UNKNOWN)
}.apply {
cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
}
modelSelectorComponentRef = modelComboBox
val secondaryFooterComponent = secondaryFooterComponentFactory?.invoke(this)?.apply {
cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
}
val secondaryFooterSeparator = if (secondaryFooterComponent != null) {
createActionSeparator()
} else {
null
}
secondaryFooterComponentRef = secondaryFooterComponent
secondaryFooterSeparatorRef = secondaryFooterSeparator
val isSecondaryFooterVisible = secondaryFooterComponentVisibilityProvider?.invoke() ?: true
secondaryFooterComponent?.isVisible = isSecondaryFooterVisible
secondaryFooterSeparator?.isVisible = isSecondaryFooterVisible
agentTokenCounterPanel?.isVisible = agentTokenCounterVisibilityProvider?.invoke() ?: true
promptEnhancerButton?.isVisible = promptEnhancerVisibilityProvider?.invoke() ?: true
val isSessionTimelineVisible = sessionTimelineVisibilityProvider?.invoke()
?: (onStartSessionTimeline != null)
sessionTimelineButton?.isVisible = isSessionTimelineVisible
sessionTimelineSeparator?.isVisible = isSessionTimelineVisible
val pnl = panel {
twoColumnsRow(
@ -610,6 +614,12 @@ class UserInputPanel @JvmOverloads constructor(
panel {
row {
cell(modelComboBox).gap(RightGap.SMALL)
if (secondaryFooterComponent != null) {
if (secondaryFooterSeparator != null) {
cell(secondaryFooterSeparator).gap(RightGap.SMALL)
}
cell(secondaryFooterComponent).gap(RightGap.SMALL)
}
if (agentTokenCounterPanel != null) {
cell(agentTokenCounterPanel).gap(RightGap.SMALL)
}
@ -683,6 +693,24 @@ class UserInputPanel @JvmOverloads constructor(
return model?.llmModel?.capabilities?.any { it is LLMCapability.Vision.Image } == true
}
fun refreshModelDependentState() {
imageActionSupported.set(isImageActionSupported())
agentTokenCounterPanel?.isVisible = agentTokenCounterVisibilityProvider?.invoke() ?: true
val isSecondaryFooterVisible = secondaryFooterComponentVisibilityProvider?.invoke() ?: true
secondaryFooterComponentRef?.isVisible = isSecondaryFooterVisible
secondaryFooterSeparatorRef?.isVisible = isSecondaryFooterVisible
promptEnhancerButton?.isVisible = promptEnhancerVisibilityProvider?.invoke() ?: true
val isSessionTimelineVisible = sessionTimelineVisibilityProvider?.invoke()
?: (onStartSessionTimeline != null)
sessionTimelineButton?.isVisible = isSessionTimelineVisible
sessionTimelineSeparator?.isVisible = isSessionTimelineVisible
(modelSelectorComponentRef?.getClientProperty("proxyai.refreshPresentation") as? Runnable)?.run()
(secondaryFooterComponentRef?.getClientProperty("proxyai.refreshPresentation") as? Runnable)?.run()
footerPanelRef?.revalidate()
footerPanelRef?.repaint()
updatePreferredSizeFromChildren()
}
private fun updatePreferredSizeFromChildren() {
val headerHeight = userInputHeaderPanel.preferredSize?.height ?: 0
val textFieldHeight = promptTextField.preferredSize?.height ?: 0

View file

@ -0,0 +1,67 @@
package ee.carlrobert.codegpt.util
import com.intellij.execution.configurations.PathEnvironmentVariableUtil
import com.intellij.util.EnvironmentUtil
import java.io.File
object CommandRuntimeHelper {
fun resolveCommand(
command: String,
extraEnvironment: Map<String, String> = emptyMap()
): String? {
val commandFile = File(command)
if (commandFile.isAbsolute && commandFile.exists() && commandFile.canExecute()) {
return commandFile.absolutePath
}
val environment = createEnvironment(
extraEnvironment = extraEnvironment,
resolvedCommand = null,
includeResolvedCommandParent = false
)
val pathValue = environment["PATH"]
return if (pathValue.isNullOrBlank()) {
PathEnvironmentVariableUtil.findInPath(command)?.absolutePath
} else {
PathEnvironmentVariableUtil.findInPath(command, pathValue, null)?.absolutePath
?: PathEnvironmentVariableUtil.findInPath(command)?.absolutePath
}
}
fun createEnvironment(
extraEnvironment: Map<String, String>,
resolvedCommand: String? = null,
includeResolvedCommandParent: Boolean = true
): MutableMap<String, String> {
val parentEnvironment = EnvironmentUtil.getEnvironmentMap().ifEmpty { System.getenv() }
val environment = parentEnvironment.toMutableMap()
environment.putAll(extraEnvironment)
EnvironmentUtil.inlineParentOccurrences(environment, parentEnvironment)
if (includeResolvedCommandParent) {
resolvedCommand
?.let(::File)
?.parentFile
?.takeIf { it.isDirectory }
?.absolutePath
?.let { appendPathEntry(environment, it) }
}
return environment
}
private fun appendPathEntry(
environment: MutableMap<String, String>,
pathEntry: String
) {
val pathEntries = linkedSetOf<String>()
val currentPath = environment["PATH"]
if (!currentPath.isNullOrBlank()) {
PathEnvironmentVariableUtil.getPathDirs(currentPath).forEach(pathEntries::add)
}
pathEntries.add(pathEntry)
environment["PATH"] = pathEntries.joinToString(File.pathSeparator)
}
}

View file

@ -1,313 +1,366 @@
<idea-plugin>
<id>ee.carlrobert.chatgpt</id>
<name>Proxy AI</name>
<vendor email="carlrobertoh@gmail.com" url="https://carlrobert.ee">Carl-Robert Linnupuu</vendor>
<depends>com.intellij.modules.platform</depends>
<depends>com.intellij.modules.lang</depends>
<depends optional="true" config-file="plugin-kotlin.xml">org.jetbrains.kotlin</depends>
<depends optional="true" config-file="plugin-java.xml">com.intellij.modules.java</depends>
<depends optional="true" config-file="plugin-javascript.xml">JavaScript</depends>
<depends optional="true" config-file="plugin-cpp.xml">com.intellij.modules.cpp</depends>
<!-- <depends optional="true" config-file="plugin-go.xml">org.jetbrains.plugins.go</depends>-->
<!-- <depends optional="true" config-file="plugin-ruby.xml">com.intellij.modules.ruby</depends>-->
<!-- <depends optional="true" config-file="plugin-php.xml">com.jetbrains.php</depends>-->
<!-- <depends optional="true" config-file="plugin-swift.xml">com.intellij.swift</depends>-->
<depends optional="true" config-file="plugin-git.xml">Git4Idea</depends>
<id>ee.carlrobert.chatgpt</id>
<name>Proxy AI</name>
<vendor email="carlrobertoh@gmail.com" url="https://carlrobert.ee">Carl-Robert Linnupuu</vendor>
<depends>com.intellij.modules.platform</depends>
<depends>com.intellij.modules.lang</depends>
<depends optional="true" config-file="plugin-kotlin.xml">org.jetbrains.kotlin</depends>
<depends optional="true" config-file="plugin-java.xml">com.intellij.modules.java</depends>
<depends optional="true" config-file="plugin-javascript.xml">JavaScript</depends>
<depends optional="true" config-file="plugin-cpp.xml">com.intellij.modules.cpp</depends>
<!-- <depends optional="true" config-file="plugin-go.xml">org.jetbrains.plugins.go</depends>-->
<!-- <depends optional="true" config-file="plugin-ruby.xml">com.intellij.modules.ruby</depends>-->
<!-- <depends optional="true" config-file="plugin-php.xml">com.jetbrains.php</depends>-->
<!-- <depends optional="true" config-file="plugin-swift.xml">com.intellij.swift</depends>-->
<depends optional="true" config-file="plugin-git.xml">Git4Idea</depends>
<projectListeners>
<listener topic="com.intellij.codeInsight.lookup.LookupManagerListener"
class="ee.carlrobert.codegpt.refactorings.RenameCompletionLookupListener"/>
<listener topic="com.intellij.codeInsight.lookup.LookupManagerListener"
class="ee.carlrobert.codegpt.CodeGPTLookupListener"/>
<listener topic="com.intellij.openapi.wm.ex.ToolWindowManagerListener"
class="ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowListener"/>
</projectListeners>
<projectListeners>
<listener topic="com.intellij.codeInsight.lookup.LookupManagerListener"
class="ee.carlrobert.codegpt.refactorings.RenameCompletionLookupListener"/>
<listener topic="com.intellij.codeInsight.lookup.LookupManagerListener"
class="ee.carlrobert.codegpt.CodeGPTLookupListener"/>
<listener topic="com.intellij.openapi.wm.ex.ToolWindowManagerListener"
class="ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowListener"/>
</projectListeners>
<extensions defaultExtensionNs="com.intellij">
<postStartupActivity implementation="ee.carlrobert.codegpt.LegacyMigrationActivity"/>
<postStartupActivity implementation="ee.carlrobert.codegpt.CodeGPTProjectActivity"/>
<postStartupActivity implementation="ee.carlrobert.codegpt.CodeGPTUpdateActivity"/>
<editorFactoryListener implementation="ee.carlrobert.codegpt.CodeGPTEditorFactoryListener"/>
<applicationConfigurable id="settings.codegpt" parentId="tools" displayName="ProxyAI"
instance="ee.carlrobert.codegpt.settings.GeneralSettingsConfigurable"/>
<extensions defaultExtensionNs="com.intellij">
<postStartupActivity implementation="ee.carlrobert.codegpt.LegacyMigrationActivity"/>
<postStartupActivity implementation="ee.carlrobert.codegpt.CodeGPTProjectActivity"/>
<postStartupActivity implementation="ee.carlrobert.codegpt.CodeGPTUpdateActivity"/>
<editorFactoryListener implementation="ee.carlrobert.codegpt.CodeGPTEditorFactoryListener"/>
<applicationConfigurable id="settings.codegpt" parentId="tools" displayName="ProxyAI"
instance="ee.carlrobert.codegpt.settings.GeneralSettingsConfigurable"/>
<projectConfigurable id="settings.codegpt.agents" parentId="settings.codegpt" displayName="Agent"
instance="ee.carlrobert.codegpt.settings.agents.AgentConfigurable" groupWeight="35"/>
<projectConfigurable id="settings.codegpt.subagents" parentId="settings.codegpt.agents" displayName="Subagents"
instance="ee.carlrobert.codegpt.settings.agents.SubagentsConfigurable"/>
<projectConfigurable id="settings.codegpt.skills" parentId="settings.codegpt.agents" displayName="Skills"
instance="ee.carlrobert.codegpt.settings.skills.SkillsConfigurable"/>
<projectConfigurable id="settings.codegpt.hooks" parentId="settings.codegpt.agents" displayName="Hooks"
instance="ee.carlrobert.codegpt.settings.hooks.HooksConfigurable"/>
<projectConfigurable id="settings.codegpt.agents" parentId="settings.codegpt"
displayName="Agent"
instance="ee.carlrobert.codegpt.settings.agents.AgentConfigurable" groupWeight="35"/>
<projectConfigurable id="settings.codegpt.agents.acp" parentId="settings.codegpt.agents"
displayName="ACP"
instance="ee.carlrobert.codegpt.settings.agents.acp.AcpAgentConfigurable"/>
<projectConfigurable id="settings.codegpt.subagents" parentId="settings.codegpt.agents"
displayName="Subagents"
instance="ee.carlrobert.codegpt.settings.agents.SubagentsConfigurable"/>
<projectConfigurable id="settings.codegpt.skills" parentId="settings.codegpt.agents"
displayName="Skills"
instance="ee.carlrobert.codegpt.settings.skills.SkillsConfigurable"/>
<projectConfigurable id="settings.codegpt.hooks" parentId="settings.codegpt.agents"
displayName="Hooks"
instance="ee.carlrobert.codegpt.settings.hooks.HooksConfigurable"/>
<applicationConfigurable id="settings.codegpt.services" parentId="settings.codegpt" displayName="Providers"
instance="ee.carlrobert.codegpt.settings.service.ServiceConfigurable" groupWeight="30"/>
<applicationConfigurable id="settings.codegpt.services.codegpt" parentId="settings.codegpt.services" displayName="ProxyAI"
instance="ee.carlrobert.codegpt.settings.service.codegpt.CodeGPTServiceConfigurable" groupWeight="40"/>
<applicationConfigurable id="settings.codegpt.services.anthropic" parentId="settings.codegpt.services" displayName="Anthropic"
instance="ee.carlrobert.codegpt.settings.service.AnthropicServiceConfigurable" groupWeight="35"/>
<applicationConfigurable id="settings.codegpt.services.openai" parentId="settings.codegpt.services" displayName="OpenAI"
instance="ee.carlrobert.codegpt.settings.service.OpenAIServiceConfigurable" groupWeight="30"/>
<applicationConfigurable id="settings.codegpt.services.custom" parentId="settings.codegpt.services" displayName="Custom OpenAI"
instance="ee.carlrobert.codegpt.settings.service.custom.CustomServiceConfigurable" groupWeight="25"/>
<applicationConfigurable id="settings.codegpt.services.google" parentId="settings.codegpt.services" displayName="Google"
instance="ee.carlrobert.codegpt.settings.service.google.GoogleSettingsConfigurable" groupWeight="20"/>
<applicationConfigurable id="settings.codegpt.services.mistral" parentId="settings.codegpt.services" displayName="Mistral"
instance="ee.carlrobert.codegpt.settings.service.MistralServiceConfigurable" groupWeight="15"/>
<applicationConfigurable id="settings.codegpt.services.inception" parentId="settings.codegpt.services" displayName="Inception"
instance="ee.carlrobert.codegpt.settings.service.InceptionServiceConfigurable" groupWeight="10"/>
<applicationConfigurable id="settings.codegpt.services.ollama" parentId="settings.codegpt.services" displayName="Ollama (Offline)"
instance="ee.carlrobert.codegpt.settings.service.ollama.OllamaSettingsConfigurable" groupWeight="5"/>
<applicationConfigurable id="settings.codegpt.services.llama_cpp" parentId="settings.codegpt.services" displayName="LLaMA C/C++ (Offline)"
instance="ee.carlrobert.codegpt.settings.service.LlamaServiceConfigurable" groupWeight="0"/>
<applicationConfigurable id="settings.codegpt.services" parentId="settings.codegpt"
displayName="Providers"
instance="ee.carlrobert.codegpt.settings.service.ServiceConfigurable" groupWeight="30"/>
<applicationConfigurable id="settings.codegpt.services.codegpt"
parentId="settings.codegpt.services" displayName="ProxyAI"
instance="ee.carlrobert.codegpt.settings.service.codegpt.CodeGPTServiceConfigurable"
groupWeight="40"/>
<applicationConfigurable id="settings.codegpt.services.anthropic"
parentId="settings.codegpt.services" displayName="Anthropic"
instance="ee.carlrobert.codegpt.settings.service.AnthropicServiceConfigurable"
groupWeight="35"/>
<applicationConfigurable id="settings.codegpt.services.openai"
parentId="settings.codegpt.services" displayName="OpenAI"
instance="ee.carlrobert.codegpt.settings.service.OpenAIServiceConfigurable" groupWeight="30"/>
<applicationConfigurable id="settings.codegpt.services.custom"
parentId="settings.codegpt.services" displayName="Custom OpenAI"
instance="ee.carlrobert.codegpt.settings.service.custom.CustomServiceConfigurable"
groupWeight="25"/>
<applicationConfigurable id="settings.codegpt.services.google"
parentId="settings.codegpt.services" displayName="Google"
instance="ee.carlrobert.codegpt.settings.service.google.GoogleSettingsConfigurable"
groupWeight="20"/>
<applicationConfigurable id="settings.codegpt.services.mistral"
parentId="settings.codegpt.services" displayName="Mistral"
instance="ee.carlrobert.codegpt.settings.service.MistralServiceConfigurable"
groupWeight="15"/>
<applicationConfigurable id="settings.codegpt.services.inception"
parentId="settings.codegpt.services" displayName="Inception"
instance="ee.carlrobert.codegpt.settings.service.InceptionServiceConfigurable"
groupWeight="10"/>
<applicationConfigurable id="settings.codegpt.services.ollama"
parentId="settings.codegpt.services" displayName="Ollama (Offline)"
instance="ee.carlrobert.codegpt.settings.service.ollama.OllamaSettingsConfigurable"
groupWeight="5"/>
<applicationConfigurable id="settings.codegpt.services.llama_cpp"
parentId="settings.codegpt.services" displayName="LLaMA C/C++ (Offline)"
instance="ee.carlrobert.codegpt.settings.service.LlamaServiceConfigurable" groupWeight="0"/>
<applicationConfigurable id="settings.codegpt.mcp" parentId="settings.codegpt" displayName="MCP"
instance="ee.carlrobert.codegpt.settings.mcp.McpConfigurable" groupWeight="25"/>
<applicationConfigurable id="settings.codegpt.models" parentId="settings.codegpt" bundle="messages.codegpt" key="settings.models.displayName"
instance="ee.carlrobert.codegpt.settings.models.ModelSettingsConfigurable" groupWeight="20"/>
<applicationConfigurable id="settings.codegpt.prompts" parentId="settings.codegpt" displayName="Prompts"
instance="ee.carlrobert.codegpt.settings.prompts.PromptsConfigurable" groupWeight="15"/>
<applicationConfigurable id="settings.codegpt.configuration" parentId="settings.codegpt" displayName="Configuration"
instance="ee.carlrobert.codegpt.settings.configuration.ConfigurationConfigurable" groupWeight="10"/>
<applicationConfigurable id="settings.codegpt.advanced" parentId="settings.codegpt" displayName="Advanced Settings"
instance="ee.carlrobert.codegpt.settings.advanced.AdvancedSettingsConfigurable" groupWeight="5"/>
<applicationConfigurable id="tools.preferences.codegpt.telemetry" parentId="settings.codegpt" displayName="Telemetry"
instance="ee.carlrobert.codegpt.telemetry.ui.preferences.TelemetryConfigurable" groupWeight="0"/>
<applicationConfigurable id="settings.codegpt.mcp" parentId="settings.codegpt" displayName="MCP"
instance="ee.carlrobert.codegpt.settings.mcp.McpConfigurable" groupWeight="25"/>
<applicationConfigurable id="settings.codegpt.models" parentId="settings.codegpt"
bundle="messages.codegpt" key="settings.models.displayName"
instance="ee.carlrobert.codegpt.settings.models.ModelSettingsConfigurable" groupWeight="20"/>
<applicationConfigurable id="settings.codegpt.prompts" parentId="settings.codegpt"
displayName="Prompts"
instance="ee.carlrobert.codegpt.settings.prompts.PromptsConfigurable" groupWeight="15"/>
<applicationConfigurable id="settings.codegpt.configuration" parentId="settings.codegpt"
displayName="Configuration"
instance="ee.carlrobert.codegpt.settings.configuration.ConfigurationConfigurable"
groupWeight="10"/>
<applicationConfigurable id="settings.codegpt.advanced" parentId="settings.codegpt"
displayName="Advanced Settings"
instance="ee.carlrobert.codegpt.settings.advanced.AdvancedSettingsConfigurable"
groupWeight="5"/>
<applicationConfigurable id="tools.preferences.codegpt.telemetry" parentId="settings.codegpt"
displayName="Telemetry"
instance="ee.carlrobert.codegpt.telemetry.ui.preferences.TelemetryConfigurable"
groupWeight="0"/>
<actionPromoter implementation="ee.carlrobert.codegpt.actions.InlayActionPromoter"/>
<applicationService
serviceImplementation="ee.carlrobert.codegpt.telemetry.core.service.TelemetryServiceFactory"/>
<applicationService serviceImplementation="ee.carlrobert.codegpt.settings.GeneralSettings"/>
<applicationService serviceImplementation="ee.carlrobert.codegpt.settings.service.anthropic.AnthropicSettings"/>
<applicationService serviceImplementation="ee.carlrobert.codegpt.settings.service.openai.OpenAISettings"/>
<applicationService serviceImplementation="ee.carlrobert.codegpt.settings.service.mistral.MistralSettings"/>
<applicationService serviceImplementation="ee.carlrobert.codegpt.settings.service.llama.LlamaSettings"/>
<applicationService serviceImplementation="ee.carlrobert.codegpt.settings.service.ollama.OllamaSettings"/>
<applicationService serviceImplementation="ee.carlrobert.codegpt.settings.service.inception.InceptionSettings"/>
<applicationService serviceImplementation="ee.carlrobert.codegpt.settings.models.ModelSettings"/>
<applicationService serviceImplementation="ee.carlrobert.codegpt.settings.IncludedFilesSettings"/>
<applicationService serviceImplementation="ee.carlrobert.codegpt.settings.advanced.AdvancedSettings"/>
<applicationService serviceImplementation="ee.carlrobert.codegpt.conversations.ConversationsState"/>
<applicationService serviceImplementation="ee.carlrobert.codegpt.codecompletions.psi.CompletionContextService"/>
<applicationService serviceImplementation="ee.carlrobert.codegpt.settings.mcp.McpSettings"/>
<applicationService serviceImplementation="ee.carlrobert.codegpt.services.llama.ServerLogsManager"/>
<projectService serviceImplementation="ee.carlrobert.codegpt.services.ExecutableRunnerService"/>
<inline.completion.provider
id="CodeGPTInlineCompletionProvider"
implementation="ee.carlrobert.codegpt.codecompletions.DebouncedCodeCompletionProvider"/>
<toolWindow id="ProxyAI" icon="ee.carlrobert.codegpt.Icons.DefaultSmall" anchor="right"
factoryClass="ee.carlrobert.codegpt.toolwindow.ProxyAIToolWindowFactory"/>
<notificationGroup id="proxyai.notification.group" displayType="BALLOON" key="notification.group.name"/>
<notificationGroup id="proxyai.notification.sticky.group" displayType="STICKY_BALLOON" key="notification.group.sticky.name"/>
<notificationGroup id="ProxyAI.MCP.Notifications" displayType="BALLOON" key="notification.group.mcp.name"/>
<statusBarWidgetFactory order="first" id="ee.carlrobert.codegpt.statusbar.widget"
implementation="ee.carlrobert.codegpt.statusbar.CodeGPTStatusBarWidgetFactory"/>
<nameSuggestionProvider implementation="ee.carlrobert.codegpt.refactorings.DefaultNameSuggestionProvider"/>
</extensions>
<actionPromoter implementation="ee.carlrobert.codegpt.actions.InlayActionPromoter"/>
<applicationService
serviceImplementation="ee.carlrobert.codegpt.telemetry.core.service.TelemetryServiceFactory"/>
<applicationService serviceImplementation="ee.carlrobert.codegpt.settings.GeneralSettings"/>
<applicationService
serviceImplementation="ee.carlrobert.codegpt.settings.service.anthropic.AnthropicSettings"/>
<applicationService
serviceImplementation="ee.carlrobert.codegpt.settings.service.openai.OpenAISettings"/>
<applicationService
serviceImplementation="ee.carlrobert.codegpt.settings.service.mistral.MistralSettings"/>
<applicationService
serviceImplementation="ee.carlrobert.codegpt.settings.service.llama.LlamaSettings"/>
<applicationService
serviceImplementation="ee.carlrobert.codegpt.settings.service.ollama.OllamaSettings"/>
<applicationService
serviceImplementation="ee.carlrobert.codegpt.settings.service.inception.InceptionSettings"/>
<applicationService
serviceImplementation="ee.carlrobert.codegpt.settings.models.ModelSettings"/>
<applicationService
serviceImplementation="ee.carlrobert.codegpt.settings.IncludedFilesSettings"/>
<applicationService
serviceImplementation="ee.carlrobert.codegpt.settings.advanced.AdvancedSettings"/>
<applicationService
serviceImplementation="ee.carlrobert.codegpt.conversations.ConversationsState"/>
<applicationService
serviceImplementation="ee.carlrobert.codegpt.codecompletions.psi.CompletionContextService"/>
<applicationService serviceImplementation="ee.carlrobert.codegpt.settings.mcp.McpSettings"/>
<applicationService
serviceImplementation="ee.carlrobert.codegpt.services.llama.ServerLogsManager"/>
<projectService serviceImplementation="ee.carlrobert.codegpt.services.ExecutableRunnerService"/>
<inline.completion.provider
id="CodeGPTInlineCompletionProvider"
implementation="ee.carlrobert.codegpt.codecompletions.DebouncedCodeCompletionProvider"/>
<toolWindow id="ProxyAI" icon="ee.carlrobert.codegpt.Icons.DefaultSmall" anchor="right"
factoryClass="ee.carlrobert.codegpt.toolwindow.ProxyAIToolWindowFactory"/>
<notificationGroup id="proxyai.notification.group" displayType="BALLOON"
key="notification.group.name"/>
<notificationGroup id="proxyai.notification.sticky.group" displayType="STICKY_BALLOON"
key="notification.group.sticky.name"/>
<notificationGroup id="ProxyAI.MCP.Notifications" displayType="BALLOON"
key="notification.group.mcp.name"/>
<statusBarWidgetFactory order="first" id="ee.carlrobert.codegpt.statusbar.widget"
implementation="ee.carlrobert.codegpt.statusbar.CodeGPTStatusBarWidgetFactory"/>
<nameSuggestionProvider
implementation="ee.carlrobert.codegpt.refactorings.DefaultNameSuggestionProvider"/>
</extensions>
<resource-bundle>messages.codegpt</resource-bundle>
<resource-bundle>messages.codegpt</resource-bundle>
<actions>
<actions>
<action
id="codegpt.acceptNextEdit"
text="Accept Next-Edit"
class="ee.carlrobert.codegpt.nextedit.AcceptNextEditAction">
<keyboard-shortcut first-keystroke="TAB" keymap="$default"/>
</action>
<action
id="codegpt.triggerCompletion"
text="Trigger Completion Manually"
class="ee.carlrobert.codegpt.nextedit.TriggerNextEditAction">
<keyboard-shortcut first-keystroke="ctrl ENTER" keymap="$default"/>
</action>
<action
id="codegpt.acceptInlineEdit"
text="Accept Inline Edit"
class="ee.carlrobert.codegpt.actions.editor.AcceptInlineEditAction">
<keyboard-shortcut first-keystroke="ctrl ENTER" keymap="$default"/>
</action>
<group id="CodeGPTEditorPopup">
<group id="action.editor.group.EditorActionGroup"
text="ProxyAI"
class="com.intellij.openapi.actionSystem.DefaultActionGroup"
popup="true"
icon="ee.carlrobert.codegpt.Icons.DefaultSmall">
<action
id="codegpt.acceptNextEdit"
text="Accept Next-Edit"
class="ee.carlrobert.codegpt.nextedit.AcceptNextEditAction">
<keyboard-shortcut first-keystroke="TAB" keymap="$default"/>
id="CodeGPT.NewChat"
class="ee.carlrobert.codegpt.actions.editor.OpenNewChatAction"
text="New Chat"
description="Creates a new chat session">
<keyboard-shortcut keymap="$default" first-keystroke="ctrl alt shift N"
replace-all="true"/>
</action>
<action
id="codegpt.triggerCompletion"
text="Trigger Completion Manually"
class="ee.carlrobert.codegpt.nextedit.TriggerNextEditAction">
<keyboard-shortcut first-keystroke="ctrl ENTER" keymap="$default"/>
id="CodeGPT.ContextMenuInlineEditAction"
text="Inline Edit"
description="Edit code inline from editor's context menu"
class="ee.carlrobert.codegpt.actions.editor.InlineEditContextMenuAction">
<keyboard-shortcut keymap="$default" first-keystroke="ctrl shift K" replace-all="true"/>
</action>
<action
id="codegpt.acceptInlineEdit"
text="Accept Inline Edit"
class="ee.carlrobert.codegpt.actions.editor.AcceptInlineEditAction">
<keyboard-shortcut first-keystroke="ctrl ENTER" keymap="$default"/>
id="CodeGPT.AskQuestion"
text="Ask Question"
class="ee.carlrobert.codegpt.actions.editor.AskQuestionAction">
<keyboard-shortcut keymap="$default" first-keystroke="ctrl alt shift Q"
replace-all="true"/>
</action>
<group id="CodeGPTEditorPopup">
<group id="action.editor.group.EditorActionGroup"
text="ProxyAI"
class="com.intellij.openapi.actionSystem.DefaultActionGroup"
popup="true"
icon="ee.carlrobert.codegpt.Icons.DefaultSmall">
<action
id="CodeGPT.NewChat"
class="ee.carlrobert.codegpt.actions.editor.OpenNewChatAction"
text="New Chat"
description="Creates a new chat session">
<keyboard-shortcut keymap="$default" first-keystroke="ctrl alt shift N" replace-all="true"/>
</action>
<action
id="CodeGPT.ContextMenuInlineEditAction"
text="Inline Edit"
description="Edit code inline from editor's context menu"
class="ee.carlrobert.codegpt.actions.editor.InlineEditContextMenuAction">
<keyboard-shortcut keymap="$default" first-keystroke="ctrl shift K" replace-all="true"/>
</action>
<action
id="CodeGPT.AskQuestion"
text="Ask Question"
class="ee.carlrobert.codegpt.actions.editor.AskQuestionAction">
<keyboard-shortcut keymap="$default" first-keystroke="ctrl alt shift Q" replace-all="true"/>
</action>
<separator/>
<group id="CodeGPT.MyEditorActionsGroup"
text="My Actions"
class="com.intellij.openapi.actionSystem.DefaultActionGroup">
</group>
</group>
<add-to-group group-id="EditorPopupMenu1" anchor="first"/>
<separator/>
<separator/>
<group id="CodeGPT.MyEditorActionsGroup"
text="My Actions"
class="com.intellij.openapi.actionSystem.DefaultActionGroup">
</group>
<action id="CodeGPT.TriggerEditorPopup"
class="ee.carlrobert.codegpt.actions.editor.ShowEditorActionGroupAction"
text="Show ProxyAI Actions">
<keyboard-shortcut first-keystroke="ctrl shift alt m" keymap="$default"/>
</action>
<group id="CodeGPT.ProjectViewPopupMenuRootGroup">
<group id="CodeGPT.ProjectViewPopupMenuGroup"
text="ProxyAI"
class="com.intellij.openapi.actionSystem.DefaultActionGroup"
popup="true"
icon="ee.carlrobert.codegpt.Icons.DefaultSmall">
<action
id="CodeGPT.IncludeFilesInContextAction"
text="Include Files In Prompt"
class="ee.carlrobert.codegpt.actions.IncludeFilesInContextAction"/>
</group>
<add-to-group
group-id="ProjectViewPopupMenu"
relative-to-action="ProjectViewPopupMenuRefactoringGroup"
anchor="before"/>
<separator/>
</group>
<group id="CodeGPT.FloatingCodeToolbarMenuRootGroup">
<action
id="CodeGPT.FloatingMenuInlineEditAction"
text="Inline Edit"
description="Edit code inline from editor's floating menu"
class="ee.carlrobert.codegpt.actions.editor.InlineEditFloatingMenuAction">
<keyboard-shortcut keymap="$default" first-keystroke="ctrl shift K" replace-all="true"/>
</action>
<add-to-group
group-id="Floating.CodeToolbar"
relative-to-action="ProjectViewPopupMenuRefactoringGroup"
anchor="before"/>
<separator/>
</group>
</group>
<add-to-group group-id="EditorPopupMenu1" anchor="first"/>
<separator/>
</group>
<action id="CodeGPT.TriggerEditorPopup"
class="ee.carlrobert.codegpt.actions.editor.ShowEditorActionGroupAction"
text="Show ProxyAI Actions">
<keyboard-shortcut first-keystroke="ctrl shift alt m" keymap="$default"/>
</action>
<group id="CodeGPT.ProjectViewPopupMenuRootGroup">
<group id="CodeGPT.ProjectViewPopupMenuGroup"
text="ProxyAI"
class="com.intellij.openapi.actionSystem.DefaultActionGroup"
popup="true"
icon="ee.carlrobert.codegpt.Icons.DefaultSmall">
<action
id="CodeGPT.AddSelectionToContext"
class="ee.carlrobert.codegpt.actions.editor.AddSelectionToContextAction"
text="Include Selection in Prompt"
description="Adds the selected text to the ProxyAI context">
<keyboard-shortcut keymap="$default" first-keystroke="ctrl shift I" replace-all="true"/>
id="CodeGPT.IncludeFilesInContextAction"
text="Include Files In Prompt"
class="ee.carlrobert.codegpt.actions.IncludeFilesInContextAction"/>
</group>
<add-to-group
group-id="ProjectViewPopupMenu"
relative-to-action="ProjectViewPopupMenuRefactoringGroup"
anchor="before"/>
<separator/>
</group>
<add-to-group group-id="action.editor.group.EditorActionGroup" anchor="after" relative-to-action="CodeGPT.NewChat"/>
<add-to-group group-id="CodeGPT.FloatingCodeToolbarMenuRootGroup" anchor="after" relative-to-action="CodeGPT.FloatingMenuInlineEditAction"/>
</action>
<group id="CodeGPT.FloatingCodeToolbarMenuRootGroup">
<action
id="CodeGPT.FloatingMenuInlineEditAction"
text="Inline Edit"
description="Edit code inline from editor's floating menu"
class="ee.carlrobert.codegpt.actions.editor.InlineEditFloatingMenuAction">
<keyboard-shortcut keymap="$default" first-keystroke="ctrl shift K" replace-all="true"/>
</action>
<add-to-group
group-id="Floating.CodeToolbar"
relative-to-action="ProjectViewPopupMenuRefactoringGroup"
anchor="before"/>
<separator/>
</group>
<!-- Inline Edit Keyboard Shortcuts -->
<action
id="CodeGPT.AcceptAllInlineEdit"
class="ee.carlrobert.codegpt.inlineedit.AcceptAllInlineEditAction"
text="Accept All Inline Edit Changes"
description="Accept all changes in the Inline Edit diff view">
<keyboard-shortcut keymap="$default" first-keystroke="ctrl shift ENTER" replace-all="true"/>
<add-to-group group-id="EditorActions" anchor="last"/>
</action>
<action
id="CodeGPT.AddSelectionToContext"
class="ee.carlrobert.codegpt.actions.editor.AddSelectionToContextAction"
text="Include Selection in Prompt"
description="Adds the selected text to the ProxyAI context">
<keyboard-shortcut keymap="$default" first-keystroke="ctrl shift I" replace-all="true"/>
<action
id="CodeGPT.RejectAllInlineEdit"
class="ee.carlrobert.codegpt.inlineedit.RejectAllInlineEditAction"
text="Reject All Inline Edit Changes"
description="Reject all changes in the Inline Edit diff view">
<keyboard-shortcut keymap="$default" first-keystroke="ctrl shift BACK_SPACE" replace-all="true"/>
<add-to-group group-id="EditorActions" anchor="last"/>
</action>
<add-to-group group-id="action.editor.group.EditorActionGroup" anchor="after"
relative-to-action="CodeGPT.NewChat"/>
<add-to-group group-id="CodeGPT.FloatingCodeToolbarMenuRootGroup" anchor="after"
relative-to-action="CodeGPT.FloatingMenuInlineEditAction"/>
</action>
<action
id="CodeGPT.RejectInlineEdit"
class="ee.carlrobert.codegpt.inlineedit.RejectInlineEditAction"
text="Reject Inline Edit Changes"
description="Reject changes in the Inline Edit diff view">
<keyboard-shortcut keymap="$default" first-keystroke="ESCAPE" replace-all="true"/>
<add-to-group group-id="EditorActions" anchor="last"/>
</action>
<!-- Inline Edit Keyboard Shortcuts -->
<action
id="CodeGPT.AcceptAllInlineEdit"
class="ee.carlrobert.codegpt.inlineedit.AcceptAllInlineEditAction"
text="Accept All Inline Edit Changes"
description="Accept all changes in the Inline Edit diff view">
<keyboard-shortcut keymap="$default" first-keystroke="ctrl shift ENTER" replace-all="true"/>
<add-to-group group-id="EditorActions" anchor="last"/>
</action>
<action
id="CodeGPT.AcceptCurrentInlineEdit"
class="ee.carlrobert.codegpt.inlineedit.AcceptCurrentInlineEditAction"
text="Accept Current Inline Edit Change"
description="Accept the current or next pending inline edit change">
<keyboard-shortcut keymap="$default" first-keystroke="ctrl ENTER" replace-all="true"/>
<add-to-group group-id="EditorActions" anchor="last"/>
</action>
<action
id="CodeGPT.RejectAllInlineEdit"
class="ee.carlrobert.codegpt.inlineedit.RejectAllInlineEditAction"
text="Reject All Inline Edit Changes"
description="Reject all changes in the Inline Edit diff view">
<keyboard-shortcut keymap="$default" first-keystroke="ctrl shift BACK_SPACE"
replace-all="true"/>
<add-to-group group-id="EditorActions" anchor="last"/>
</action>
<action
id="CodeGPT.RejectCurrentInlineEdit"
class="ee.carlrobert.codegpt.inlineedit.RejectCurrentInlineEditAction"
text="Reject Current Inline Edit Change"
description="Reject the current or next pending inline edit change">
<keyboard-shortcut keymap="$default" first-keystroke="ctrl BACK_SPACE" replace-all="true"/>
<add-to-group group-id="EditorActions" anchor="last"/>
</action>
<action
id="CodeGPT.RejectInlineEdit"
class="ee.carlrobert.codegpt.inlineedit.RejectInlineEditAction"
text="Reject Inline Edit Changes"
description="Reject changes in the Inline Edit diff view">
<keyboard-shortcut keymap="$default" first-keystroke="ESCAPE" replace-all="true"/>
<add-to-group group-id="EditorActions" anchor="last"/>
</action>
<action
id="codegpt.openSettings"
text="Open Settings"
class="ee.carlrobert.codegpt.actions.OpenSettingsAction">
<override-text place="MainMenu" text="Open Settings"/>
<override-text place="popup" use-text-of-place="MainMenu"/>
</action>
<action
id="statusbar.enableCompletions"
class="ee.carlrobert.codegpt.actions.EnableCompletionsAction">
<keyboard-shortcut first-keystroke="ctrl shift alt c" keymap="$default"/>
<action
id="CodeGPT.AcceptCurrentInlineEdit"
class="ee.carlrobert.codegpt.inlineedit.AcceptCurrentInlineEditAction"
text="Accept Current Inline Edit Change"
description="Accept the current or next pending inline edit change">
<keyboard-shortcut keymap="$default" first-keystroke="ctrl ENTER" replace-all="true"/>
<add-to-group group-id="EditorActions" anchor="last"/>
</action>
<override-text place="MainMenu"/>
<override-text place="popup" use-text-of-place="MainMenu"/>
</action>
<action
id="statusbar.disableCompletions"
class="ee.carlrobert.codegpt.actions.DisableCompletionsAction">
<keyboard-shortcut first-keystroke="ctrl shift alt c" keymap="$default"/>
<action
id="CodeGPT.RejectCurrentInlineEdit"
class="ee.carlrobert.codegpt.inlineedit.RejectCurrentInlineEditAction"
text="Reject Current Inline Edit Change"
description="Reject the current or next pending inline edit change">
<keyboard-shortcut keymap="$default" first-keystroke="ctrl BACK_SPACE" replace-all="true"/>
<add-to-group group-id="EditorActions" anchor="last"/>
</action>
<override-text place="MainMenu"/>
<override-text place="popup" use-text-of-place="MainMenu"/>
</action>
<action
id="statusbar.startServer"
class="ee.carlrobert.codegpt.actions.StartServerAction">
<keyboard-shortcut first-keystroke="ctrl shift alt s" keymap="$default"/>
<action
id="codegpt.openSettings"
text="Open Settings"
class="ee.carlrobert.codegpt.actions.OpenSettingsAction">
<override-text place="MainMenu" text="Open Settings"/>
<override-text place="popup" use-text-of-place="MainMenu"/>
</action>
<action
id="statusbar.enableCompletions"
class="ee.carlrobert.codegpt.actions.EnableCompletionsAction">
<keyboard-shortcut first-keystroke="ctrl shift alt c" keymap="$default"/>
<override-text place="MainMenu"/>
<override-text place="popup" use-text-of-place="MainMenu"/>
</action>
<action
id="statusbar.stopServer"
class="ee.carlrobert.codegpt.actions.StopServerAction">
<keyboard-shortcut first-keystroke="ctrl shift alt s" keymap="$default"/>
<override-text place="MainMenu"/>
<override-text place="popup" use-text-of-place="MainMenu"/>
</action>
<action
id="statusbar.disableCompletions"
class="ee.carlrobert.codegpt.actions.DisableCompletionsAction">
<keyboard-shortcut first-keystroke="ctrl shift alt c" keymap="$default"/>
<override-text place="MainMenu"/>
<override-text place="popup" use-text-of-place="MainMenu"/>
</action>
<override-text place="MainMenu"/>
<override-text place="popup" use-text-of-place="MainMenu"/>
</action>
<action
id="statusbar.startServer"
class="ee.carlrobert.codegpt.actions.StartServerAction">
<keyboard-shortcut first-keystroke="ctrl shift alt s" keymap="$default"/>
<group id="codegpt.statusBarPopup">
<reference id="codegpt.openSettings"/>
<separator/>
<reference id="statusbar.stopServer"/>
<reference id="statusbar.startServer"/>
<reference id="statusbar.disableCompletions"/>
<reference id="statusbar.enableCompletions"/>
</group>
<override-text place="MainMenu"/>
<override-text place="popup" use-text-of-place="MainMenu"/>
</action>
<action
id="statusbar.stopServer"
class="ee.carlrobert.codegpt.actions.StopServerAction">
<keyboard-shortcut first-keystroke="ctrl shift alt s" keymap="$default"/>
</actions>
<override-text place="MainMenu"/>
<override-text place="popup" use-text-of-place="MainMenu"/>
</action>
<group id="codegpt.statusBarPopup">
<reference id="codegpt.openSettings"/>
<separator/>
<reference id="statusbar.stopServer"/>
<reference id="statusbar.startServer"/>
<reference id="statusbar.disableCompletions"/>
<reference id="statusbar.enableCompletions"/>
</group>
</actions>
</idea-plugin>

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="86" viewBox="0 0 512 86" fill="#000">
<path d="M15.444 60.984v12.3L37.806 86 75.62 64.5V52.2L37.81 73.705zm49.362-33.339v25.436l10.814-6.15V21.499L37.806 0l-10.81 6.146zM0 21.5v43l10.814 6.15V27.646l22.362-12.72-10.81-6.146zm122.932 43v-5.564h15.859q4.15 0 6.33-1.575 2.215-1.575 2.215-4.584v-.035q0-3.044-2.32-4.584-2.286-1.575-6.717-1.575h-15.367V41.44h14.101q4.079 0 6.259-1.504 2.216-1.54 2.216-4.34v-.035q0-2.66-1.899-4.059t-5.275-1.4h-15.402v-5.564h17.336q5.802 0 9.284 2.73 3.481 2.694 3.481 7.244v.035q0 3.255-2.285 5.773-2.252 2.485-5.732 3.01v.14q3.059.28 5.31 1.61 2.285 1.294 3.516 3.394 1.266 2.064 1.266 4.724v.035q0 3.5-1.793 6.019-1.76 2.52-5.029 3.884-3.27 1.365-7.877 1.365zm-3.763 0V24.537h7.56V64.5zm40.194 0V24.537h7.56v33.98h22.049V64.5zm30.629 0 17.09-39.963h6.506v6.93h-2.251L197.974 64.5zm8.299-10.428 2.25-5.459h21.733l2.25 5.459zm26.55 10.428-13.363-33.034v-6.929h4.255L232.858 64.5zm29.855.7q-6.681 0-11.57-2.52-4.887-2.52-7.56-7.173-2.673-4.655-2.673-10.988v-.035q0-6.334 2.638-10.988 2.672-4.655 7.56-7.139 4.924-2.52 11.605-2.52 5.415 0 9.706 1.785t6.998 5.004q2.742 3.22 3.411 7.489l.035.28h-7.526l-.14-.455q-.739-2.52-2.427-4.304t-4.22-2.73-5.802-.945q-4.325 0-7.49 1.785t-4.888 5.04q-1.688 3.254-1.688 7.698v.035q0 4.41 1.723 7.698 1.723 3.255 4.888 5.074 3.165 1.785 7.455 1.785 3.2 0 5.732-.945t4.255-2.8q1.724-1.854 2.532-4.479l.07-.244h7.526l-.035.28q-.669 4.304-3.376 7.523-2.708 3.219-6.998 5.004t-9.741 1.785m31.578-13.368v-8.608h.844l19.236-18.687h9.283l-18.919 18.057h-.949zM279.382 64.5V24.537h7.56V64.5zm27.992 0-16.599-19.281 5.557-4.585L316.903 64.5zm16.035 0v-5.564h15.859q4.15 0 6.33-1.575 2.216-1.575 2.216-4.584v-.035q0-3.044-2.321-4.584-2.286-1.575-6.717-1.575h-15.367V41.44h14.101q4.08 0 6.26-1.504 2.215-1.54 2.215-4.34v-.035q0-2.66-1.899-4.059t-5.275-1.4h-15.402v-5.564h17.336q5.803 0 9.284 2.73 3.481 2.694 3.481 7.244v.035q0 3.255-2.285 5.773-2.251 2.485-5.732 3.01v.14q3.059.28 5.31 1.61 2.286 1.294 3.516 3.394 1.266 2.064 1.266 4.724v.035q0 3.5-1.793 6.019-1.758 2.52-5.029 3.884-3.27 1.365-7.877 1.365zm-3.763 0V24.537h7.561V64.5zm60.695.7q-6.716 0-11.674-2.52-4.959-2.52-7.702-7.173-2.707-4.655-2.707-10.988v-.035q0-6.335 2.707-10.953 2.708-4.654 7.666-7.174 4.994-2.52 11.71-2.52 6.752 0 11.711 2.52 4.957 2.52 7.666 7.174 2.707 4.62 2.707 10.953v.035q0 6.333-2.707 10.988-2.709 4.654-7.666 7.173-4.924 2.52-11.711 2.52m.036-6.124q4.395 0 7.595-1.785a12.6 12.6 0 0 0 4.959-5.074q1.758-3.29 1.758-7.698v-.035q0-4.479-1.758-7.734t-4.994-5.004q-3.2-1.785-7.56-1.785t-7.596 1.785q-3.235 1.75-4.994 5.004-1.758 3.255-1.758 7.734v.035q0 4.479 1.758 7.768a12.34 12.34 0 0 0 4.994 5.04q3.236 1.749 7.596 1.749"/>
<path d="m401.968 64.5 17.442-22.43-1.829 5.178v-2.904l-15.508-19.807h9.179l11.534 15.258h3.305l-4.852 2.484 13.292-17.742h8.792l-15.93 19.947v2.904l-1.829-5.179L443.076 64.5h-8.967l-11.64-14.977h-3.305l4.923-2.415L410.548 64.5zm56.44 0 17.09-39.963h6.506v6.93h-2.25L466.391 64.5zm8.299-10.428 2.251-5.459h21.732l2.251 5.459zm26.55 10.428-13.363-33.034v-6.929h4.255L501.275 64.5zm11.182 0V24.537H512V64.5z" fill="#000"/>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 1200 1200">
<path fill="currentColor" d="M 233.959793 800.214905 L 468.644287 668.536987 L 472.590637 657.100647 L 468.644287 650.738403 L 457.208069 650.738403 L 417.986633 648.322144 L 283.892639 644.69812 L 167.597321 639.865845 L 54.926208 633.825623 L 26.577238 627.785339 L 3.3e-05 592.751709 L 2.73832 575.27533 L 26.577238 559.248352 L 60.724873 562.228149 L 136.187973 567.382629 L 249.422867 575.194763 L 331.570496 580.026978 L 453.261841 592.671082 L 472.590637 592.671082 L 475.328857 584.859009 L 468.724915 580.026978 L 463.570557 575.194763 L 346.389313 495.785217 L 219.543671 411.865906 L 153.100723 363.543762 L 117.181267 339.060425 L 99.060455 316.107361 L 91.248367 266.01355 L 123.865784 230.093994 L 167.677887 233.073853 L 178.872513 236.053772 L 223.248367 270.201477 L 318.040283 343.570496 L 441.825592 434.738342 L 459.946411 449.798706 L 467.194672 444.64447 L 468.080597 441.020203 L 459.946411 427.409485 L 392.617493 305.718323 L 320.778564 181.932983 L 288.80542 130.630859 L 280.348999 99.865845 C 277.369171 87.221436 275.194641 76.590698 275.194641 63.624268 L 312.322174 13.20813 L 332.8591 6.604126 L 382.389313 13.20813 L 403.248352 31.328979 L 434.013519 101.71814 L 483.865753 212.537048 L 561.181274 363.221497 L 583.812134 407.919434 L 595.892639 449.315491 L 600.40271 461.959839 L 608.214783 461.959839 L 608.214783 454.711609 L 614.577271 369.825623 L 626.335632 265.61084 L 637.771851 131.516846 L 641.718201 93.745117 L 660.402832 48.483276 L 697.530334 24.000122 L 726.52356 37.852417 L 750.362549 72 L 747.060486 94.067139 L 732.886047 186.201416 L 705.100708 330.52356 L 686.979919 427.167847 L 697.530334 427.167847 L 709.61084 415.087341 L 758.496704 350.174561 L 840.644348 247.490051 L 876.885925 206.738342 L 919.167847 161.71814 L 946.308838 140.29541 L 997.61084 140.29541 L 1035.38269 196.429626 L 1018.469849 254.416199 L 965.637634 321.422852 L 921.825562 378.201538 L 859.006714 462.765259 L 819.785278 530.41626 L 823.409424 535.812073 L 832.75177 534.92627 L 974.657776 504.724915 L 1051.328979 490.872559 L 1142.818848 475.167786 L 1184.214844 494.496582 L 1188.724854 514.147644 L 1172.456421 554.335693 L 1074.604126 578.496765 L 959.838989 601.449829 L 788.939636 641.879272 L 786.845764 643.409485 L 789.261841 646.389343 L 866.255127 653.637634 L 899.194702 655.409424 L 979.812134 655.409424 L 1129.932861 666.604187 L 1169.154419 692.537109 L 1192.671265 724.268677 L 1188.724854 748.429688 L 1128.322144 779.194641 L 1046.818848 759.865845 L 856.590759 714.604126 L 791.355774 698.335754 L 782.335693 698.335754 L 782.335693 703.731567 L 836.69812 756.885986 L 936.322205 846.845581 L 1061.073975 962.81897 L 1067.436279 991.490112 L 1051.409424 1014.120911 L 1034.496704 1011.704712 L 924.885986 929.234924 L 882.604126 892.107544 L 786.845764 811.48999 L 780.483276 811.48999 L 780.483276 819.946289 L 802.550415 852.241699 L 919.087341 1027.409424 L 925.127625 1081.127686 L 916.671204 1098.604126 L 886.469849 1109.154419 L 853.288696 1103.114136 L 785.073914 1007.355835 L 714.684631 899.516785 L 657.906067 802.872498 L 650.979858 806.81897 L 617.476624 1167.704834 L 601.771851 1186.147705 L 565.530212 1200 L 535.328857 1177.046997 L 519.302124 1139.919556 L 535.328857 1066.550537 L 554.657776 970.792053 L 570.362488 894.68457 L 584.536926 800.134277 L 592.993347 768.724976 L 592.429626 766.630859 L 585.503479 767.516968 L 514.22821 865.369263 L 405.825531 1011.865906 L 320.053711 1103.677979 L 299.516815 1111.812256 L 263.919525 1093.369263 L 267.221497 1060.429688 L 287.114136 1031.114136 L 405.825531 880.107361 L 477.422913 786.52356 L 523.651062 732.483276 L 523.328918 724.671265 L 520.590698 724.671265 L 205.288605 929.395935 L 149.154434 936.644409 L 124.993355 914.01355 L 127.973183 876.885986 L 139.409409 864.80542 L 234.201385 799.570435 L 233.879227 799.8927 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

View file

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.74684 10.3074L6.50076 8.76322L6.54684 8.62864L6.50076 8.55426H6.36608L5.90532 8.52593L4.33165 8.48343L2.96709 8.42676L1.64506 8.35593L1.3119 8.2851L1 7.87427L1.0319 7.66886L1.3119 7.48115L1.71241 7.51657L2.59848 7.57678L3.92759 7.66886L4.89165 7.72552L6.32 7.87427H6.54684L6.57873 7.78219L6.50076 7.72552L6.44051 7.66886L5.06532 6.73741L3.57671 5.75285L2.79696 5.18619L2.37519 4.89932L2.16253 4.63015L2.07038 4.04225L2.45316 3.62079L2.96709 3.65621L3.09823 3.69163L3.61924 4.09183L4.73215 4.95244L6.18532 6.02201L6.39797 6.19909L6.48304 6.13888L6.49367 6.09638L6.39797 5.93701L5.60759 4.50974L4.76405 3.05768L4.38835 2.4556L4.28911 2.09436C4.25367 1.94561 4.22886 1.82165 4.22886 1.66937L4.66481 1.07792L4.90582 1L5.48709 1.07792L5.73165 1.29041L6.09316 2.11561L6.67797 3.41538L7.58532 5.18265L7.85114 5.70681L7.99291 6.19201L8.04608 6.34075H8.13823V6.25576L8.21266 5.26056L8.35089 4.0387L8.48557 2.46623L8.53165 2.02353L8.75139 1.49228L9.18734 1.20541L9.5276 1.36833L9.80759 1.76853L9.76861 2.02707L9.60203 3.10726L9.27595 4.80015L9.06329 5.93347H9.18734L9.32911 5.7918L9.90329 5.03036L10.8673 3.82621L11.2927 3.34809L11.7889 2.82039L12.1078 2.56894H12.7104L13.1534 3.22767L12.9549 3.90767L12.3347 4.6939L11.8208 5.35973L11.0835 6.35138L10.6228 7.1447L10.6653 7.20845L10.7752 7.19782L12.441 6.84366L13.3413 6.68075L14.4152 6.49658L14.9008 6.72325L14.9539 6.95345L14.7625 7.42449L13.6142 7.70782L12.2673 7.97698L10.2613 8.45156L10.2365 8.46926L10.2648 8.50468L11.1686 8.58968L11.5549 8.61093H12.5013L14.2628 8.74197L14.7235 9.04655L15 9.41842L14.9539 9.70175L14.2451 10.063L13.2881 9.83633L11.0552 9.30509L10.2896 9.11384H10.1833V9.17759L10.8213 9.80091L11.9909 10.8563L13.4547 12.2163L13.5291 12.5527L13.3413 12.8184L13.1428 12.79L11.8562 11.8232L11.36 11.3876L10.2365 10.4419H10.162V10.5411L10.4208 10.9201L11.7889 12.9742L11.8597 13.6046L11.7605 13.81L11.4061 13.934L11.0162 13.8631L10.2152 12.7404L9.38937 11.4761L8.72304 10.3428L8.64152 10.3888L8.2481 14.621L8.0638 14.8371L7.63848 15L7.28405 14.7308L7.0962 14.2952L7.28405 13.4346L7.51089 12.3119L7.69519 11.4194L7.86177 10.3109L7.96101 9.94258L7.95392 9.91778L7.87241 9.92841L7.03595 11.0759L5.76354 12.7936L4.75696 13.8702L4.51595 13.9658L4.09772 13.7498L4.13671 13.3638L4.37063 13.0202L5.76354 11.2494L6.60354 10.1515L7.14582 9.51758L7.14228 9.4255H7.11038L3.41013 11.8267L2.75089 11.9117L2.46734 11.6461L2.50278 11.2105L2.63747 11.0688L3.75038 10.3038L3.74684 10.3074Z" fill="#D97757"/>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 92 96" fill="currentColor">
<path d="M65.45 16.3c10.89 0 19.71 8.86 19.71 19.8v6.6l5.74 11.46a4.48 4.48 0 0 1-.01 3.6L85.16 69.1v6.6c0 10.94-8.83 19.8-19.71 19.8H26.02c-10.89 0-19.71-8.86-19.71-19.8v-6.6L.45 57.8a4.48 4.48 0 0 1-.01-3.67L6.31 42.7v-6.6c0-10.94 8.83-19.8 19.71-19.8h39.43zm-2.52 5.7H29.19c-9.32 0-16.87 7.56-16.87 16.88v5.62l-4.88 9.46a4.48 4.48 0 0 0 .01 3.68l4.87 9.36v5.62c0 9.32 7.55 16.88 16.87 16.88h33.74c9.32 0 16.87-7.55 16.87-16.88V67l4.77-9.39a4.48 4.48 0 0 0 0-3.61L79.8 44.5v-5.62C79.8 29.56 72.25 22 62.93 22z" fill-rule="nonzero"/>
<circle cx="45.73" cy="11" r="11"/>
<rect stroke="currentColor" stroke-width="8" x="31" y="44.5" width="5" height="22" rx="2.5"/>
<rect stroke="currentColor" stroke-width="8" x="55" y="44.5" width="5" height="22" rx="2.5"/>
</svg>

After

Width:  |  Height:  |  Size: 878 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-code-xml"><path d="m18 16 4-4-4-4"/><path d="m6 8-4 4 4 4"/><path d="m14.5 4-5 16"/></svg>

After

Width:  |  Height:  |  Size: 293 B

View file

@ -0,0 +1,10 @@
<svg width="13" height="13" viewBox="2 2 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.064 3.344a4.578 4.578 0 0 1 2.285-.312c1 .115 1.891.54 2.673 1.275.01.01.024.017.037.021a.09.09 0 0 0 .043 0 4.55 4.55 0 0 1 3.046.275l.047.022.116.057a4.581 4.581 0 0 1 2.188 2.399c.209.51.313 1.041.315 1.595a4.24 4.24 0 0 1-.134 1.223.123.123 0 0 0 .03.115c.594.607.988 1.33 1.183 2.17.289 1.425-.007 2.71-.887 3.854l-.136.166a4.548 4.548 0 0 1-2.201 1.388.123.123 0 0 0-.081.076c-.191.551-.383 1.023-.74 1.494-.9 1.187-2.222 1.846-3.711 1.838-1.187-.006-2.239-.44-3.157-1.302a.107.107 0 0 0-.105-.024c-.388.125-.78.143-1.204.138a4.441 4.441 0 0 1-1.945-.466 4.544 4.544 0 0 1-1.61-1.335c-.152-.202-.303-.392-.414-.617a5.81 5.81 0 0 1-.37-.961 4.582 4.582 0 0 1-.014-2.298.124.124 0 0 0 .006-.056.085.085 0 0 0-.027-.048 4.467 4.467 0 0 1-1.034-1.651 3.896 3.896 0 0 1-.251-1.192 5.189 5.189 0 0 1 .141-1.6c.337-1.112.982-1.985 1.933-2.618.212-.141.413-.251.601-.33.215-.089.43-.164.646-.227a.098.098 0 0 0 .065-.066 4.51 4.51 0 0 1 .829-1.615 4.535 4.535 0 0 1 1.837-1.388Zm3.482 10.565a.637.637 0 0 0 0 1.272h3.636a.637.637 0 1 0 0-1.272h-3.636ZM8.462 9.23a.637.637 0 0 0-1.106.631l1.272 2.224-1.266 2.136a.636.636 0 1 0 1.095.649l1.454-2.455a.636.636 0 0 0 .005-.64L8.462 9.23Z" fill="url(#codex-fill)"/>
<defs>
<linearGradient id="codex-fill" x1="12" y1="3" x2="12" y2="21" gradientUnits="userSpaceOnUse">
<stop stop-color="#B1A7FF"/>
<stop offset="0.5" stop-color="#7A9DFF"/>
<stop offset="1" stop-color="#3941FF"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -0,0 +1,3 @@
<svg width="13" height="13" viewBox="0 0 545 545" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M466.383 137.073 259.914 17.8696c-6.63-3.8287-14.811-3.8287-21.441 0L32.0144 137.073C26.441 140.291 23 146.242 23 152.688v240.375c0 6.436 3.441 12.397 9.0144 15.615l206.4686 119.203c6.63 3.829 14.811 3.829 21.441 0l206.468-119.203c5.574-3.218 9.015-9.17 9.015-15.615V152.688c0-6.436-3.441-12.397-9.015-15.615Zm-12.969 25.25-199.316 345.223c-1.347 2.326-4.904 1.376-4.904-1.319V280.179c0-4.517-2.414-8.695-6.33-10.963L47.1063 156.197c-2.3263-1.347-1.3764-4.905 1.3182-4.905h398.6305c5.661 0 9.199 6.136 6.368 11.041h-.009Z" fill="#1F1F1F"/>
</svg>

After

Width:  |  Height:  |  Size: 656 B

View file

@ -0,0 +1,3 @@
<svg width="13" height="13" viewBox="0 0 545 545" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M466.383 137.073 259.914 17.8696c-6.63-3.8287-14.811-3.8287-21.441 0L32.0144 137.073C26.441 140.291 23 146.242 23 152.688v240.375c0 6.436 3.441 12.397 9.0144 15.615l206.4686 119.203c6.63 3.829 14.811 3.829 21.441 0l206.468-119.203c5.574-3.218 9.015-9.17 9.015-15.615V152.688c0-6.436-3.441-12.397-9.015-15.615Zm-12.969 25.25-199.316 345.223c-1.347 2.326-4.904 1.376-4.904-1.319V280.179c0-4.517-2.414-8.695-6.33-10.963L47.1063 156.197c-2.3263-1.347-1.3764-4.905 1.3182-4.905h398.6305c5.661 0 9.199 6.136 6.368 11.041h-.009Z" fill="#F3F3F3"/>
</svg>

After

Width:  |  Height:  |  Size: 656 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 900 900" fill="none"><path fill="currentColor" d="M622.037 192.524a10.58 10.58 0 0 1-4.056-2.001 10.573 10.573 0 0 1-3.98-7.89 10.595 10.595 0 0 1 .804-4.452c20.214-49.194 29.134-88.555 14.74-105.033-38.123-43.716-191.004 43.219-239.75 72.663a10.596 10.596 0 0 1-10.841.041 10.576 10.576 0 0 1-4.396-5.07c-20.491-49.089-42.031-83.233-63.865-84.714-57.871-3.96-104.516 165.624-118.17 220.895a10.574 10.574 0 0 1-1.993 4.057 10.572 10.572 0 0 1-7.884 3.977 10.586 10.586 0 0 1-4.447-.802c-49.194-20.215-88.572-29.134-105.033-14.74-43.717 38.123 43.202 191.005 72.644 239.75a10.553 10.553 0 0 1 1.454 4.279 10.573 10.573 0 0 1-6.481 10.958c-49.072 20.491-83.216 42.031-84.715 63.864-3.944 57.871 165.624 104.516 220.913 118.171a10.566 10.566 0 0 1 5.658 3.601 10.57 10.57 0 0 1 2.355 6.281 10.58 10.58 0 0 1-.799 4.442c-20.214 49.194-29.134 88.572-14.739 105.033 38.121 43.717 191.021-43.202 239.767-72.644a10.566 10.566 0 0 1 15.238 5.027c20.489 49.072 42.013 83.216 63.862 84.715 57.871 3.944 104.516-165.624 118.153-220.913a10.603 10.603 0 0 1 9.895-8.021 10.566 10.566 0 0 1 4.449.808c49.193 20.214 88.553 29.116 105.032 14.738 43.718-38.121-43.219-191.022-72.663-239.767a10.602 10.602 0 0 1 1.326-12.655 10.604 10.604 0 0 1 3.703-2.582c49.089-20.49 83.233-42.031 84.714-63.863 3.96-57.871-165.624-104.516-220.895-118.153Zm-66.394-55.478c11.123 19.938-46.196 152.797-88.83 245.724a8.36 8.36 0 0 1-8.239 4.855 8.374 8.374 0 0 1-7.412-6.043c-17.219-60.42-36.899-131.411-57.957-191.675a10.557 10.557 0 0 1 4.924-12.759c52.586-28.721 142.568-66.86 157.514-40.102ZM303.635 153.49c21.953 6.233 75.365 140.709 110.92 236.564a8.364 8.364 0 0 1-2.394 9.249 8.364 8.364 0 0 1-9.504.978c-54.943-30.493-119.013-66.824-176.522-94.546a10.565 10.565 0 0 1-5.528-12.501c16.926-57.44 53.532-148.095 83.028-139.744ZM137.064 343.322c19.921-11.123 152.795 46.197 245.707 88.83a8.369 8.369 0 0 1-1.189 15.652c-60.401 17.219-131.411 36.899-191.675 57.957a10.552 10.552 0 0 1-12.742-4.925c-28.668-52.584-66.876-142.568-40.101-157.514Zm16.443 252.009c6.217-21.953 140.709-75.365 236.564-110.921a8.368 8.368 0 0 1 10.227 11.898c-30.511 54.945-66.842 119.014-94.563 176.507a10.548 10.548 0 0 1-5.229 5.075 10.544 10.544 0 0 1-7.271.468c-57.441-16.822-148.095-53.531-139.728-83.027ZM343.34 761.902c-11.14-19.922 46.197-152.796 88.829-245.707a8.383 8.383 0 0 1 5.713-4.66 8.38 8.38 0 0 1 7.182 1.664 8.371 8.371 0 0 1 2.758 4.184c17.217 60.403 36.898 131.412 57.957 191.675a10.558 10.558 0 0 1-4.942 12.743c-52.568 28.668-142.568 66.875-157.445 40.101h-.052Zm252.009-16.443c-21.971-6.216-75.383-140.709-110.939-236.564a8.37 8.37 0 0 1 11.916-10.228c54.926 30.494 119.014 66.842 176.506 94.563a10.538 10.538 0 0 1 5.527 12.502c-16.909 57.526-53.515 148.094-83.01 139.727Zm166.57-189.834c-19.938 11.141-152.796-46.197-245.724-88.83a8.367 8.367 0 0 1-2.993-12.892 8.378 8.378 0 0 1 4.182-2.759c60.419-17.218 131.41-36.899 191.675-57.958a10.577 10.577 0 0 1 12.758 4.943c28.652 52.568 66.86 142.569 40.102 157.496Zm-16.444-252.009c-6.232 21.971-140.709 75.383-236.562 110.94a8.371 8.371 0 0 1-10.229-11.917c30.495-54.926 66.825-119.012 94.547-176.505a10.555 10.555 0 0 1 12.5-5.528c57.441 16.909 148.096 53.516 139.744 83.01Z"/></svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View file

@ -0,0 +1,293 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 360 360" width="16" height="16">
<g fill="currentColor">
<circle cx="100.50" cy="40.50" r="4.50"/>
<circle cx="112.50" cy="40.50" r="4.50"/>
<circle cx="124.50" cy="40.50" r="4.50"/>
<circle cx="136.50" cy="40.50" r="4.50"/>
<circle cx="148.50" cy="40.50" r="4.50"/>
<circle cx="76.50" cy="52.50" r="4.50"/>
<circle cx="88.50" cy="52.50" r="4.50"/>
<circle cx="100.50" cy="52.50" r="4.50"/>
<circle cx="136.50" cy="52.50" r="4.50"/>
<circle cx="148.50" cy="52.50" r="4.50"/>
<circle cx="160.50" cy="52.50" r="4.50"/>
<circle cx="64.50" cy="64.50" r="4.50"/>
<circle cx="76.50" cy="64.50" r="4.50"/>
<circle cx="88.50" cy="64.50" r="4.50"/>
<circle cx="100.50" cy="64.50" r="4.50"/>
<circle cx="136.50" cy="64.50" r="4.50"/>
<circle cx="148.50" cy="64.50" r="4.50"/>
<circle cx="160.50" cy="64.50" r="4.50"/>
<circle cx="172.50" cy="64.50" r="4.50"/>
<circle cx="52.50" cy="76.50" r="4.50"/>
<circle cx="64.50" cy="76.50" r="4.50"/>
<circle cx="76.50" cy="76.50" r="4.50"/>
<circle cx="88.50" cy="76.50" r="4.50"/>
<circle cx="100.50" cy="76.50" r="4.50"/>
<circle cx="136.50" cy="76.50" r="4.50"/>
<circle cx="148.50" cy="76.50" r="4.50"/>
<circle cx="160.50" cy="76.50" r="4.50"/>
<circle cx="52.50" cy="88.50" r="4.50"/>
<circle cx="64.50" cy="88.50" r="4.50"/>
<circle cx="76.50" cy="88.50" r="4.50"/>
<circle cx="88.50" cy="88.50" r="4.50"/>
<circle cx="100.50" cy="88.50" r="4.50"/>
<circle cx="148.50" cy="88.50" r="4.50"/>
<circle cx="52.50" cy="100.50" r="4.50"/>
<circle cx="64.50" cy="100.50" r="4.50"/>
<circle cx="76.50" cy="100.50" r="4.50"/>
<circle cx="88.50" cy="100.50" r="4.50"/>
<circle cx="100.50" cy="100.50" r="4.50"/>
<circle cx="52.50" cy="112.50" r="4.50"/>
<circle cx="64.50" cy="112.50" r="4.50"/>
<circle cx="76.50" cy="112.50" r="4.50"/>
<circle cx="88.50" cy="112.50" r="4.50"/>
<circle cx="100.50" cy="112.50" r="4.50"/>
<circle cx="52.50" cy="124.50" r="4.50"/>
<circle cx="64.50" cy="124.50" r="4.50"/>
<circle cx="76.50" cy="124.50" r="4.50"/>
<circle cx="88.50" cy="124.50" r="4.50"/>
<circle cx="100.50" cy="124.50" r="4.50"/>
<circle cx="232.50" cy="124.50" r="4.50"/>
<circle cx="244.50" cy="124.50" r="4.50"/>
<circle cx="256.50" cy="124.50" r="4.50"/>
<circle cx="28.50" cy="136.50" r="4.50"/>
<circle cx="40.50" cy="136.50" r="4.50"/>
<circle cx="52.50" cy="136.50" r="4.50"/>
<circle cx="64.50" cy="136.50" r="4.50"/>
<circle cx="76.50" cy="136.50" r="4.50"/>
<circle cx="88.50" cy="136.50" r="4.50"/>
<circle cx="100.50" cy="136.50" r="4.50"/>
<circle cx="112.50" cy="136.50" r="4.50"/>
<circle cx="124.50" cy="136.50" r="4.50"/>
<circle cx="196.50" cy="136.50" r="4.50"/>
<circle cx="208.50" cy="136.50" r="4.50"/>
<circle cx="220.50" cy="136.50" r="4.50"/>
<circle cx="232.50" cy="136.50" r="4.50"/>
<circle cx="244.50" cy="136.50" r="4.50"/>
<circle cx="256.50" cy="136.50" r="4.50"/>
<circle cx="268.50" cy="136.50" r="4.50"/>
<circle cx="280.50" cy="136.50" r="4.50"/>
<circle cx="28.50" cy="148.50" r="4.50"/>
<circle cx="40.50" cy="148.50" r="4.50"/>
<circle cx="52.50" cy="148.50" r="4.50"/>
<circle cx="64.50" cy="148.50" r="4.50"/>
<circle cx="76.50" cy="148.50" r="4.50"/>
<circle cx="88.50" cy="148.50" r="4.50"/>
<circle cx="100.50" cy="148.50" r="4.50"/>
<circle cx="112.50" cy="148.50" r="4.50"/>
<circle cx="124.50" cy="148.50" r="4.50"/>
<circle cx="172.50" cy="148.50" r="4.50"/>
<circle cx="184.50" cy="148.50" r="4.50"/>
<circle cx="196.50" cy="148.50" r="4.50"/>
<circle cx="256.50" cy="148.50" r="4.50"/>
<circle cx="268.50" cy="148.50" r="4.50"/>
<circle cx="280.50" cy="148.50" r="4.50"/>
<circle cx="292.50" cy="148.50" r="4.50"/>
<circle cx="52.50" cy="160.50" r="4.50"/>
<circle cx="64.50" cy="160.50" r="4.50"/>
<circle cx="76.50" cy="160.50" r="4.50"/>
<circle cx="88.50" cy="160.50" r="4.50"/>
<circle cx="100.50" cy="160.50" r="4.50"/>
<circle cx="160.50" cy="160.50" r="4.50"/>
<circle cx="172.50" cy="160.50" r="4.50"/>
<circle cx="184.50" cy="160.50" r="4.50"/>
<circle cx="196.50" cy="160.50" r="4.50"/>
<circle cx="256.50" cy="160.50" r="4.50"/>
<circle cx="268.50" cy="160.50" r="4.50"/>
<circle cx="280.50" cy="160.50" r="4.50"/>
<circle cx="292.50" cy="160.50" r="4.50"/>
<circle cx="304.50" cy="160.50" r="4.50"/>
<circle cx="52.50" cy="172.50" r="4.50"/>
<circle cx="64.50" cy="172.50" r="4.50"/>
<circle cx="76.50" cy="172.50" r="4.50"/>
<circle cx="88.50" cy="172.50" r="4.50"/>
<circle cx="100.50" cy="172.50" r="4.50"/>
<circle cx="160.50" cy="172.50" r="4.50"/>
<circle cx="172.50" cy="172.50" r="4.50"/>
<circle cx="184.50" cy="172.50" r="4.50"/>
<circle cx="196.50" cy="172.50" r="4.50"/>
<circle cx="208.50" cy="172.50" r="4.50"/>
<circle cx="256.50" cy="172.50" r="4.50"/>
<circle cx="268.50" cy="172.50" r="4.50"/>
<circle cx="280.50" cy="172.50" r="4.50"/>
<circle cx="292.50" cy="172.50" r="4.50"/>
<circle cx="304.50" cy="172.50" r="4.50"/>
<circle cx="52.50" cy="184.50" r="4.50"/>
<circle cx="64.50" cy="184.50" r="4.50"/>
<circle cx="76.50" cy="184.50" r="4.50"/>
<circle cx="88.50" cy="184.50" r="4.50"/>
<circle cx="100.50" cy="184.50" r="4.50"/>
<circle cx="160.50" cy="184.50" r="4.50"/>
<circle cx="172.50" cy="184.50" r="4.50"/>
<circle cx="184.50" cy="184.50" r="4.50"/>
<circle cx="196.50" cy="184.50" r="4.50"/>
<circle cx="208.50" cy="184.50" r="4.50"/>
<circle cx="256.50" cy="184.50" r="4.50"/>
<circle cx="268.50" cy="184.50" r="4.50"/>
<circle cx="280.50" cy="184.50" r="4.50"/>
<circle cx="292.50" cy="184.50" r="4.50"/>
<circle cx="304.50" cy="184.50" r="4.50"/>
<circle cx="52.50" cy="196.50" r="4.50"/>
<circle cx="64.50" cy="196.50" r="4.50"/>
<circle cx="76.50" cy="196.50" r="4.50"/>
<circle cx="88.50" cy="196.50" r="4.50"/>
<circle cx="100.50" cy="196.50" r="4.50"/>
<circle cx="172.50" cy="196.50" r="4.50"/>
<circle cx="184.50" cy="196.50" r="4.50"/>
<circle cx="196.50" cy="196.50" r="4.50"/>
<circle cx="256.50" cy="196.50" r="4.50"/>
<circle cx="268.50" cy="196.50" r="4.50"/>
<circle cx="280.50" cy="196.50" r="4.50"/>
<circle cx="292.50" cy="196.50" r="4.50"/>
<circle cx="304.50" cy="196.50" r="4.50"/>
<circle cx="52.50" cy="208.50" r="4.50"/>
<circle cx="64.50" cy="208.50" r="4.50"/>
<circle cx="76.50" cy="208.50" r="4.50"/>
<circle cx="88.50" cy="208.50" r="4.50"/>
<circle cx="100.50" cy="208.50" r="4.50"/>
<circle cx="244.50" cy="208.50" r="4.50"/>
<circle cx="256.50" cy="208.50" r="4.50"/>
<circle cx="268.50" cy="208.50" r="4.50"/>
<circle cx="280.50" cy="208.50" r="4.50"/>
<circle cx="292.50" cy="208.50" r="4.50"/>
<circle cx="304.50" cy="208.50" r="4.50"/>
<circle cx="52.50" cy="220.50" r="4.50"/>
<circle cx="64.50" cy="220.50" r="4.50"/>
<circle cx="76.50" cy="220.50" r="4.50"/>
<circle cx="88.50" cy="220.50" r="4.50"/>
<circle cx="100.50" cy="220.50" r="4.50"/>
<circle cx="220.50" cy="220.50" r="4.50"/>
<circle cx="232.50" cy="220.50" r="4.50"/>
<circle cx="256.50" cy="220.50" r="4.50"/>
<circle cx="268.50" cy="220.50" r="4.50"/>
<circle cx="280.50" cy="220.50" r="4.50"/>
<circle cx="292.50" cy="220.50" r="4.50"/>
<circle cx="304.50" cy="220.50" r="4.50"/>
<circle cx="52.50" cy="232.50" r="4.50"/>
<circle cx="64.50" cy="232.50" r="4.50"/>
<circle cx="76.50" cy="232.50" r="4.50"/>
<circle cx="88.50" cy="232.50" r="4.50"/>
<circle cx="100.50" cy="232.50" r="4.50"/>
<circle cx="196.50" cy="232.50" r="4.50"/>
<circle cx="208.50" cy="232.50" r="4.50"/>
<circle cx="220.50" cy="232.50" r="4.50"/>
<circle cx="256.50" cy="232.50" r="4.50"/>
<circle cx="268.50" cy="232.50" r="4.50"/>
<circle cx="280.50" cy="232.50" r="4.50"/>
<circle cx="292.50" cy="232.50" r="4.50"/>
<circle cx="304.50" cy="232.50" r="4.50"/>
<circle cx="52.50" cy="244.50" r="4.50"/>
<circle cx="64.50" cy="244.50" r="4.50"/>
<circle cx="76.50" cy="244.50" r="4.50"/>
<circle cx="88.50" cy="244.50" r="4.50"/>
<circle cx="100.50" cy="244.50" r="4.50"/>
<circle cx="184.50" cy="244.50" r="4.50"/>
<circle cx="196.50" cy="244.50" r="4.50"/>
<circle cx="208.50" cy="244.50" r="4.50"/>
<circle cx="256.50" cy="244.50" r="4.50"/>
<circle cx="268.50" cy="244.50" r="4.50"/>
<circle cx="280.50" cy="244.50" r="4.50"/>
<circle cx="292.50" cy="244.50" r="4.50"/>
<circle cx="304.50" cy="244.50" r="4.50"/>
<circle cx="52.50" cy="256.50" r="4.50"/>
<circle cx="64.50" cy="256.50" r="4.50"/>
<circle cx="76.50" cy="256.50" r="4.50"/>
<circle cx="88.50" cy="256.50" r="4.50"/>
<circle cx="100.50" cy="256.50" r="4.50"/>
<circle cx="172.50" cy="256.50" r="4.50"/>
<circle cx="184.50" cy="256.50" r="4.50"/>
<circle cx="196.50" cy="256.50" r="4.50"/>
<circle cx="208.50" cy="256.50" r="4.50"/>
<circle cx="256.50" cy="256.50" r="4.50"/>
<circle cx="268.50" cy="256.50" r="4.50"/>
<circle cx="280.50" cy="256.50" r="4.50"/>
<circle cx="292.50" cy="256.50" r="4.50"/>
<circle cx="304.50" cy="256.50" r="4.50"/>
<circle cx="52.50" cy="268.50" r="4.50"/>
<circle cx="64.50" cy="268.50" r="4.50"/>
<circle cx="76.50" cy="268.50" r="4.50"/>
<circle cx="88.50" cy="268.50" r="4.50"/>
<circle cx="100.50" cy="268.50" r="4.50"/>
<circle cx="160.50" cy="268.50" r="4.50"/>
<circle cx="172.50" cy="268.50" r="4.50"/>
<circle cx="184.50" cy="268.50" r="4.50"/>
<circle cx="196.50" cy="268.50" r="4.50"/>
<circle cx="256.50" cy="268.50" r="4.50"/>
<circle cx="268.50" cy="268.50" r="4.50"/>
<circle cx="280.50" cy="268.50" r="4.50"/>
<circle cx="292.50" cy="268.50" r="4.50"/>
<circle cx="304.50" cy="268.50" r="4.50"/>
<circle cx="52.50" cy="280.50" r="4.50"/>
<circle cx="64.50" cy="280.50" r="4.50"/>
<circle cx="76.50" cy="280.50" r="4.50"/>
<circle cx="88.50" cy="280.50" r="4.50"/>
<circle cx="100.50" cy="280.50" r="4.50"/>
<circle cx="160.50" cy="280.50" r="4.50"/>
<circle cx="172.50" cy="280.50" r="4.50"/>
<circle cx="184.50" cy="280.50" r="4.50"/>
<circle cx="196.50" cy="280.50" r="4.50"/>
<circle cx="208.50" cy="280.50" r="4.50"/>
<circle cx="256.50" cy="280.50" r="4.50"/>
<circle cx="268.50" cy="280.50" r="4.50"/>
<circle cx="280.50" cy="280.50" r="4.50"/>
<circle cx="292.50" cy="280.50" r="4.50"/>
<circle cx="304.50" cy="280.50" r="4.50"/>
<circle cx="52.50" cy="292.50" r="4.50"/>
<circle cx="64.50" cy="292.50" r="4.50"/>
<circle cx="76.50" cy="292.50" r="4.50"/>
<circle cx="88.50" cy="292.50" r="4.50"/>
<circle cx="100.50" cy="292.50" r="4.50"/>
<circle cx="160.50" cy="292.50" r="4.50"/>
<circle cx="172.50" cy="292.50" r="4.50"/>
<circle cx="184.50" cy="292.50" r="4.50"/>
<circle cx="196.50" cy="292.50" r="4.50"/>
<circle cx="208.50" cy="292.50" r="4.50"/>
<circle cx="220.50" cy="292.50" r="4.50"/>
<circle cx="232.50" cy="292.50" r="4.50"/>
<circle cx="244.50" cy="292.50" r="4.50"/>
<circle cx="256.50" cy="292.50" r="4.50"/>
<circle cx="268.50" cy="292.50" r="4.50"/>
<circle cx="280.50" cy="292.50" r="4.50"/>
<circle cx="292.50" cy="292.50" r="4.50"/>
<circle cx="304.50" cy="292.50" r="4.50"/>
<circle cx="52.50" cy="304.50" r="4.50"/>
<circle cx="64.50" cy="304.50" r="4.50"/>
<circle cx="76.50" cy="304.50" r="4.50"/>
<circle cx="88.50" cy="304.50" r="4.50"/>
<circle cx="100.50" cy="304.50" r="4.50"/>
<circle cx="160.50" cy="304.50" r="4.50"/>
<circle cx="172.50" cy="304.50" r="4.50"/>
<circle cx="184.50" cy="304.50" r="4.50"/>
<circle cx="196.50" cy="304.50" r="4.50"/>
<circle cx="208.50" cy="304.50" r="4.50"/>
<circle cx="220.50" cy="304.50" r="4.50"/>
<circle cx="232.50" cy="304.50" r="4.50"/>
<circle cx="256.50" cy="304.50" r="4.50"/>
<circle cx="268.50" cy="304.50" r="4.50"/>
<circle cx="280.50" cy="304.50" r="4.50"/>
<circle cx="292.50" cy="304.50" r="4.50"/>
<circle cx="304.50" cy="304.50" r="4.50"/>
<circle cx="316.50" cy="304.50" r="4.50"/>
<circle cx="328.50" cy="304.50" r="4.50"/>
<circle cx="28.50" cy="316.50" r="4.50"/>
<circle cx="40.50" cy="316.50" r="4.50"/>
<circle cx="52.50" cy="316.50" r="4.50"/>
<circle cx="64.50" cy="316.50" r="4.50"/>
<circle cx="76.50" cy="316.50" r="4.50"/>
<circle cx="88.50" cy="316.50" r="4.50"/>
<circle cx="100.50" cy="316.50" r="4.50"/>
<circle cx="112.50" cy="316.50" r="4.50"/>
<circle cx="124.50" cy="316.50" r="4.50"/>
<circle cx="172.50" cy="316.50" r="4.50"/>
<circle cx="184.50" cy="316.50" r="4.50"/>
<circle cx="196.50" cy="316.50" r="4.50"/>
<circle cx="208.50" cy="316.50" r="4.50"/>
<circle cx="220.50" cy="316.50" r="4.50"/>
<circle cx="268.50" cy="316.50" r="4.50"/>
<circle cx="280.50" cy="316.50" r="4.50"/>
<circle cx="292.50" cy="316.50" r="4.50"/>
<circle cx="304.50" cy="316.50" r="4.50"/>
<circle cx="316.50" cy="316.50" r="4.50"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

View file

@ -0,0 +1,20 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_3179_67531)">
<g clip-path="url(#clip1_3179_67531)">
<path d="M16 7.516C13.9242 7.64339 11.9666 8.52545 10.496 9.99603C9.02545 11.4666 8.14339 13.4242 8.016 15.5H7.984C7.85682 13.4241 6.97483 11.4664 5.5042 9.9958C4.03358 8.52518 2.07588 7.64318 0 7.516L0 7.484C2.07588 7.35682 4.03358 6.47483 5.5042 5.0042C6.97483 3.53358 7.85682 1.57588 7.984 -0.5L8.016 -0.5C8.14339 1.57581 9.02545 3.53339 10.496 5.00397C11.9666 6.47455 13.9242 7.35661 16 7.484V7.516Z" fill="url(#paint0_radial_3179_67531)"/>
</g>
</g>
<defs>
<radialGradient id="paint0_radial_3179_67531" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(1.588 6.003) rotate(18.6832) scale(17.03 136.421)">
<stop offset="0.067" stop-color="#9168C0"/>
<stop offset="0.343" stop-color="#5684D1"/>
<stop offset="0.672" stop-color="#1BA1E3"/>
</radialGradient>
<clipPath id="clip0_3179_67531">
<rect width="16" height="16" fill="white"/>
</clipPath>
<clipPath id="clip1_3179_67531">
<rect width="16" height="16" fill="white" transform="translate(0 -0.5)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1,3 @@
<svg fill="currentColor" fill-rule="evenodd" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M19.245 5.364c1.322 1.36 1.877 3.216 2.11 5.817.622 0 1.2.135 1.592.654l.73.964c.21.278.323.61.323.955v2.62c0 .339-.173.669-.453.868C20.239 19.602 16.157 21.5 12 21.5c-4.6 0-9.205-2.583-11.547-4.258-.28-.2-.452-.53-.453-.868v-2.62c0-.345.113-.679.321-.956l.73-.963c.392-.517.974-.654 1.593-.654l.029-.297c.25-2.446.81-4.213 2.082-5.52 2.461-2.54 5.71-2.851 7.146-2.864h.198c1.436.013 4.685.323 7.146 2.864zm-7.244 4.328c-.284 0-.613.016-.962.05-.123.447-.305.85-.57 1.108-1.05 1.023-2.316 1.18-2.994 1.18-.638 0-1.306-.13-1.851-.464-.516.165-1.012.403-1.044.996a65.882 65.882 0 00-.063 2.884l-.002.48c-.002.563-.005 1.126-.013 1.69.002.326.204.63.51.765 2.482 1.102 4.83 1.657 6.99 1.657 2.156 0 4.504-.555 6.985-1.657a.854.854 0 00.51-.766c.03-1.682.006-3.372-.076-5.053-.031-.596-.528-.83-1.046-.996-.546.333-1.212.464-1.85.464-.677 0-1.942-.157-2.993-1.18-.266-.258-.447-.661-.57-1.108-.32-.032-.64-.049-.96-.05zm-2.525 4.013c.539 0 .976.426.976.95v1.753c0 .525-.437.95-.976.95a.964.964 0 01-.976-.95v-1.752c0-.525.437-.951.976-.951zm5 0c.539 0 .976.426.976.95v1.753c0 .525-.437.95-.976.95a.964.964 0 01-.976-.95v-1.752c0-.525.437-.951.976-.951zM7.635 5.087c-1.05.102-1.935.438-2.385.906-.975 1.037-.765 3.668-.21 4.224.405.394 1.17.657 1.995.657h.09c.649-.013 1.785-.176 2.73-1.11.435-.41.705-1.433.675-2.47-.03-.834-.27-1.52-.63-1.813-.39-.336-1.275-.482-2.265-.394zm6.465.394c-.36.292-.6.98-.63 1.813-.03 1.037.24 2.06.675 2.47.968.957 2.136 1.104 2.776 1.11h.044c.825 0 1.59-.263 1.995-.657.555-.556.765-3.187-.21-4.224-.45-.468-1.335-.804-2.385-.906-.99-.088-1.875.058-2.265.394zM12 7.615c-.24 0-.525.015-.84.044.03.16.045.336.06.526l-.001.159a2.94 2.94 0 01-.014.25c.225-.022.425-.027.612-.028h.366c.187 0 .387.006.612.028-.015-.146-.015-.277-.015-.409.015-.19.03-.365.06-.526a9.29 9.29 0 00-.84-.044z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View file

@ -0,0 +1,3 @@
<svg fill="currentColor" fill-rule="evenodd" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M21.595 23.61c1.167-.254 2.405-.944 2.405-.944l-2.167-1.784a12.124 12.124 0 01-2.695-3.131 12.127 12.127 0 00-3.97-4.049l-.794-.462a1.115 1.115 0 01-.488-.815.844.844 0 01.154-.575c.413-.582 2.548-3.115 2.94-3.44.503-.416 1.065-.762 1.586-1.159.074-.056.148-.112.221-.17.003-.002.007-.004.009-.007.167-.131.325-.272.45-.438.453-.524.563-.988.59-1.193-.061-.197-.244-.639-.753-1.148.319.02.705.272 1.056.569.235-.376.481-.773.727-1.171.165-.266-.08-.465-.086-.471h-.001V3.22c-.007-.007-.206-.25-.471-.086-.567.35-1.134.702-1.639 1.021 0 0-.597-.012-1.305.599a2.464 2.464 0 00-.438.45l-.007.009c-.058.072-.114.147-.17.221-.397.521-.743 1.083-1.16 1.587-.323.391-2.857 2.526-3.44 2.94a.842.842 0 01-.574.153 1.115 1.115 0 01-.815-.488l-.462-.794a12.123 12.123 0 00-4.049-3.97 12.133 12.133 0 01-3.13-2.695L1.332 0S.643 1.238.39 2.405c.352.428 1.27 1.49 2.34 2.302C1.58 4.167.73 3.75.06 3.4c-.103.765-.063 1.92.043 2.816.726.317 1.961.806 3.219 1.066-1.006.236-2.11.278-2.961.262.15.554.358 1.119.64 1.688.119.263.25.52.39.77.452.125 2.222.383 3.164.171l-2.51.897a27.776 27.776 0 002.544 2.726c2.031-1.092 2.494-1.241 4.018-2.238-2.467 2.008-3.108 2.828-3.8 3.67l-.483.678c-.25.351-.469.725-.65 1.117-.61 1.31-1.47 4.1-1.47 4.1-.154.486.202.842.674.674 0 0 2.79-.861 4.1-1.47.392-.182.766-.4 1.118-.65l.677-.483c.227-.187.453-.37.701-.586 0 0 1.705 2.02 3.458 3.349l.896-2.511c-.211.942.046 2.712.17 3.163.252.142.509.272.772.392.569.28 1.134.49 1.688.64-.016-.853.026-1.956.261-2.962.26 1.258.75 2.493 1.067 3.219.895.106 2.051.146 2.816.043a73.87 73.87 0 01-1.308-2.67c.811 1.07 1.874 1.988 2.302 2.34h-.001z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 40 40" fill="none">
<path d="M25 15H35V16.75C35 29 30.5001 35 16.5001 35H15V25H16.5001C22.6251 25 25 22.875 25 16.75V15Z" fill="currentColor"/>
<rect x="5" y="15" width="10" height="10" fill="currentColor"/>
<rect x="15" y="5" width="10" height="10" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 360 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24">
<path fill="currentColor" d="M19.514 7.342C19.711 7.862 19.814 8.413 19.816 8.969C19.816 12.494 15.998 14.697 12.946 12.934C11.529 12.116 10.657 10.605 10.657 8.969C10.657 5.646 14.085 3.431 17.115 4.793C15.26 3.373 12.99 2.607 10.655 2.612C4.772 2.614 0.005 7.381 0 13.265C0.002 19.148 4.772 23.918 10.655 23.92C16.538 23.916 21.306 19.147 21.31 13.265C21.312 11.155 20.687 9.095 19.514 7.342ZM14.841 4.666C14.841 8.191 18.657 10.395 21.709 8.632C23.127 7.814 24 6.302 24 4.666C24 1.14 20.184-1.061 17.13 0.699C15.714 1.519 14.841 3.03 14.841 4.666"/>
</svg>

After

Width:  |  Height:  |  Size: 646 B

View file

@ -0,0 +1,11 @@
<svg width="1200" height="1200" viewBox="0 0 1200 1200" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="1200" height="1200" rx="260" fill="#9046FF"/>
<mask id="mask0_1106_4856" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="272" y="202" width="655" height="796">
<path d="M926.578 202.793H272.637V997.857H926.578V202.793Z" fill="white"/>
</mask>
<g mask="url(#mask0_1106_4856)">
<path d="M398.554 818.914C316.315 1001.03 491.477 1046.74 620.672 940.156C658.687 1059.66 801.052 970.473 852.234 877.795C964.787 673.567 919.318 465.357 907.64 422.374C827.637 129.443 427.623 128.946 358.8 423.865C342.651 475.544 342.402 534.18 333.458 595.051C328.986 625.86 325.507 645.488 313.83 677.785C306.873 696.424 297.68 712.819 282.773 740.645C259.915 783.881 269.604 867.113 387.87 823.883L399.051 818.914H398.554Z" fill="white"/>
<path d="M636.123 549.353C603.328 549.353 598.359 510.097 598.359 486.742C598.359 465.623 602.086 448.977 609.293 438.293C615.504 428.852 624.697 424.131 636.123 424.131C647.555 424.131 657.492 428.852 664.447 438.541C672.398 449.474 676.623 466.12 676.623 486.742C676.623 525.998 661.471 549.353 636.375 549.353H636.123Z" fill="black"/>
<path d="M771.24 549.353C738.445 549.353 733.477 510.097 733.477 486.742C733.477 465.623 737.203 448.977 744.41 438.293C750.621 428.852 759.814 424.131 771.24 424.131C782.672 424.131 792.609 428.852 799.564 438.541C807.516 449.474 811.74 466.12 811.74 486.742C811.74 525.998 796.588 549.353 771.492 549.353H771.24Z" fill="black"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -0,0 +1,19 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<rect fill="none" stroke="currentColor" stroke-width="1.1" x="4.5" y="2.5" width="7" height="10" rx="3.5"/>
<line stroke="currentColor" stroke-width="1" x1="3" y1="5.5" x2="13" y2="5.5"/>
<circle fill="none" stroke="currentColor" stroke-width="1" cx="6.5" cy="5.5" r="1.6"/>
<circle fill="none" stroke="currentColor" stroke-width="1" cx="9.5" cy="5.5" r="1.6"/>
<circle fill="currentColor" cx="6.8" cy="5.6" r="0.55"/>
<circle fill="currentColor" cx="9.8" cy="5.6" r="0.55"/>
<path fill="none" stroke="currentColor" stroke-width="0.6" stroke-linecap="round" d="M7 8.3 Q8 9 9 8.3"/>
<path fill="none" stroke="currentColor" stroke-width="0.9" stroke-linecap="round" d="M4.5 8 L3 9.5"/>
<path fill="none" stroke="currentColor" stroke-width="0.9" stroke-linecap="round" d="M11.5 8 L13 9.5"/>
<line stroke="currentColor" stroke-width="0.7" x1="5" y1="9.5" x2="11" y2="9.5"/>
<line stroke="currentColor" stroke-width="0.9" x1="6.5" y1="12.5" x2="6.5" y2="14"/>
<line stroke="currentColor" stroke-width="0.9" x1="9.5" y1="12.5" x2="9.5" y2="14"/>
<line stroke="currentColor" stroke-width="0.9" stroke-linecap="round" x1="6.5" y1="14" x2="5.5" y2="14"/>
<line stroke="currentColor" stroke-width="0.9" stroke-linecap="round" x1="9.5" y1="14" x2="10.5" y2="14"/>
<line stroke="currentColor" stroke-width="0.7" stroke-linecap="round" x1="8" y1="2.5" x2="7.3" y2="0.8"/>
<line stroke="currentColor" stroke-width="0.7" stroke-linecap="round" x1="8" y1="2.5" x2="8" y2="0.5"/>
<line stroke="currentColor" stroke-width="0.7" stroke-linecap="round" x1="8" y1="2.5" x2="8.7" y2="0.8"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -0,0 +1,12 @@
<svg width="16" height="16" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.8147 5.35803H5.35791V8.46914H8.8147V5.35803Z" fill="currentColor"/>
<path d="M22.6419 5.35803H19.1851V8.46914H22.6419V5.35803Z" fill="currentColor"/>
<path d="M15.7283 15.7284H12.2715V18.8395H15.7283V15.7284Z" fill="currentColor"/>
<path d="M8.8147 15.7284H5.35791V18.8395H8.8147V15.7284Z" fill="currentColor"/>
<path d="M22.6419 15.7284H19.1851V18.8395H22.6419V15.7284Z" fill="currentColor"/>
<path d="M12.2715 8.81482H5.35791V11.9259H12.2715V8.81482Z" fill="currentColor"/>
<path d="M12.2718 19.1852H1.90137V22.2963H12.2718V19.1852Z" fill="currentColor"/>
<path d="M26.0989 19.1852H15.7285V22.2963H26.0989V19.1852Z" fill="currentColor"/>
<path d="M22.6419 12.2716H5.35791V15.3827H22.6419V12.2716Z" fill="currentColor"/>
<path d="M22.6421 8.81482H15.7285V11.9259H22.6421V8.81482Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 919 B

View file

@ -0,0 +1,22 @@
<svg viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="lobster-gradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#ff4d4d"/>
<stop offset="100%" stop-color="#991b1b"/>
</linearGradient>
</defs>
<!-- Body -->
<path d="M60 10 C30 10 15 35 15 55 C15 75 30 95 45 100 L45 110 L55 110 L55 100 C55 100 60 102 65 100 L65 110 L75 110 L75 100 C90 95 105 75 105 55 C105 35 90 10 60 10Z" fill="url(#lobster-gradient)"/>
<!-- Left Claw -->
<path d="M20 45 C5 40 0 50 5 60 C10 70 20 65 25 55 C28 48 25 45 20 45Z" fill="url(#lobster-gradient)"/>
<!-- Right Claw -->
<path d="M100 45 C115 40 120 50 115 60 C110 70 100 65 95 55 C92 48 95 45 100 45Z" fill="url(#lobster-gradient)"/>
<!-- Antenna -->
<path d="M45 15 Q35 5 30 8" stroke="#ff4d4d" stroke-width="3" stroke-linecap="round"/>
<path d="M75 15 Q85 5 90 8" stroke="#ff4d4d" stroke-width="3" stroke-linecap="round"/>
<!-- Eyes -->
<circle cx="45" cy="35" r="6" fill="#050810"/>
<circle cx="75" cy="35" r="6" fill="#050810"/>
<circle cx="46" cy="34" r="2.5" fill="#00e5cc"/>
<circle cx="76" cy="34" r="2.5" fill="#00e5cc"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1,16 @@
<svg width="13" height="13" viewBox="0 0 240 300" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<mask id="mask0" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="240" height="300">
<path d="M240 0H0V300H240V0Z" fill="white"/>
</mask>
<g mask="url(#mask0)">
<path d="M180 240H60V120H180V240Z" fill="#CFCECD"/>
<path d="M180 60H60V240H180V60ZM240 300H0V0H240V300Z" fill="#211E1E"/>
</g>
</g>
<defs>
<clipPath id="clip0">
<rect width="240" height="300" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 603 B

View file

@ -0,0 +1,16 @@
<svg width="13" height="13" viewBox="0 0 240 300" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<mask id="mask0" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="240" height="300">
<path d="M240 0H0V300H240V0Z" fill="white"/>
</mask>
<g mask="url(#mask0)">
<path d="M180 240H60V120H180V240Z" fill="#4B4646"/>
<path d="M180 60H60V240H180V60ZM240 300H0V0H240V300Z" fill="#F1ECEC"/>
</g>
</g>
<defs>
<clipPath id="clip0">
<rect width="240" height="300" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 603 B

View file

@ -0,0 +1,9 @@
<svg width="47" height="30" viewBox="0 0 148 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M71.754 16.863V2.97414C71.754 1.82355 72.6871 0.890503 73.8377 0.890503C74.9883 0.890503 75.9213 1.82355 75.9213 2.97414V16.863C75.9213 18.0136 74.9883 18.9466 73.8377 18.9466C72.6871 18.9466 71.754 18.0136 71.754 16.863Z" fill="black"/>
<path d="M82.5273 18.9329L89.4717 6.90477C90.047 5.90832 91.3215 5.5668 92.318 6.1421C93.3144 6.7174 93.6559 7.99197 93.0806 8.98841L86.1362 21.0165C85.5609 22.0129 84.2864 22.3545 83.2899 21.7792C82.2935 21.2039 81.952 19.9293 82.5273 18.9329Z" fill="black"/>
<path d="M65.1481 18.9329L58.2037 6.90477C57.6284 5.90832 56.3538 5.5668 55.3574 6.1421C54.3609 6.7174 54.0194 7.99197 54.5947 8.98841L61.5391 21.0165C62.1144 22.0129 63.389 22.3545 64.3854 21.7792C65.3819 21.2039 65.7234 19.9293 65.1481 18.9329Z" fill="black"/>
<path d="M98.5045 92.4969C95.1427 89.7602 90.8793 83.6628 89.0929 77.664C86.8598 70.1655 82.0309 62.777 80.9578 60.9138C80.0056 59.2602 83.0313 53.2605 89.0929 58.0459C93.9421 61.8742 97.3878 68.466 98.5045 71.2833C98.9566 72.4239 103.006 79.2475 101.827 56.6051C101.455 49.445 100.49 44.3126 100.036 33.9388C99.6807 25.7997 101.824 23.057 105.11 23.057C108.396 23.057 111.106 24.1244 111.106 32.4283C111.106 22.8682 113.154 20.7141 117.646 20.7147C121.39 20.7153 123.069 23.9077 123.069 27.0952V32.5084C123.069 27.0952 126.669 26.5896 128.519 26.5896C130.37 26.5896 134.076 28.1957 134.076 32.5084V43.317C134.076 38.116 137.324 36.601 139.773 36.765C142.963 36.9786 144.405 39.2382 143.965 44.3126C143.651 47.9349 142.689 58.5273 142.689 62.0291C142.689 65.1712 143.965 71.6801 142.689 77.664C141.953 81.1168 140.137 90.2628 130.885 94.4102C121.634 98.5575 105.364 98.0807 98.5045 92.4969Z" fill="#FFFF8B"/>
<path d="M140.606 62.0292C140.606 58.409 141.583 47.6748 141.89 44.1323C142.097 41.7374 141.809 40.4247 141.424 39.7542C141.141 39.2626 140.699 38.915 139.634 38.8436C138.865 38.7921 138.027 39.0114 137.401 39.5761C136.814 40.1052 136.159 41.1682 136.159 43.3176L136.155 43.4388L135.198 59.758C135.164 60.3451 134.883 60.8911 134.424 61.2599C133.966 61.6284 133.374 61.7859 132.793 61.6941L122.764 60.1068L111.948 58.6703C110.949 58.5376 110.188 57.7084 110.142 56.7016L109.545 43.8055L109.54 43.6959C109.274 38.5609 109.022 33.8767 109.022 32.4282C109.022 28.3859 108.338 26.6806 107.74 25.9634C107.263 25.3915 106.577 25.1402 105.11 25.1402C104.583 25.1402 104.212 25.2481 103.933 25.4111C103.659 25.5714 103.346 25.8587 103.049 26.4208C102.41 27.6257 101.945 29.891 102.118 33.8479C102.342 38.9804 102.692 42.8146 103.035 46.2718C103.377 49.7231 103.718 52.8561 103.908 56.4971C104.204 62.1966 104.178 66.1256 103.945 68.7924C103.828 70.124 103.656 71.1996 103.423 72.0501C103.202 72.8558 102.871 73.6757 102.296 74.2887C101.6 75.0303 100.608 75.3844 99.577 75.136C98.7592 74.9389 98.1847 74.4215 97.8706 74.0916C97.2141 73.4017 96.7501 72.5106 96.568 72.0512C95.5097 69.3812 92.2352 63.1808 87.8023 59.6811C86.5089 58.6599 85.5666 58.3652 84.9736 58.3204C84.4148 58.2783 84.0094 58.4436 83.6909 58.6967C83.34 58.9756 83.0781 59.3811 82.9479 59.7643C82.9019 59.8999 82.8823 59.9968 82.8741 60.0584C84.0759 62.0865 88.8421 69.5222 91.0896 77.069C92.7648 82.6941 96.8038 88.4259 99.8194 90.8809C102.74 93.258 107.988 94.7313 113.9 95.0218C119.756 95.3095 125.788 94.4121 130.033 92.5092C138.233 88.8334 139.903 80.7382 140.651 77.2292C141.232 74.5057 141.243 71.5987 141.087 68.9009C141.01 67.5551 140.894 66.2969 140.793 65.1373C140.695 64.0105 140.606 62.9215 140.606 62.0292ZM120.986 27.0953C120.986 25.8314 120.648 24.7049 120.089 23.9514C119.583 23.27 118.84 22.7987 117.646 22.7984C116.668 22.7982 116.011 22.9187 115.546 23.1167C115.13 23.2943 114.781 23.5699 114.463 24.0831C113.73 25.2671 113.192 27.6455 113.189 32.384L113.707 43.6021C113.901 47.3443 114.103 51.3994 114.236 54.7707L120.986 55.6666V27.0953ZM125.153 56.2652L131.172 57.218L131.992 43.267V32.5083C131.992 31.031 131.39 30.1275 130.678 29.5489C129.884 28.9039 128.957 28.6731 128.519 28.6731C127.722 28.6731 126.899 28.797 126.306 29.2179C125.849 29.5421 125.153 30.3087 125.153 32.5083V56.2652ZM136.159 35.4278C137.406 34.8069 138.74 34.6083 139.912 34.6868C142.037 34.8292 143.91 35.718 145.037 37.6779C146.06 39.4592 146.273 41.8136 146.041 44.4927C145.72 48.1949 144.772 58.6457 144.772 62.0292C144.772 62.708 144.843 63.6116 144.944 64.7758C145.042 65.907 145.165 67.2389 145.247 68.6606C145.411 71.4987 145.422 74.8383 144.727 78.0987C144.002 81.4953 142.041 91.6918 131.738 96.3108C126.731 98.5551 120.002 99.4936 113.696 99.1838C107.445 98.8767 101.128 97.3189 97.1887 94.1122C93.4809 91.0936 88.9938 84.6307 87.0962 78.2589C84.9529 71.0619 80.3109 63.9646 79.1527 61.9533C78.4706 60.7689 78.684 59.3628 79.0019 58.4258C79.3607 57.3688 80.0554 56.2631 81.0993 55.4337C82.1758 54.5784 83.6043 54.0377 85.2876 54.1647C86.9369 54.2893 88.6462 55.0393 90.3834 56.4107C94.8541 59.9401 98.1342 65.5082 99.7424 68.9231C99.759 68.7664 99.779 68.6024 99.7941 68.4298C100.003 66.0435 100.039 62.3344 99.7467 56.7132C99.5635 53.1942 99.2356 50.1809 98.8888 46.6828C98.5425 43.1904 98.184 39.2713 97.955 34.0302C97.7722 29.8481 98.2012 26.6722 99.3672 24.471C99.9716 23.3302 100.79 22.4223 101.83 21.814C102.866 21.2087 103.995 20.974 105.11 20.974C106.759 20.974 108.813 21.2062 110.448 22.7678C110.593 22.4576 110.75 22.1652 110.921 21.8899C111.676 20.6698 112.681 19.8084 113.912 19.2835C115.095 18.7791 116.378 18.6309 117.646 18.6311C120.195 18.6315 122.165 19.7565 123.435 21.4683C124.257 22.576 124.75 23.8776 124.985 25.1982C126.338 24.5876 127.691 24.5068 128.519 24.5068C129.933 24.5068 131.784 25.0791 133.305 26.3154C134.908 27.6179 136.159 29.6733 136.159 32.5083V35.4278Z" fill="black"/>
<path d="M49.258 92.4969C52.6198 89.7602 56.8832 83.6628 58.6696 77.664C60.9027 70.1655 65.7316 62.777 66.8047 60.9138C67.757 59.2602 64.7312 53.2605 58.6696 58.0459C53.8204 61.8742 50.3747 68.466 49.258 71.2833C48.8059 72.4239 44.7566 79.2475 45.935 56.6051C46.3077 49.445 47.2728 44.3126 47.7261 33.9388C48.0818 25.7997 45.9381 23.057 42.6523 23.057C39.3666 23.057 36.6568 24.1244 36.6568 32.4283C36.6568 22.8682 34.6087 20.7141 30.1168 20.7147C26.3728 20.7153 24.6934 23.9077 24.6934 27.0952V32.5084C24.6934 27.0952 21.0938 26.5896 19.243 26.5896C17.3923 26.5896 13.687 28.1957 13.687 32.5084V43.317C13.687 38.116 10.4388 36.601 7.98968 36.765C4.79943 36.9786 3.3574 39.2382 3.79721 44.3126C4.11115 47.9349 5.07331 58.5273 5.07331 62.0291C5.07331 65.1712 3.79721 71.6801 5.07331 77.664C5.80964 81.1168 7.62551 90.2628 16.8772 94.4102C26.129 98.5575 42.3987 98.0807 49.258 92.4969Z" fill="#FFFF8B"/>
<path d="M7.15667 62.0292C7.15667 58.409 6.18001 47.6748 5.87297 44.1323C5.66546 41.7374 5.95363 40.4247 6.33881 39.7542C6.62122 39.2626 7.06342 38.915 8.1284 38.8436C8.89765 38.7921 9.7355 39.0114 10.3617 39.5761C10.9484 40.1052 11.6032 41.1682 11.6032 43.3176L11.6075 43.4388L12.5644 59.758C12.5989 60.3451 12.8798 60.8911 13.338 61.2599C13.7961 61.6284 14.3888 61.7859 14.9695 61.6941L24.9988 60.1068L35.8144 58.6703C36.8136 58.5376 37.5741 57.7084 37.6208 56.7016L38.2174 43.8055L38.2226 43.6959C38.4887 38.5609 38.7401 33.8767 38.7401 32.4282C38.7401 28.3859 39.4246 26.6806 40.0228 25.9634C40.4997 25.3915 41.1851 25.1402 42.6523 25.1402C43.1795 25.1402 43.5506 25.2481 43.8296 25.4111C44.1038 25.5714 44.416 25.8587 44.7139 26.4208C45.3521 27.6257 45.8174 29.891 45.6445 33.8479C45.4202 38.9804 45.0703 42.8146 44.7276 46.2718C44.3854 49.7231 44.0444 52.8561 43.8549 56.4971C43.5583 62.1966 43.5848 66.1256 43.818 68.7924C43.9345 70.124 44.107 71.1996 44.3397 72.0501C44.5602 72.8558 44.891 73.6757 45.4664 74.2887C46.1626 75.0303 47.1547 75.3844 48.1855 75.136C49.0033 74.9389 49.5779 74.4215 49.8919 74.0916C50.5484 73.4017 51.0124 72.5106 51.1945 72.0512C52.2528 69.3812 55.5273 63.1808 59.9602 59.6811C61.2536 58.6599 62.1959 58.3652 62.7889 58.3204C63.3477 58.2783 63.7531 58.4436 64.0716 58.6967C64.4225 58.9756 64.6844 59.3811 64.8146 59.7643C64.8606 59.8999 64.8802 59.9968 64.8884 60.0584C63.6866 62.0865 58.9205 69.5222 56.6729 77.069C54.9978 82.6941 50.9587 88.4259 47.9431 90.8809C45.0229 93.258 39.7748 94.7313 33.8625 95.0218C28.0069 95.3095 21.9748 94.4121 17.7298 92.5092C9.52994 88.8334 7.85968 80.7382 7.11135 77.2292C6.5306 74.5057 6.51956 71.5987 6.67502 68.9009C6.75257 67.5551 6.86815 66.2969 6.96907 65.1373C7.06713 64.0105 7.15666 62.9215 7.15667 62.0292ZM26.7768 27.0953C26.7768 25.8314 27.1148 24.7049 27.6737 23.9514C28.1793 23.27 28.9221 22.7987 30.1168 22.7984C31.0942 22.7982 31.7519 22.9187 32.2162 23.1167C32.6327 23.2943 32.9818 23.5699 33.2997 24.0831C34.0329 25.2671 34.5706 27.6455 34.5739 32.384L34.0554 43.6021C33.8615 47.3443 33.6592 51.3994 33.5263 54.7707L26.7768 55.6666V27.0953ZM22.6096 56.2652L16.5905 57.218L15.7705 43.267V32.5083C15.7705 31.031 16.3726 30.1275 17.0848 29.5489C17.8786 28.9039 18.8059 28.6731 19.2433 28.6731C20.0405 28.6731 20.8635 28.797 21.4565 29.2179C21.9131 29.5421 22.6095 30.3087 22.6096 32.5083V56.2652ZM11.6032 35.4278C10.3568 34.8069 9.02271 34.6083 7.85016 34.6868C5.72547 34.8292 3.85203 35.718 2.7259 37.6779C1.70253 39.4592 1.4893 41.8136 1.7215 44.4927C2.04237 48.1949 2.99044 58.6457 2.99044 62.0292C2.99043 62.708 2.91997 63.6116 2.81865 64.7758C2.7202 65.907 2.59705 67.2389 2.51511 68.6606C2.35156 71.4987 2.34047 74.8383 3.03576 78.0987C3.76011 81.4953 5.7216 91.6918 16.0245 96.3108C21.0312 98.5551 27.7601 99.4936 34.0669 99.1838C40.3173 98.8767 46.6346 97.3189 50.5738 94.1122C54.2816 91.0936 58.7687 84.6307 60.6663 78.2589C62.8096 71.0619 67.4516 63.9646 68.6099 61.9533C69.292 60.7689 69.0785 59.3628 68.7606 58.4258C68.4018 57.3688 67.7071 56.2631 66.6632 55.4337C65.5867 54.5784 64.1582 54.0377 62.4749 54.1647C60.8256 54.2893 59.1163 55.0393 57.3791 56.4107C52.9084 59.9401 49.6283 65.5082 48.0201 68.9231C48.0035 68.7664 47.9835 68.6024 47.9684 68.4298C47.7597 66.0435 47.7233 62.3344 48.0159 56.7132C48.199 53.1942 48.5269 50.1809 48.8738 46.6828C49.22 43.1904 49.5785 39.2713 49.8076 34.0302C49.9903 29.8481 49.5613 26.6722 48.3953 24.471C47.7909 23.3302 46.9729 22.4223 45.9322 21.814C44.8964 21.2087 43.7676 20.974 42.6523 20.974C41.0038 20.974 38.9498 21.2062 37.3141 22.7678C37.1699 22.4576 37.0125 22.1652 36.842 21.8899C36.0864 20.6698 35.0817 19.8084 33.8509 19.2835C32.668 18.7791 31.3849 18.6309 30.1168 18.6311C27.5676 18.6315 25.5976 19.7565 24.3275 21.4683C23.5057 22.576 23.0121 23.8776 22.7771 25.1982C21.4248 24.5876 20.0718 24.5068 19.2433 24.5068C17.8299 24.5068 15.9789 25.0791 14.4573 26.3154C12.8543 27.6179 11.6033 29.6733 11.6032 32.5083V35.4278Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 10 KiB

View file

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 1H11.7692V7.9999H8.17942V11.4999H4.58982V15H1V1ZM4.58982 4.50005V7.9999H8.17942V4.50005H4.58982Z" fill="currentColor"/>
<path d="M11.7692 7.46154H15V15H11.7692V7.46154Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 307 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path fill="currentColor" d="M8 1C4.134 1 1 4.134 1 8s3.134 7 7 7c1.5 0 2.9-.47 4.05-1.28l1.12 1.12a.75.75 0 1 0 1.06-1.06l-1.12-1.12A6.97 6.97 0 0 0 15 8c0-3.866-3.134-7-7-7Zm0 2c2.761 0 5 2.239 5 5s-2.239 5-5 5-5-2.239-5-5 2.239-5 5-5Z"/>
</svg>

After

Width:  |  Height:  |  Size: 334 B

View file

Before

Width:  |  Height:  |  Size: 713 B

After

Width:  |  Height:  |  Size: 713 B

Before After
Before After

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path fill="currentColor" d="M6.53 4.412h5.883v3.587H9.471v3.588H3.588V7.999H6.53Z"/>
</svg>

After

Width:  |  Height:  |  Size: 179 B

View file

@ -0,0 +1,4 @@
<svg fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M2.75 6.25h2.37l2.02 6.61 2.04-6.61h2.34l-3.4 10.5H6.18z"/>
<path d="M12.5 6.25h8.75v2.06h-3.31v8.44h-2.13V8.31H12.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 222 B

View file

@ -191,7 +191,18 @@ class AgentServiceIntegrationTest : IntegrationTest() {
contentManager.createNewAgentTab(
AgentSession(
sessionId = sessionId,
conversation = Conversation()
conversation = Conversation(),
displayName = "",
serviceType = null,
externalAgentId = null,
externalAgentSessionId = null,
externalAgentConfigOptions = emptyList(),
externalAgentConfigLoading = false,
runtimeAgentId = null,
resumeCheckpointRef = null,
referencedFiles = emptyList(),
createdAt = System.currentTimeMillis(),
lastActiveAt = System.currentTimeMillis()
),
select = false
)