mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
IJPL-233558 fix after merge
GitOrigin-RevId: 43768e934687f8c2439341b6df14b8e698a1ab23
This commit is contained in:
committed by
intellij-monorepo-bot
parent
6396c310d6
commit
2fb1cd6085
@@ -283,7 +283,7 @@ class CodexAppServerClient(
|
||||
val configuredExecutable = executablePathProvider()
|
||||
?.trim()
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
val executable = configuredExecutable ?: CODEX_COMMAND
|
||||
val executable = configuredExecutable ?: CodexCliUtils.CODEX_COMMAND
|
||||
val process = try {
|
||||
GeneralCommandLine(executable, "app-server")
|
||||
.withParentEnvironmentType(GeneralCommandLine.ParentEnvironmentType.CONSOLE)
|
||||
@@ -295,7 +295,6 @@ class CodexAppServerClient(
|
||||
}
|
||||
}
|
||||
.createProcess()
|
||||
.createProcess()
|
||||
}
|
||||
catch (t: Throwable) {
|
||||
if (configuredExecutable == null && isExecutableNotFound(t)) {
|
||||
|
||||
@@ -82,4 +82,4 @@ jps_test(
|
||||
name = "sessions_test",
|
||||
runtime_deps = [":sessions_test_lib"]
|
||||
)
|
||||
### auto-generated section `test intellij.agent.workbench.sessions` end
|
||||
### auto-generated section `test intellij.agent.workbench.sessions` end
|
||||
@@ -41,7 +41,6 @@ import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.nio.file.InvalidPathException
|
||||
import java.nio.file.Path
|
||||
import java.util.UUID
|
||||
import kotlin.io.path.invariantSeparatorsPathString
|
||||
import kotlin.io.path.name
|
||||
|
||||
@@ -49,7 +48,7 @@ private val LOG = logger<AgentSessionsService>()
|
||||
|
||||
private const val SUPPRESS_BRANCH_MISMATCH_DIALOG_KEY = "agent.workbench.suppress.branch.mismatch.dialog"
|
||||
private const val OPEN_PROJECT_ACTION_KEY_PREFIX = "project-open"
|
||||
private const val CREATE_THREAD_ACTION_KEY_PREFIX = "thread-create"
|
||||
private const val CREATE_SESSION_ACTION_KEY_PREFIX = "session-create"
|
||||
private const val OPEN_THREAD_ACTION_KEY_PREFIX = "thread-open"
|
||||
private const val OPEN_SUB_AGENT_ACTION_KEY_PREFIX = "subagent-open"
|
||||
|
||||
@@ -337,26 +336,6 @@ internal class AgentSessionsService private constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun createAndOpenThread(path: String, currentProject: Project? = null) {
|
||||
val normalized = normalizePath(path)
|
||||
val key = buildCreateThreadActionKey(normalized)
|
||||
actionGate.launch(
|
||||
scope = serviceScope,
|
||||
key = key,
|
||||
policy = SingleFlightPolicy.DROP,
|
||||
progress = dedicatedFrameOpenProgressRequest(currentProject),
|
||||
onDrop = { LOG.debug("Dropped duplicate create thread action for $normalized") },
|
||||
) {
|
||||
val freshThread = createFreshCodexThread()
|
||||
openChat(
|
||||
path = normalized,
|
||||
thread = freshThread,
|
||||
subAgent = null,
|
||||
shellCommandOverride = buildAgentSessionNewCommand(AgentSessionProvider.CODEX),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun showMoreProjects() {
|
||||
mutableState.update { it.copy(visibleProjectCount = it.visibleProjectCount + DEFAULT_VISIBLE_PROJECT_COUNT) }
|
||||
}
|
||||
@@ -430,9 +409,23 @@ internal class AgentSessionsService private constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun createNewSession(path: String, provider: AgentSessionProvider, yolo: Boolean = false) {
|
||||
serviceScope.launch {
|
||||
val normalized = normalizePath(path)
|
||||
fun createNewSession(
|
||||
path: String,
|
||||
provider: AgentSessionProvider,
|
||||
yolo: Boolean = false,
|
||||
currentProject: Project? = null,
|
||||
) {
|
||||
val normalized = normalizePath(path)
|
||||
val key = buildCreateSessionActionKey(normalized, provider, yolo)
|
||||
actionGate.launch(
|
||||
scope = serviceScope,
|
||||
key = key,
|
||||
policy = SingleFlightPolicy.DROP,
|
||||
progress = dedicatedFrameOpenProgressRequest(currentProject),
|
||||
onDrop = {
|
||||
LOG.debug("Dropped duplicate create session action for $normalized:$provider:yolo=$yolo")
|
||||
},
|
||||
) {
|
||||
service<AgentSessionsTreeUiStateService>().setLastUsedProvider(provider)
|
||||
|
||||
val identity: String
|
||||
@@ -639,8 +632,8 @@ internal class AgentSessionsService private constructor(
|
||||
return "$OPEN_PROJECT_ACTION_KEY_PREFIX:$path"
|
||||
}
|
||||
|
||||
private fun buildCreateThreadActionKey(path: String): String {
|
||||
return "$CREATE_THREAD_ACTION_KEY_PREFIX:$path"
|
||||
private fun buildCreateSessionActionKey(path: String, provider: AgentSessionProvider, yolo: Boolean): String {
|
||||
return "$CREATE_SESSION_ACTION_KEY_PREFIX:$path:$provider:yolo=$yolo"
|
||||
}
|
||||
|
||||
private fun buildOpenThreadActionKey(path: String, thread: AgentSessionThread): String {
|
||||
@@ -1126,17 +1119,6 @@ internal class AgentSessionsService private constructor(
|
||||
return null
|
||||
}
|
||||
|
||||
private fun createFreshCodexThread(): AgentSessionThread {
|
||||
return AgentSessionThread(
|
||||
id = "new-${UUID.randomUUID()}",
|
||||
title = AgentSessionsBundle.message("toolwindow.provider.codex"),
|
||||
updatedAt = System.currentTimeMillis(),
|
||||
archived = false,
|
||||
activity = AgentSessionActivity.PROCESSING,
|
||||
provider = AgentSessionProvider.CODEX,
|
||||
)
|
||||
}
|
||||
|
||||
internal data class ProjectEntry(
|
||||
val path: String,
|
||||
val name: String,
|
||||
|
||||
@@ -50,15 +50,14 @@ internal fun agentSessionsToolWindow(currentProject: Project) {
|
||||
state = state,
|
||||
onRefresh = { service.refresh() },
|
||||
onOpenProject = { service.openOrFocusProject(it) },
|
||||
onCreateThread = { path -> service.createAndOpenThread(path, currentProject) },
|
||||
onProjectExpanded = { service.loadProjectThreadsOnDemand(it) },
|
||||
onWorktreeExpanded = { projectPath, worktreePath ->
|
||||
service.loadWorktreeThreadsOnDemand(projectPath, worktreePath)
|
||||
},
|
||||
onOpenThread = { path, thread -> service.openChatThread(path, thread, currentProject) },
|
||||
onOpenSubAgent = { path, thread, subAgent -> service.openChatSubAgent(path, thread, subAgent, currentProject) },
|
||||
onCreateSession = { path, provider, yolo -> service.createNewSession(path, provider, yolo) },
|
||||
lastUsedProvider = lastUsedProvider,
|
||||
onOpenThread = { path, thread -> service.openChatThread(path, thread, currentProject) },
|
||||
onOpenSubAgent = { path, thread, subAgent -> service.openChatSubAgent(path, thread, subAgent, currentProject) },
|
||||
onCreateSession = { path, provider, yolo -> service.createNewSession(path, provider, yolo, currentProject) },
|
||||
lastUsedProvider = lastUsedProvider,
|
||||
visibleProjectCount = state.visibleProjectCount,
|
||||
onShowMoreProjects = { service.showMoreProjects() },
|
||||
visibleThreadCounts = state.visibleThreadCounts,
|
||||
@@ -72,7 +71,6 @@ internal fun agentSessionsToolWindowContent(
|
||||
state: AgentSessionsState,
|
||||
onRefresh: () -> Unit,
|
||||
onOpenProject: (String) -> Unit,
|
||||
onCreateThread: (String) -> Unit = {},
|
||||
onProjectExpanded: (String) -> Unit = {},
|
||||
onWorktreeExpanded: (String, String) -> Unit = { _, _ -> },
|
||||
onOpenThread: (String, AgentSessionThread) -> Unit = { _, _ -> },
|
||||
@@ -98,7 +96,6 @@ internal fun agentSessionsToolWindowContent(
|
||||
projects = state.projects,
|
||||
onRefresh = onRefresh,
|
||||
onOpenProject = onOpenProject,
|
||||
onCreateThread = onCreateThread,
|
||||
onProjectExpanded = onProjectExpanded,
|
||||
onWorktreeExpanded = onWorktreeExpanded,
|
||||
onOpenThread = onOpenThread,
|
||||
|
||||
@@ -4,10 +4,7 @@ import androidx.compose.foundation.focusable
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.intellij.agent.workbench.chat.AgentChatTabSelection
|
||||
import org.jetbrains.jewel.foundation.ExperimentalJewelApi
|
||||
@@ -21,15 +18,12 @@ import org.jetbrains.jewel.ui.component.styling.LazyTreeMetrics
|
||||
import org.jetbrains.jewel.ui.component.styling.LazyTreeStyle
|
||||
import org.jetbrains.jewel.ui.theme.treeStyle
|
||||
|
||||
private const val PROJECT_CLICK_SUPPRESSION_MS = 500L
|
||||
|
||||
@OptIn(ExperimentalJewelApi::class)
|
||||
@Composable
|
||||
internal fun sessionTree(
|
||||
projects: List<AgentProjectSessions>,
|
||||
onRefresh: () -> Unit,
|
||||
onOpenProject: (String) -> Unit,
|
||||
onCreateThread: (String) -> Unit,
|
||||
onProjectExpanded: (String) -> Unit,
|
||||
onWorktreeExpanded: (String, String) -> Unit = { _, _ -> },
|
||||
onOpenThread: (String, AgentSessionThread) -> Unit,
|
||||
@@ -78,27 +72,6 @@ internal fun sessionTree(
|
||||
val tree = remember(projects, visibleProjectCount, visibleThreadCounts) {
|
||||
buildSessionTree(projects, visibleProjectCount, visibleThreadCounts)
|
||||
}
|
||||
var suppressedProjectClick by remember { mutableStateOf<SuppressedProjectClick?>(null) }
|
||||
|
||||
fun suppressNextProjectClick(path: String) {
|
||||
suppressedProjectClick = SuppressedProjectClick(
|
||||
path = path,
|
||||
expiresAt = System.currentTimeMillis() + PROJECT_CLICK_SUPPRESSION_MS,
|
||||
)
|
||||
}
|
||||
|
||||
fun shouldSuppressProjectClick(path: String): Boolean {
|
||||
val suppression = suppressedProjectClick ?: return false
|
||||
if (System.currentTimeMillis() > suppression.expiresAt) {
|
||||
suppressedProjectClick = null
|
||||
return false
|
||||
}
|
||||
if (suppression.path != path) {
|
||||
return false
|
||||
}
|
||||
suppressedProjectClick = null
|
||||
return true
|
||||
}
|
||||
|
||||
val treeStyle = run {
|
||||
val baseStyle = JewelTheme.treeStyle
|
||||
@@ -127,9 +100,7 @@ internal fun sessionTree(
|
||||
onElementClick = { element ->
|
||||
when (val node = element.data) {
|
||||
is SessionTreeNode.Project -> {
|
||||
if (!shouldSuppressProjectClick(node.project.path)) {
|
||||
onOpenProject(node.project.path)
|
||||
}
|
||||
onOpenProject(node.project.path)
|
||||
}
|
||||
is SessionTreeNode.Thread -> {
|
||||
val path = when (val id = element.id) {
|
||||
@@ -166,10 +137,6 @@ internal fun sessionTree(
|
||||
sessionTreeNodeContent(
|
||||
element = element,
|
||||
onOpenProject = onOpenProject,
|
||||
onCreateThread = { path ->
|
||||
suppressNextProjectClick(path)
|
||||
onCreateThread(path)
|
||||
},
|
||||
onRefresh = onRefresh,
|
||||
onCreateSession = onCreateSession,
|
||||
lastUsedProvider = lastUsedProvider,
|
||||
@@ -179,11 +146,6 @@ internal fun sessionTree(
|
||||
}
|
||||
}
|
||||
|
||||
private data class SuppressedProjectClick(
|
||||
val path: String,
|
||||
val expiresAt: Long,
|
||||
)
|
||||
|
||||
private fun buildSessionTree(
|
||||
projects: List<AgentProjectSessions>,
|
||||
visibleProjectCount: Int,
|
||||
|
||||
@@ -3,7 +3,6 @@ package com.intellij.agent.workbench.sessions
|
||||
import androidx.compose.foundation.ContextMenuArea
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.hoverable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsHoveredAsState
|
||||
@@ -25,8 +24,6 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.graphics.takeOrElse
|
||||
import androidx.compose.ui.input.pointer.PointerIcon
|
||||
import androidx.compose.ui.input.pointer.pointerHoverIcon
|
||||
import androidx.compose.ui.text.TextLayoutResult
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
@@ -37,20 +34,17 @@ import org.jetbrains.jewel.foundation.theme.JewelTheme
|
||||
import org.jetbrains.jewel.foundation.theme.LocalContentColor
|
||||
import org.jetbrains.jewel.ui.component.CircularProgressIndicator
|
||||
import org.jetbrains.jewel.ui.component.ContextMenuItemOption
|
||||
import org.jetbrains.jewel.ui.component.Icon
|
||||
import org.jetbrains.jewel.ui.component.OutlinedButton
|
||||
import org.jetbrains.jewel.ui.component.Text
|
||||
import org.jetbrains.jewel.ui.component.Tooltip
|
||||
import org.jetbrains.jewel.ui.component.search.highlightSpeedSearchMatches
|
||||
import org.jetbrains.jewel.ui.component.search.highlightTextSearch
|
||||
import org.jetbrains.jewel.ui.icons.AllIconsKeys
|
||||
|
||||
@OptIn(ExperimentalJewelApi::class)
|
||||
@Composable
|
||||
internal fun SelectableLazyItemScope.sessionTreeNodeContent(
|
||||
element: Tree.Element<SessionTreeNode>,
|
||||
onOpenProject: (String) -> Unit,
|
||||
onCreateThread: (String) -> Unit,
|
||||
onRefresh: () -> Unit,
|
||||
onCreateSession: (String, AgentSessionProvider, Boolean) -> Unit = { _, _, _ -> },
|
||||
lastUsedProvider: AgentSessionProvider? = null,
|
||||
@@ -61,7 +55,6 @@ internal fun SelectableLazyItemScope.sessionTreeNodeContent(
|
||||
is SessionTreeNode.Project -> projectNodeRow(
|
||||
project = node.project,
|
||||
onOpenProject = onOpenProject,
|
||||
onCreateThread = onCreateThread,
|
||||
onCreateSession = onCreateSession,
|
||||
lastUsedProvider = lastUsedProvider,
|
||||
)
|
||||
@@ -138,8 +131,6 @@ private fun rememberTreeRowChrome(
|
||||
private fun SelectableLazyItemScope.projectNodeRow(
|
||||
project: AgentProjectSessions,
|
||||
onOpenProject: (String) -> Unit,
|
||||
onCreateThread: (String) -> Unit,
|
||||
onCreateThread: (String) -> Unit,
|
||||
onCreateSession: (String, AgentSessionProvider, Boolean) -> Unit,
|
||||
lastUsedProvider: AgentSessionProvider?,
|
||||
) {
|
||||
@@ -203,14 +194,7 @@ private fun SelectableLazyItemScope.projectNodeRow(
|
||||
NewSessionHoverActions(
|
||||
path = project.path,
|
||||
lastUsedProvider = lastUsedProvider,
|
||||
onCreateSession = { path, provider, yolo ->
|
||||
if (provider == AgentSessionProvider.CODEX && !yolo && lastUsedProvider == null) {
|
||||
onCreateThread(path)
|
||||
}
|
||||
else {
|
||||
onCreateSession(path, provider, yolo)
|
||||
}
|
||||
},
|
||||
onCreateSession = onCreateSession,
|
||||
popupVisible = newSessionPopupVisible,
|
||||
onPopupVisibleChange = { newSessionPopupVisible = it },
|
||||
)
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.agent.workbench.sessions.providers.codex
|
||||
|
||||
import com.intellij.agent.workbench.codex.common.CodexAppServerClient
|
||||
import com.intellij.agent.workbench.codex.common.CodexAppServerException
|
||||
import com.intellij.agent.workbench.sessions.codex.CodexProjectSessionService
|
||||
import com.intellij.agent.workbench.sessions.codex.resolveProjectDirectoryFromPath
|
||||
import com.intellij.openapi.project.Project
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
@Suppress("unused")
|
||||
internal class CodexAppServerSessionBackend(
|
||||
private val coroutineScope: CoroutineScope,
|
||||
) : CodexSessionBackend {
|
||||
override suspend fun listThreads(path: String, openProject: Project?): List<CodexBackendThread> {
|
||||
if (openProject != null) {
|
||||
return listThreadsFromOpenProject(openProject)
|
||||
}
|
||||
return listThreadsFromClosedPath(path)
|
||||
}
|
||||
|
||||
private suspend fun listThreadsFromOpenProject(project: Project): List<CodexBackendThread> {
|
||||
val service = project.getService(CodexProjectSessionService::class.java)
|
||||
if (service == null || !service.hasWorkingDirectory()) {
|
||||
throw CodexAppServerException("Project directory is not available")
|
||||
}
|
||||
return service.listThreads().map(::CodexBackendThread)
|
||||
}
|
||||
|
||||
private suspend fun listThreadsFromClosedPath(path: String): List<CodexBackendThread> {
|
||||
val workingDirectory = resolveProjectDirectoryFromPath(path)
|
||||
?: return emptyList()
|
||||
val client = CodexAppServerClient(coroutineScope = coroutineScope, workingDirectory = workingDirectory)
|
||||
try {
|
||||
return client.listThreads(archived = false).map(::CodexBackendThread)
|
||||
}
|
||||
finally {
|
||||
client.shutdown()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -100,8 +100,10 @@ class AgentSessionsToolWindowTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun hoveringProjectRowShowsNewThreadActionAndDoesNotInvokeOpenCallback() {
|
||||
var createdPath: String? = null
|
||||
fun hoveringProjectRowShowsQuickCreateSessionActionAndDoesNotInvokeOpenCallback() {
|
||||
var createdSessionPath: String? = null
|
||||
var createdSessionProvider: AgentSessionProvider? = null
|
||||
var createdSessionYolo: Boolean? = null
|
||||
var openedPath: String? = null
|
||||
val projectPath = "/work/project-plus"
|
||||
val projects = listOf(
|
||||
@@ -117,23 +119,30 @@ class AgentSessionsToolWindowTest {
|
||||
state = AgentSessionsState(projects = projects),
|
||||
onRefresh = {},
|
||||
onOpenProject = { openedPath = it },
|
||||
onCreateThread = { createdPath = it },
|
||||
onCreateSession = { path, provider, yolo ->
|
||||
createdSessionPath = path
|
||||
createdSessionProvider = provider
|
||||
createdSessionYolo = yolo
|
||||
},
|
||||
lastUsedProvider = AgentSessionProvider.CLAUDE,
|
||||
)
|
||||
}
|
||||
|
||||
val newThreadLabel = AgentSessionsBundle.message("toolwindow.action.new.thread")
|
||||
composeRule.onAllNodesWithContentDescription(newThreadLabel).assertCountEquals(0)
|
||||
val quickActionLabel = AgentSessionsBundle.message("toolwindow.provider.claude")
|
||||
composeRule.onAllNodesWithContentDescription(quickActionLabel).assertCountEquals(0)
|
||||
|
||||
composeRule.onNodeWithText("Project Plus")
|
||||
.assertIsDisplayed()
|
||||
.performMouseInput { moveTo(center) }
|
||||
|
||||
composeRule.onNodeWithContentDescription(newThreadLabel)
|
||||
composeRule.onNodeWithContentDescription(quickActionLabel)
|
||||
.assertIsDisplayed()
|
||||
.performClick()
|
||||
|
||||
composeRule.runOnIdle {
|
||||
assertThat(createdPath).isEqualTo(projectPath)
|
||||
assertThat(createdSessionPath).isEqualTo(projectPath)
|
||||
assertThat(createdSessionProvider).isEqualTo(AgentSessionProvider.CLAUDE)
|
||||
assertThat(createdSessionYolo).isFalse()
|
||||
assertThat(openedPath).isNull()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,10 +215,21 @@ private fun resolveTestClasspath(): String {
|
||||
.map(String::trim)
|
||||
.filter(String::isNotEmpty)
|
||||
if (entries.isNotEmpty()) {
|
||||
return entries.joinToString(File.pathSeparator)
|
||||
return absolutizeClasspathEntries(entries)
|
||||
}
|
||||
}
|
||||
return System.getProperty("java.class.path")
|
||||
val classpath = System.getProperty("java.class.path")
|
||||
val entries = classpath.split(File.pathSeparator)
|
||||
.map(String::trim)
|
||||
.filter(String::isNotEmpty)
|
||||
return absolutizeClasspathEntries(entries)
|
||||
}
|
||||
|
||||
private fun absolutizeClasspathEntries(entries: List<String>): String {
|
||||
return entries.joinToString(File.pathSeparator) { entry ->
|
||||
val path = Path.of(entry)
|
||||
if (path.isAbsolute) entry else path.toAbsolutePath().normalize().toString()
|
||||
}
|
||||
}
|
||||
|
||||
private fun writeAppServerArgsFile(tempDir: Path, classpath: String, configPath: Path): Path {
|
||||
|
||||
@@ -20,11 +20,17 @@ import org.jetbrains.jewel.foundation.theme.ThemeDefinition
|
||||
import org.jetbrains.jewel.foundation.theme.ThemeIconData
|
||||
import org.jetbrains.jewel.intui.standalone.theme.default
|
||||
import org.jetbrains.jewel.ui.ComponentStyling
|
||||
import org.jetbrains.jewel.ui.LocalMenuItemShortcutHintProvider
|
||||
import org.jetbrains.jewel.ui.LocalMenuItemShortcutProvider
|
||||
import org.jetbrains.jewel.ui.LocalTypography
|
||||
import org.jetbrains.jewel.ui.MenuItemShortcutHintProvider
|
||||
import org.jetbrains.jewel.ui.MenuItemShortcutProvider
|
||||
import org.jetbrains.jewel.ui.Typography
|
||||
import org.jetbrains.jewel.ui.component.ContextMenuItemOptionAction
|
||||
import org.jetbrains.jewel.ui.icon.LocalNewUiChecker
|
||||
import org.jetbrains.jewel.ui.icon.NewUiChecker
|
||||
import org.jetbrains.jewel.ui.theme.BaseJewelTheme
|
||||
import javax.swing.KeyStroke
|
||||
|
||||
internal fun ComposeContentTestRule.setContentWithTheme(content: @Composable () -> Unit) {
|
||||
setContent {
|
||||
@@ -32,6 +38,8 @@ internal fun ComposeContentTestRule.setContentWithTheme(content: @Composable ()
|
||||
CompositionLocalProvider(
|
||||
LocalTypography provides TestTypography,
|
||||
LocalNewUiChecker provides TestNewUiChecker,
|
||||
LocalMenuItemShortcutProvider provides EmptyMenuItemShortcutProvider,
|
||||
LocalMenuItemShortcutHintProvider provides EmptyMenuItemShortcutHintProvider,
|
||||
) {
|
||||
content()
|
||||
}
|
||||
@@ -131,3 +139,11 @@ private object TestTypography : Typography {
|
||||
private object TestNewUiChecker : NewUiChecker {
|
||||
override fun isNewUi(): Boolean = true
|
||||
}
|
||||
|
||||
private object EmptyMenuItemShortcutProvider : MenuItemShortcutProvider {
|
||||
override fun getShortcutKeyStroke(actionType: ContextMenuItemOptionAction): KeyStroke? = null
|
||||
}
|
||||
|
||||
private object EmptyMenuItemShortcutHintProvider : MenuItemShortcutHintProvider {
|
||||
override fun getShortcutHint(actionType: ContextMenuItemOptionAction): String = ""
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
---
|
||||
name: Agent Sessions New-Session Actions
|
||||
description: Requirements for project/worktree new-session actions and provider-specific new-session routing in Agent Threads.
|
||||
targets:
|
||||
- ../../sessions/src/SessionTreeNewSessionActions.kt
|
||||
- ../../sessions/src/SessionTreeRows.kt
|
||||
- ../../sessions/src/AgentSessionsToolWindow.kt
|
||||
- ../../sessions/src/AgentSessionsService.kt
|
||||
- ../../sessions/src/codex/SharedCodexAppServerService.kt
|
||||
- ../../sessions/src/AgentSessionCli.kt
|
||||
- ../../sessions/src/providers/codex/CodexCliCommands.kt
|
||||
- ../../sessions/resources/messages/AgentSessionsBundle.properties
|
||||
- ../../sessions/testSrc/AgentSessionsToolWindowTest.kt
|
||||
- ../../sessions/testSrc/AgentSessionCliTest.kt
|
||||
- ../../sessions/testSrc/CodexAppServerClientTest.kt
|
||||
---
|
||||
|
||||
# Agent Sessions New-Session Actions
|
||||
|
||||
Status: Draft
|
||||
Date: 2026-02-13
|
||||
|
||||
## Summary
|
||||
Define project/worktree `New Session` actions (`+` and quick-provider icon) and map each provider + mode to the chat-open flow.
|
||||
|
||||
## Goals
|
||||
- Keep session-creation behavior identical across project and worktree rows.
|
||||
- Keep provider/mode semantics explicit and testable.
|
||||
- Avoid reintroducing legacy Codex "fresh session without thread id" flow.
|
||||
|
||||
## Non-goals
|
||||
- Thread loading/sorting/paging behavior.
|
||||
- Dedicated-frame routing rules outside new-session open progress integration.
|
||||
- New user-facing warning text for `Codex (Full Auto)`.
|
||||
|
||||
## Requirements
|
||||
- Project/worktree row hover actions must route through `onCreateSession(path, provider, yolo)`; no separate create-thread callback path may be used.
|
||||
[@test] ../../sessions/testSrc/AgentSessionsToolWindowTest.kt
|
||||
- Quick provider icon (when `lastUsedProvider` exists) must create a session with `yolo=false`.
|
||||
- Popup must expose four selectable entries: Claude, Codex, Claude YOLO, Codex YOLO.
|
||||
- Popup section label remains `YOLO`; Codex YOLO entry label remains `Codex (Full Auto)`.
|
||||
|
||||
- `AgentSessionsService.createNewSession(path, provider, yolo, currentProject)` is the service entry point for row actions.
|
||||
- `createNewSession` must deduplicate in-flight actions by normalized `path + provider + yolo` with single-flight `DROP`.
|
||||
- `createNewSession` must set `lastUsedProvider` to the selected provider before opening chat.
|
||||
|
||||
- Claude `yolo=false` new-session command must be `claude`.
|
||||
- Claude `yolo=true` new-session command must be `claude --dangerously-skip-permissions`.
|
||||
- Codex new-session must not use direct `codex`/`--full-auto` command construction for session creation.
|
||||
[@test] ../../sessions/testSrc/AgentSessionCliTest.kt
|
||||
|
||||
- Codex new-session must call app-server `thread/start`, persist the new thread id, then open chat with `codex resume <threadId>`.
|
||||
- Codex `yolo=true` must call `thread/start` with `approvalPolicy="on-request"` and `sandbox="workspace-write"`.
|
||||
- Codex `yolo=false` must call `thread/start` with default app-server parameters (no forced approval/sandbox overrides).
|
||||
[@test] ../../sessions/testSrc/CodexAppServerClientTest.kt
|
||||
|
||||
- `Codex (Full Auto)` semantics are documented in spec and do not require extra warning text in UI.
|
||||
|
||||
## User Experience
|
||||
- Hovering a project/worktree row reveals new-session controls without opening the project.
|
||||
- The quick-provider icon provides one-click repeat creation.
|
||||
- Popup grouping keeps normal and YOLO choices explicit.
|
||||
|
||||
## Data & Backend
|
||||
- Codex action flow uses `SharedCodexAppServerService.createThread(cwd, yolo)` and persists created threads before resume.
|
||||
- Codex new-session identity is concrete (`CODEX:<threadId>`), not synthetic `new-*`.
|
||||
|
||||
## Error Handling
|
||||
- Provider CLI/app-server failures must continue using provider-specific error paths in existing service flow.
|
||||
- Duplicate clicks on the same action tuple must be dropped rather than opening multiple sessions.
|
||||
|
||||
## Testing / Local Run
|
||||
- `./tests.cmd '-Dintellij.build.test.patterns=com.intellij.agent.workbench.sessions.AgentSessionsToolWindowTest'`
|
||||
- `./tests.cmd '-Dintellij.build.test.patterns=com.intellij.agent.workbench.sessions.AgentSessionCliTest'`
|
||||
- `./tests.cmd '-Dintellij.build.test.patterns=com.intellij.agent.workbench.sessions.CodexAppServerClientTest'`
|
||||
|
||||
## Open Questions / Risks
|
||||
- `createNewSession` dedup + yolo parameter mapping currently has limited direct service-level test coverage.
|
||||
|
||||
## References
|
||||
- `../agent-sessions.spec.md`
|
||||
- `../agent-sessions-codex-rollout-source.spec.md`
|
||||
- `../agent-dedicated-frame.spec.md`
|
||||
|
||||
@@ -54,7 +54,7 @@ Define dedicated-frame behavior for Agent chat opening. By default, chat opens i
|
||||
- Chat resume command remains provider-specific in dedicated mode as in current-project mode:
|
||||
- Codex: `codex resume <threadId>`
|
||||
- Claude: `claude --resume <threadId>`
|
||||
- Codex fresh-thread opens (project-row `New Thread`) use `codex` without `resume`.
|
||||
- New-session action semantics (including Codex `Codex (Full Auto)` mapping) are defined in `spec/actions/new-thread.spec.md` and do not change between dedicated/current-project modes.
|
||||
|
||||
[@test] ../sessions/testSrc/AgentSessionsGearActionsTest.kt
|
||||
[@test] ../sessions/testSrc/AgentSessionsOpenModeRoutingTest.kt
|
||||
@@ -94,4 +94,5 @@ Define dedicated-frame behavior for Agent chat opening. By default, chat opens i
|
||||
## References
|
||||
- `spec/agent-chat-editor.spec.md`
|
||||
- `spec/agent-sessions.spec.md`
|
||||
- `spec/actions/new-thread.spec.md`
|
||||
- `community/platform/platform-impl/src/com/intellij/openapi/wm/ex/WelcomeScreenProjectProvider.kt`
|
||||
|
||||
@@ -21,7 +21,7 @@ Codex thread discovery for Agent Threads defaults to rollout files under `~/.cod
|
||||
- Make Codex thread indicators reflect real activity based on rollout data.
|
||||
- Keep app-server implementation in code as an alternate backend.
|
||||
- Avoid archived-session handling in this iteration.
|
||||
- Fix Codex `+` action to open fresh CLI chat (`codex`) without pre-creating a session.
|
||||
- Keep rollout backend changes independent from new-session action semantics.
|
||||
|
||||
## Non-goals
|
||||
- Archived session browsing or unarchive actions.
|
||||
@@ -41,7 +41,7 @@ Codex thread discovery for Agent Threads defaults to rollout files under `~/.cod
|
||||
- `reviewing`: teal (`#2FD1C4`)
|
||||
- `processing`: orange (`#FF9F43`)
|
||||
- `ready`: green (`#3FE47E`)
|
||||
- Codex project-row `+` action must open a fresh chat command `codex` (no `codex resume <id>`).
|
||||
- New-session action semantics (including Codex `Codex (Full Auto)` parameters) are defined in `spec/actions/new-thread.spec.md` and are backend-invariant.
|
||||
- Existing thread open behavior remains `codex resume <threadId>`.
|
||||
|
||||
## Data & Backend
|
||||
@@ -57,3 +57,4 @@ Codex thread discovery for Agent Threads defaults to rollout files under `~/.cod
|
||||
## References
|
||||
- `spec/agent-sessions.spec.md`
|
||||
- `spec/agent-chat-editor.spec.md`
|
||||
- `spec/actions/new-thread.spec.md`
|
||||
|
||||
@@ -14,7 +14,7 @@ targets:
|
||||
# Agent Threads Tool Window
|
||||
|
||||
Status: Draft
|
||||
Date: 2026-02-11
|
||||
Date: 2026-02-13
|
||||
|
||||
## Summary
|
||||
Define the Agent Threads tool window as a provider-agnostic, project-scoped session browser. Threads from supported providers are aggregated per project/worktree, rendered in one tree, and opened through a shared chat routing flow.
|
||||
@@ -46,7 +46,7 @@ Define the Agent Threads tool window as a provider-agnostic, project-scoped sess
|
||||
- Resume command must be provider-specific:
|
||||
- Codex: `codex resume <sessionId>`
|
||||
- Claude: `claude --resume <sessionId>`
|
||||
- Codex project-row `New Thread` action must open fresh CLI chat with `codex` (without pre-creating/resuming a session id).
|
||||
- New-session action behavior (provider options, Codex/Claude command mapping, and Full Auto semantics) is defined in `spec/actions/new-thread.spec.md` and must be used by both project and worktree rows.
|
||||
- Branch mismatch between thread origin and current worktree branch must show a warning confirmation before opening chat.
|
||||
|
||||
[@test] ../sessions/testSrc/AgentSessionLoadAggregationTest.kt
|
||||
@@ -86,3 +86,4 @@ Define the Agent Threads tool window as a provider-agnostic, project-scoped sess
|
||||
- `spec/agent-sessions-thread-visibility.spec.md`
|
||||
- `spec/agent-dedicated-frame.spec.md`
|
||||
- `spec/agent-chat-editor.spec.md`
|
||||
- `spec/actions/new-thread.spec.md`
|
||||
|
||||
Reference in New Issue
Block a user