mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
IJPL-248623 add task folder agent workflow
Add the Pi-only task-folder agent workflow so users can start an agent-backed folder from the tree popup or from the folder creation dialog while the generic MCP surface remains out of scope. The implementation uses the existing authenticated Pi extension control socket to expose a create-and-assign task-folder tool to the running Pi session. Launch profile selection is scoped to Pi and remembered separately from the general new-thread preference. GitOrigin-RevId: 178d047a365462c4cc142c3f0a700f05a1e2e7fe
This commit is contained in:
committed by
intellij-monorepo-bot
parent
ae1894fc5f
commit
e94aefbf55
+3
-1
@@ -1,5 +1,5 @@
|
||||
import {type ExtensionAPI, type ExtensionContext} from "@earendil-works/pi-coding-agent";
|
||||
import {startControlBridge} from "./control.ts";
|
||||
import {registerTaskFolderTool, startControlBridge} from "./control.ts";
|
||||
import {registerJbCentralProvider} from "./jbcentral.ts";
|
||||
import {parseModelCatalogMetadata} from "./modelCatalog.ts";
|
||||
import {registerOmlxProviders} from "./omlx.ts";
|
||||
@@ -33,6 +33,8 @@ export default async function agentWorkbenchTheme(pi: ExtensionAPI) {
|
||||
let lastSessionInfoSignature: string | undefined;
|
||||
let lastSessionInfoLeafId: string | null | undefined;
|
||||
|
||||
registerTaskFolderTool(pi, () => controlBridge);
|
||||
|
||||
const updateLastStatusSignature = (signature: string) => {
|
||||
lastStatusSignature = signature;
|
||||
};
|
||||
|
||||
+52
-1
@@ -1,4 +1,5 @@
|
||||
import {type ExtensionContext} from "@earendil-works/pi-coding-agent";
|
||||
import {Type} from "@earendil-works/pi-ai";
|
||||
import {defineTool, type ExtensionAPI, type ExtensionContext} from "@earendil-works/pi-coding-agent";
|
||||
import * as crypto from "node:crypto";
|
||||
import * as http from "node:http";
|
||||
import {type Duplex} from "node:stream";
|
||||
@@ -14,6 +15,7 @@ type AgentWorkbenchControlBridge = {
|
||||
setContext: (ctx: ExtensionContext) => void;
|
||||
getCurrentTaskFolder: () => Promise<AgentWorkbenchTaskFolder | undefined>;
|
||||
listTaskFolderThreads: (folderId?: string) => Promise<AgentWorkbenchTaskFolderThread[]>;
|
||||
createAndAssignTaskFolder: (name: string, metadata?: Record<string, string>) => Promise<AgentWorkbenchCreatedTaskFolder>;
|
||||
getTaskFolderMetadata: (folderId?: string) => Promise<Record<string, string> | undefined>;
|
||||
setTaskFolderMetadata: (key: string, value: string, folderId?: string) => Promise<boolean>;
|
||||
deleteTaskFolderMetadata: (key: string, folderId?: string) => Promise<boolean>;
|
||||
@@ -28,6 +30,7 @@ type AgentWorkbenchControlMessageType =
|
||||
| "forkFromEntry"
|
||||
| "getCurrentTaskFolder"
|
||||
| "listTaskFolderThreads"
|
||||
| "createAndAssignTaskFolder"
|
||||
| "getTaskFolderMetadata"
|
||||
| "setTaskFolderMetadata"
|
||||
| "deleteTaskFolderMetadata";
|
||||
@@ -36,6 +39,7 @@ type AgentWorkbenchControlSessionMessageType = "hello" | "sessionState";
|
||||
type AgentWorkbenchControlRequestType =
|
||||
| "getCurrentTaskFolder"
|
||||
| "listTaskFolderThreads"
|
||||
| "createAndAssignTaskFolder"
|
||||
| "getTaskFolderMetadata"
|
||||
| "setTaskFolderMetadata"
|
||||
| "deleteTaskFolderMetadata";
|
||||
@@ -51,12 +55,14 @@ type AgentWorkbenchControlCommand = {
|
||||
cancelled?: boolean;
|
||||
error?: string;
|
||||
folderId?: string;
|
||||
name?: string;
|
||||
key?: string;
|
||||
value?: string;
|
||||
changed?: boolean;
|
||||
folder?: AgentWorkbenchTaskFolder | null;
|
||||
threads?: AgentWorkbenchTaskFolderThread[];
|
||||
metadata?: Record<string, string>;
|
||||
assigned?: boolean;
|
||||
};
|
||||
|
||||
type AgentWorkbenchControlThread = {
|
||||
@@ -84,6 +90,11 @@ type AgentWorkbenchTaskFolderThread = {
|
||||
assignedAt?: number;
|
||||
};
|
||||
|
||||
type AgentWorkbenchCreatedTaskFolder = {
|
||||
folder: AgentWorkbenchTaskFolder;
|
||||
assigned: boolean;
|
||||
};
|
||||
|
||||
type AgentWorkbenchControlContext = ExtensionContext & {
|
||||
navigateTree?: (entryId: string) => Promise<void> | void;
|
||||
fork?: (
|
||||
@@ -293,6 +304,13 @@ export function startControlBridge(ctx: ExtensionContext): AgentWorkbenchControl
|
||||
const response = requireOk(await sendRequest("listTaskFolderThreads", {folderId}));
|
||||
return response.threads ?? [];
|
||||
},
|
||||
createAndAssignTaskFolder: async (name, metadata) => {
|
||||
const response = requireOk(await sendRequest("createAndAssignTaskFolder", {name, metadata}));
|
||||
if (response.folder === undefined || response.folder === null) {
|
||||
throw new Error("Agent Workbench did not return the created task folder");
|
||||
}
|
||||
return {folder: response.folder, assigned: response.assigned === true};
|
||||
},
|
||||
getTaskFolderMetadata: async (folderId) => {
|
||||
const response = requireOk(await sendRequest("getTaskFolderMetadata", {folderId}));
|
||||
return response.metadata;
|
||||
@@ -316,6 +334,39 @@ export function startControlBridge(ctx: ExtensionContext): AgentWorkbenchControl
|
||||
};
|
||||
}
|
||||
|
||||
export function registerTaskFolderTool(
|
||||
pi: ExtensionAPI,
|
||||
bridgeProvider: () => AgentWorkbenchControlBridge | undefined,
|
||||
): void {
|
||||
pi.registerTool(defineTool({
|
||||
name: "agent_workbench_create_task_folder",
|
||||
label: "Create Task Folder",
|
||||
description: "Create an Agent Workbench task folder for the current project and assign the current Pi session to it.",
|
||||
promptSnippet: "Create an Agent Workbench task folder and assign this Pi thread to it",
|
||||
promptGuidelines: [
|
||||
"Use agent_workbench_create_task_folder when the user asks to create a task folder or start work in a task folder.",
|
||||
"Pass the issue id as issue when the user mentions an issue tracker id.",
|
||||
],
|
||||
parameters: Type.Object({
|
||||
name: Type.String({description: "Task folder name"}),
|
||||
issue: Type.Optional(Type.String({description: "Issue tracker id to store as task folder metadata key 'issue'"})),
|
||||
}),
|
||||
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
||||
const bridge = bridgeProvider();
|
||||
if (bridge === undefined) {
|
||||
throw new Error("Agent Workbench control bridge is unavailable");
|
||||
}
|
||||
const issue = params.issue?.trim();
|
||||
const metadata = issue === undefined || issue.length === 0 ? undefined : {issue};
|
||||
const result = await bridge.createAndAssignTaskFolder(params.name, metadata);
|
||||
return {
|
||||
content: [{type: "text", text: `Created task folder '${result.folder.name}' and assigned this thread.`}],
|
||||
details: result,
|
||||
};
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
function resolveCapabilities(ctx: ExtensionContext): { navigateTree: boolean; fork: boolean } {
|
||||
const controlCtx = ctx as AgentWorkbenchControlContext;
|
||||
return {
|
||||
|
||||
+1
@@ -89,6 +89,7 @@ internal object PiExtensionControlBridge {
|
||||
PiControlMessageType.RESPONSE -> handleResponse(webSocketClient, payload)
|
||||
PiControlMessageType.GET_CURRENT_TASK_FOLDER,
|
||||
PiControlMessageType.LIST_TASK_FOLDER_THREADS,
|
||||
PiControlMessageType.CREATE_AND_ASSIGN_TASK_FOLDER,
|
||||
PiControlMessageType.GET_TASK_FOLDER_METADATA,
|
||||
PiControlMessageType.SET_TASK_FOLDER_METADATA,
|
||||
PiControlMessageType.DELETE_TASK_FOLDER_METADATA,
|
||||
|
||||
+25
@@ -21,6 +21,7 @@ internal enum class PiControlMessageType(@JvmField val wireName: String) {
|
||||
FORK_FROM_ENTRY("forkFromEntry"),
|
||||
GET_CURRENT_TASK_FOLDER("getCurrentTaskFolder"),
|
||||
LIST_TASK_FOLDER_THREADS("listTaskFolderThreads"),
|
||||
CREATE_AND_ASSIGN_TASK_FOLDER("createAndAssignTaskFolder"),
|
||||
GET_TASK_FOLDER_METADATA("getTaskFolderMetadata"),
|
||||
SET_TASK_FOLDER_METADATA("setTaskFolderMetadata"),
|
||||
DELETE_TASK_FOLDER_METADATA("deleteTaskFolderMetadata");
|
||||
@@ -69,8 +70,10 @@ internal data class PiControlPayload(
|
||||
@JvmField val thread: PiControlThreadPayload? = null,
|
||||
@JvmField val capabilities: PiControlCapabilities? = null,
|
||||
@JvmField val folderId: String? = null,
|
||||
@JvmField val name: String? = null,
|
||||
@JvmField val key: String? = null,
|
||||
@JvmField val value: String? = null,
|
||||
@JvmField val metadata: Map<String, String>? = null,
|
||||
)
|
||||
|
||||
internal data class PiControlThreadPayload(
|
||||
@@ -165,8 +168,10 @@ private fun readControlPayload(parser: JsonParser): PiControlPayload {
|
||||
var thread: PiControlThreadPayload? = null
|
||||
var capabilities: PiControlCapabilities? = null
|
||||
var folderId: String? = null
|
||||
var name: String? = null
|
||||
var key: String? = null
|
||||
var value: String? = null
|
||||
var metadata: Map<String, String>? = null
|
||||
forEachJsonObjectField(parser) { fieldName ->
|
||||
when (fieldName) {
|
||||
"type" -> type = PiControlMessageType.fromWireName(readJsonStringOrNull(parser))
|
||||
@@ -180,8 +185,10 @@ private fun readControlPayload(parser: JsonParser): PiControlPayload {
|
||||
"thread" -> thread = readControlThreadPayload(parser)
|
||||
"capabilities" -> capabilities = readControlCapabilities(parser)
|
||||
"folderId" -> folderId = readJsonStringOrNull(parser)
|
||||
"name" -> name = readJsonStringOrNull(parser)
|
||||
"key" -> key = readJsonStringOrNull(parser)
|
||||
"value" -> value = readJsonStringOrNull(parser)
|
||||
"metadata" -> metadata = readControlStringMap(parser)
|
||||
else -> parser.skipChildren()
|
||||
}
|
||||
true
|
||||
@@ -198,8 +205,10 @@ private fun readControlPayload(parser: JsonParser): PiControlPayload {
|
||||
thread = thread,
|
||||
capabilities = capabilities,
|
||||
folderId = folderId,
|
||||
name = name,
|
||||
key = key,
|
||||
value = value,
|
||||
metadata = metadata,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -243,6 +252,22 @@ private fun readControlCapabilities(parser: JsonParser): PiControlCapabilities?
|
||||
return PiControlCapabilities(navigateTree = navigateTree, fork = fork)
|
||||
}
|
||||
|
||||
private fun readControlStringMap(parser: JsonParser): Map<String, String>? {
|
||||
if (parser.currentToken() != JsonToken.START_OBJECT) {
|
||||
parser.skipChildren()
|
||||
return null
|
||||
}
|
||||
val result = LinkedHashMap<String, String>()
|
||||
forEachJsonObjectField(parser) { fieldName ->
|
||||
when (parser.currentToken()) {
|
||||
JsonToken.VALUE_STRING -> readJsonStringOrNull(parser)?.let { value -> result[fieldName] = value }
|
||||
else -> parser.skipChildren()
|
||||
}
|
||||
true
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@Suppress("DuplicatedCode")
|
||||
private fun readJsonBooleanOrNull(parser: JsonParser): Boolean? {
|
||||
return when (parser.currentToken()) {
|
||||
|
||||
+27
@@ -26,6 +26,22 @@ internal class PiTaskFolderControlHandler(
|
||||
buildTaskFolderAssignmentsResponse(requestId, service.listFolderThreadAssignments(folderId))
|
||||
}
|
||||
}
|
||||
PiControlMessageType.CREATE_AND_ASSIGN_TASK_FOLDER -> {
|
||||
val name = payload.name?.trim()?.takeIf { it.isNotEmpty() }
|
||||
if (name == null) {
|
||||
buildPiControlErrorResponse(requestId, "Task folder name is required")
|
||||
}
|
||||
else {
|
||||
val folder = service.createFolder(context.projectPath, name, payload.metadata.orEmpty())
|
||||
if (folder == null) {
|
||||
buildPiControlErrorResponse(requestId, "Task folder could not be created")
|
||||
}
|
||||
else {
|
||||
val assigned = service.assignThread(context.projectPath, PI_AGENT_SESSION_PROVIDER, context.sessionId, folder.id)
|
||||
buildTaskFolderCreatedResponse(requestId = requestId, folder = folder, assigned = assigned)
|
||||
}
|
||||
}
|
||||
}
|
||||
PiControlMessageType.GET_TASK_FOLDER_METADATA -> {
|
||||
val folder = resolveTaskFolder(service, context, payload)
|
||||
if (folder == null) {
|
||||
@@ -63,6 +79,17 @@ internal class PiTaskFolderControlHandler(
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildTaskFolderCreatedResponse(requestId: String, folder: AgentTaskFolder, assigned: Boolean): String {
|
||||
return buildPiControlJsonObject { generator ->
|
||||
generator.writeStringProperty("type", PiControlMessageType.RESPONSE.wireName)
|
||||
generator.writeStringProperty("requestId", requestId)
|
||||
generator.writeBooleanProperty("ok", true)
|
||||
generator.writeName("folder")
|
||||
writeTaskFolder(generator, folder)
|
||||
generator.writeBooleanProperty("assigned", assigned)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildTaskFolderResponse(requestId: String, folder: AgentTaskFolder?): String {
|
||||
return buildPiControlJsonObject { generator ->
|
||||
generator.writeStringProperty("type", PiControlMessageType.RESPONSE.wireName)
|
||||
|
||||
+27
@@ -241,6 +241,24 @@ class PiExtensionControlWebSocketHandlerTest {
|
||||
val deleteResponse = listener.nextMessage()
|
||||
assertThat(deleteResponse).contains("\"changed\":true")
|
||||
assertThat(folderService.getFolder(folder.id)?.metadata).doesNotContainKey("review")
|
||||
|
||||
webSocket.sendText(
|
||||
controlTaskFolderRequest(
|
||||
requestId = "request-create",
|
||||
type = "createAndAssignTaskFolder",
|
||||
name = "Follow-up task",
|
||||
metadata = mapOf("issue" to "IJPL-999999"),
|
||||
),
|
||||
true,
|
||||
).join()
|
||||
val createResponse = listener.nextMessage()
|
||||
val createdFolder = requireNotNull(folderService.getFolderForThread(projectDir.toString(), PI_AGENT_SESSION_PROVIDER, sessionId))
|
||||
assertThat(createResponse).contains("\"requestId\":\"request-create\"")
|
||||
assertThat(createResponse).contains("\"ok\":true")
|
||||
assertThat(createResponse).contains("\"assigned\":true")
|
||||
assertThat(createResponse).contains("\"id\":${createdFolder.id.jsonString()}")
|
||||
assertThat(createdFolder.name).isEqualTo("Follow-up task")
|
||||
assertThat(createdFolder.metadata).containsEntry("issue", "IJPL-999999")
|
||||
}
|
||||
finally {
|
||||
webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "done").join()
|
||||
@@ -424,16 +442,25 @@ private fun controlTaskFolderRequest(
|
||||
requestId: String,
|
||||
type: String,
|
||||
folderId: String? = null,
|
||||
name: String? = null,
|
||||
key: String? = null,
|
||||
value: String? = null,
|
||||
metadata: Map<String, String>? = null,
|
||||
): String {
|
||||
val fields = mutableListOf(
|
||||
"\"type\":${type.jsonString()}",
|
||||
"\"requestId\":${requestId.jsonString()}",
|
||||
)
|
||||
folderId?.let { fields += "\"folderId\":${it.jsonString()}" }
|
||||
name?.let { fields += "\"name\":${it.jsonString()}" }
|
||||
key?.let { fields += "\"key\":${it.jsonString()}" }
|
||||
value?.let { fields += "\"value\":${it.jsonString()}" }
|
||||
metadata?.let { values ->
|
||||
val metadataFields = values.entries.joinToString(",") { (metadataKey, metadataValue) ->
|
||||
"${metadataKey.jsonString()}:${metadataValue.jsonString()}"
|
||||
}
|
||||
fields += "\"metadata\":{$metadataFields}"
|
||||
}
|
||||
return "{${fields.joinToString(",")}}"
|
||||
}
|
||||
|
||||
|
||||
+6
@@ -49,6 +49,11 @@
|
||||
id="AgentWorkbenchSessions.TreePopup.CreateTaskFolder"
|
||||
icon="AllIcons.General.Add"
|
||||
class="com.intellij.agent.workbench.sessions.toolwindow.actions.AgentSessionsTreePopupCreateTaskFolderAction"/>
|
||||
<group
|
||||
id="AgentWorkbenchSessions.TreePopup.TaskFolderAgent"
|
||||
popup="true"
|
||||
icon="AllIcons.General.Add"
|
||||
class="com.intellij.agent.workbench.sessions.toolwindow.actions.AgentSessionsTreePopupTaskFolderAgentGroup"/>
|
||||
<group
|
||||
id="AgentWorkbenchSessions.TreePopup.MoveToTaskFolder"
|
||||
popup="true"
|
||||
@@ -91,6 +96,7 @@
|
||||
<reference ref="AgentWorkbenchSessions.TreePopup.More"/>
|
||||
<reference ref="AgentWorkbenchSessions.TreePopup.NewThread"/>
|
||||
<reference ref="AgentWorkbenchSessions.TreePopup.CreateTaskFolder"/>
|
||||
<reference ref="AgentWorkbenchSessions.TreePopup.TaskFolderAgent"/>
|
||||
<separator/>
|
||||
<reference ref="AgentWorkbenchSessions.TreePopup.MoveToTaskFolder"/>
|
||||
<reference ref="AgentWorkbenchSessions.TreePopup.RemoveFromTaskFolder"/>
|
||||
|
||||
+231
-42
@@ -6,6 +6,7 @@ import com.intellij.agent.workbench.sessions.model.ArchiveThreadTarget
|
||||
import com.intellij.agent.workbench.sessions.service.AgentSessionArchiveRequestResult
|
||||
import com.intellij.agent.workbench.sessions.service.AgentSessionArchiveService
|
||||
import com.intellij.agent.workbench.sessions.statistics.AgentWorkbenchEntryPoint
|
||||
import com.intellij.agent.workbench.prompt.core.AgentPromptLaunchProfile
|
||||
import com.intellij.openapi.actionSystem.ActionGroup
|
||||
import com.intellij.openapi.actionSystem.ActionUpdateThread
|
||||
import com.intellij.openapi.actionSystem.AnAction
|
||||
@@ -14,35 +15,59 @@ import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.project.DumbAware
|
||||
import com.intellij.openapi.project.DumbAwareAction
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.ui.ComboBox
|
||||
import com.intellij.openapi.ui.DialogWrapper
|
||||
import com.intellij.openapi.ui.InputValidatorEx
|
||||
import com.intellij.openapi.ui.Messages
|
||||
import com.intellij.openapi.ui.ValidationInfo
|
||||
import com.intellij.openapi.util.NlsContexts
|
||||
import com.intellij.openapi.util.NlsSafe
|
||||
import com.intellij.platform.ai.agent.core.session.AgentSessionProvider
|
||||
import com.intellij.platform.ai.agent.sessions.core.SessionActionTarget
|
||||
import com.intellij.platform.ai.agent.sessions.core.folders.AgentTaskFolder
|
||||
import com.intellij.platform.ai.agent.sessions.core.folders.AgentTaskFolderService
|
||||
import com.intellij.platform.ai.agent.sessions.core.folders.AgentTaskFolderStatus
|
||||
import com.intellij.ui.components.JBLabel
|
||||
import com.intellij.ui.components.JBTextField
|
||||
import com.intellij.util.ui.JBUI
|
||||
import java.awt.event.ActionEvent
|
||||
import java.awt.GridBagConstraints
|
||||
import java.awt.GridBagLayout
|
||||
import javax.swing.DefaultComboBoxModel
|
||||
import javax.swing.Action
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JPanel
|
||||
|
||||
internal class AgentSessionsTreePopupCreateTaskFolderAction : DumbAwareAction {
|
||||
private val resolveContext: (AnActionEvent) -> AgentSessionsTreePopupActionContext?
|
||||
private val promptForName: (Project) -> String?
|
||||
private val createFolder: (String, String) -> Unit
|
||||
private val promptForCreateRequest: (Project, Boolean) -> CreateTaskFolderRequest?
|
||||
private val createFolder: (String, String) -> AgentTaskFolder?
|
||||
private val taskFolderAgentProfile: (Project) -> AgentPromptLaunchProfile?
|
||||
private val openTaskFolderAgent: (String, AgentTaskFolder, AgentPromptLaunchProfile, Project) -> Unit
|
||||
|
||||
@Suppress("unused")
|
||||
constructor() {
|
||||
resolveContext = ::resolveAgentSessionsTreePopupActionContext
|
||||
promptForName = ::showCreateTaskFolderDialog
|
||||
promptForCreateRequest = ::showCreateTaskFolderDialog
|
||||
createFolder = { path, name -> service<AgentTaskFolderService>().createFolder(path, name) }
|
||||
taskFolderAgentProfile = ::resolveTaskFolderAgentQuickStartProfile
|
||||
openTaskFolderAgent = { path, folder, profile, project ->
|
||||
createTaskFolderAgentViaService(path, profile, project, AgentWorkbenchEntryPoint.TREE_POPUP, folder)
|
||||
}
|
||||
}
|
||||
|
||||
internal constructor(
|
||||
resolveContext: (AnActionEvent) -> AgentSessionsTreePopupActionContext?,
|
||||
promptForName: (Project) -> String?,
|
||||
createFolder: (String, String) -> Unit,
|
||||
promptForCreateRequest: (Project, Boolean) -> CreateTaskFolderRequest?,
|
||||
createFolder: (String, String) -> AgentTaskFolder?,
|
||||
taskFolderAgentProfile: (Project) -> AgentPromptLaunchProfile? = { null },
|
||||
openTaskFolderAgent: (String, AgentTaskFolder, AgentPromptLaunchProfile, Project) -> Unit = { _, _, _, _ -> },
|
||||
) {
|
||||
this.resolveContext = resolveContext
|
||||
this.promptForName = promptForName
|
||||
this.promptForCreateRequest = promptForCreateRequest
|
||||
this.createFolder = createFolder
|
||||
this.taskFolderAgentProfile = taskFolderAgentProfile
|
||||
this.openTaskFolderAgent = openTaskFolderAgent
|
||||
}
|
||||
|
||||
override fun update(e: AnActionEvent) {
|
||||
@@ -52,8 +77,13 @@ internal class AgentSessionsTreePopupCreateTaskFolderAction : DumbAwareAction {
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
val context = resolveContext(e) ?: return
|
||||
val path = createFolderPath(context.target) ?: return
|
||||
val name = promptForName(context.project) ?: return
|
||||
createFolder(path, name)
|
||||
val profile = taskFolderAgentProfile(context.project)
|
||||
val request = promptForCreateRequest(context.project, profile != null) ?: return
|
||||
val folder = createFolder(path, request.name) ?: return
|
||||
if (request.createWithAgent) {
|
||||
val effectiveProfile = profile ?: taskFolderAgentProfile(context.project) ?: return
|
||||
openTaskFolderAgent(path, folder, effectiveProfile, context.project)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT
|
||||
@@ -173,13 +203,26 @@ internal class AgentSessionsTreePopupRemoveFromTaskFolderAction : DumbAwareActio
|
||||
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT
|
||||
}
|
||||
|
||||
internal class AgentSessionsTreePopupSetTaskFolderMetadataAction : DumbAwareAction() {
|
||||
private val resolveContext: (AnActionEvent) -> AgentSessionsTreePopupActionContext? =
|
||||
::resolveAgentSessionsTreePopupActionContext
|
||||
private val promptForMetadata: (Project, SessionActionTarget.TaskFolder) -> Pair<String, String>? =
|
||||
::showSetTaskFolderMetadataDialog
|
||||
private val setMetadata: (SessionActionTarget.TaskFolder, String, String) -> Unit = { target, key, value ->
|
||||
service<AgentTaskFolderService>().setMetadata(target.folderId, key, value)
|
||||
internal class AgentSessionsTreePopupSetTaskFolderMetadataAction : DumbAwareAction {
|
||||
private val resolveContext: (AnActionEvent) -> AgentSessionsTreePopupActionContext?
|
||||
private val promptForMetadata: (Project, SessionActionTarget.TaskFolder) -> TaskFolderMetadataEdit?
|
||||
private val setMetadata: (SessionActionTarget.TaskFolder, String, String) -> Unit
|
||||
|
||||
@Suppress("unused")
|
||||
constructor() {
|
||||
resolveContext = ::resolveAgentSessionsTreePopupActionContext
|
||||
promptForMetadata = ::showSetTaskFolderMetadataDialog
|
||||
setMetadata = { target, key, value -> service<AgentTaskFolderService>().setMetadata(target.folderId, key, value) }
|
||||
}
|
||||
|
||||
internal constructor(
|
||||
resolveContext: (AnActionEvent) -> AgentSessionsTreePopupActionContext?,
|
||||
promptForMetadata: (Project, SessionActionTarget.TaskFolder) -> TaskFolderMetadataEdit?,
|
||||
setMetadata: (SessionActionTarget.TaskFolder, String, String) -> Unit,
|
||||
) {
|
||||
this.resolveContext = resolveContext
|
||||
this.promptForMetadata = promptForMetadata
|
||||
this.setMetadata = setMetadata
|
||||
}
|
||||
|
||||
override fun update(e: AnActionEvent) {
|
||||
@@ -189,13 +232,35 @@ internal class AgentSessionsTreePopupSetTaskFolderMetadataAction : DumbAwareActi
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
val context = resolveContext(e) ?: return
|
||||
val target = context.target as? SessionActionTarget.TaskFolder ?: return
|
||||
val (key, value) = promptForMetadata(context.project, target) ?: return
|
||||
setMetadata(target, key, value)
|
||||
val metadata = promptForMetadata(context.project, target)?.let(::resolveTaskFolderMetadataUpdate) ?: return
|
||||
setMetadata(target, metadata.key, metadata.value)
|
||||
}
|
||||
|
||||
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT
|
||||
}
|
||||
|
||||
private val TASK_FOLDER_METADATA_KEY_PRESETS: List<@NlsSafe String> = listOf("issue", "review")
|
||||
|
||||
internal data class TaskFolderMetadataEdit(
|
||||
@JvmField val key: String,
|
||||
@JvmField val value: String,
|
||||
)
|
||||
|
||||
internal data class CreateTaskFolderRequest(
|
||||
@JvmField val name: String,
|
||||
@JvmField val createWithAgent: Boolean,
|
||||
)
|
||||
|
||||
internal data class TaskFolderMetadataUpdate(
|
||||
@JvmField val key: String,
|
||||
@JvmField val value: String,
|
||||
)
|
||||
|
||||
internal fun resolveTaskFolderMetadataUpdate(edit: TaskFolderMetadataEdit): TaskFolderMetadataUpdate? {
|
||||
val key = edit.key.trim().takeIf { it.isNotEmpty() } ?: return null
|
||||
return TaskFolderMetadataUpdate(key = key, value = edit.value)
|
||||
}
|
||||
|
||||
internal class AgentSessionsTreePopupDeleteTaskFolderMetadataAction : DumbAwareAction() {
|
||||
private val resolveContext: (AnActionEvent) -> AgentSessionsTreePopupActionContext? =
|
||||
::resolveAgentSessionsTreePopupActionContext
|
||||
@@ -337,13 +402,10 @@ private fun createFolderPath(target: SessionActionTarget?): String? {
|
||||
}
|
||||
}
|
||||
|
||||
private fun showCreateTaskFolderDialog(project: Project): String? {
|
||||
return showTaskFolderNameDialog(
|
||||
project = project,
|
||||
title = AgentSessionsBundle.message("toolwindow.task.folder.create.dialog.title"),
|
||||
message = AgentSessionsBundle.message("toolwindow.task.folder.create.dialog.message"),
|
||||
initialValue = "",
|
||||
)
|
||||
private fun showCreateTaskFolderDialog(project: Project, canCreateWithAgent: Boolean): CreateTaskFolderRequest? {
|
||||
val dialog = CreateTaskFolderDialog(project, canCreateWithAgent)
|
||||
if (!dialog.showAndGet()) return null
|
||||
return dialog.createRequest()
|
||||
}
|
||||
|
||||
private fun showRenameTaskFolderDialog(project: Project, currentName: String): String? {
|
||||
@@ -373,24 +435,9 @@ private fun confirmDeleteTaskFolder(project: Project, target: SessionActionTarge
|
||||
) == Messages.YES
|
||||
}
|
||||
|
||||
private fun showSetTaskFolderMetadataDialog(project: Project, target: SessionActionTarget.TaskFolder): Pair<String, String>? {
|
||||
val key = Messages.showInputDialog(
|
||||
project,
|
||||
AgentSessionsBundle.message("toolwindow.task.folder.metadata.key.dialog.message"),
|
||||
AgentSessionsBundle.message("toolwindow.task.folder.metadata.set.dialog.title"),
|
||||
Messages.getQuestionIcon(),
|
||||
target.metadata.keys.firstOrNull().orEmpty(),
|
||||
NonBlankInputValidator(),
|
||||
) ?: return null
|
||||
val value = Messages.showInputDialog(
|
||||
project,
|
||||
AgentSessionsBundle.message("toolwindow.task.folder.metadata.value.dialog.message"),
|
||||
AgentSessionsBundle.message("toolwindow.task.folder.metadata.set.dialog.title"),
|
||||
Messages.getQuestionIcon(),
|
||||
target.metadata[key].orEmpty(),
|
||||
null,
|
||||
) ?: return null
|
||||
return key to value
|
||||
private fun showSetTaskFolderMetadataDialog(project: Project, target: SessionActionTarget.TaskFolder): TaskFolderMetadataEdit? {
|
||||
val dialog = TaskFolderMetadataDialog(project, target)
|
||||
return if (dialog.showAndGet()) dialog.metadataEdit() else null
|
||||
}
|
||||
|
||||
private fun showDeleteTaskFolderMetadataDialog(project: Project, target: SessionActionTarget.TaskFolder): String? {
|
||||
@@ -413,3 +460,145 @@ private class NonBlankInputValidator : InputValidatorEx {
|
||||
return if (checkInput(inputString)) null else AgentSessionsBundle.message("toolwindow.task.folder.dialog.validation.empty")
|
||||
}
|
||||
}
|
||||
|
||||
private class CreateTaskFolderDialog(
|
||||
project: Project,
|
||||
private val canCreateWithAgent: Boolean,
|
||||
) : DialogWrapper(project) {
|
||||
private val nameField = JBTextField(32)
|
||||
private var createWithAgent = false
|
||||
|
||||
init {
|
||||
title = AgentSessionsBundle.message("toolwindow.task.folder.create.dialog.title")
|
||||
setOKButtonText(AgentSessionsBundle.message("toolwindow.task.folder.create.dialog.create"))
|
||||
init()
|
||||
initValidation()
|
||||
}
|
||||
|
||||
override fun createCenterPanel(): JComponent {
|
||||
return JPanel(GridBagLayout()).apply {
|
||||
border = JBUI.Borders.empty(8)
|
||||
addMetadataRow(0, AgentSessionsBundle.message("toolwindow.task.folder.create.dialog.message"), nameField)
|
||||
}
|
||||
}
|
||||
|
||||
override fun createActions(): Array<Action> {
|
||||
val createWithAgentAction = object : DialogWrapperAction(AgentSessionsBundle.message("toolwindow.task.folder.create.dialog.with.agent")) {
|
||||
init {
|
||||
isEnabled = canCreateWithAgent
|
||||
}
|
||||
|
||||
override fun doAction(e: ActionEvent?) {
|
||||
createWithAgent = true
|
||||
doOKAction()
|
||||
if (!isOK) {
|
||||
createWithAgent = false
|
||||
}
|
||||
}
|
||||
}
|
||||
return arrayOf(okAction, createWithAgentAction, cancelAction)
|
||||
}
|
||||
|
||||
override fun getPreferredFocusedComponent(): JComponent = nameField
|
||||
|
||||
override fun doValidate(): ValidationInfo? {
|
||||
if (nameField.text.isBlank()) {
|
||||
return ValidationInfo(AgentSessionsBundle.message("toolwindow.task.folder.dialog.validation.empty"), nameField)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun createRequest(): CreateTaskFolderRequest {
|
||||
return CreateTaskFolderRequest(name = nameField.text, createWithAgent = createWithAgent)
|
||||
}
|
||||
}
|
||||
|
||||
private class TaskFolderMetadataDialog(
|
||||
project: Project,
|
||||
private val target: SessionActionTarget.TaskFolder,
|
||||
) : DialogWrapper(project) {
|
||||
private val keyCombo = ComboBox<@NlsSafe String>().apply {
|
||||
model = DefaultComboBoxModel(taskFolderMetadataKeyOptions(target.metadata))
|
||||
isEditable = true
|
||||
}
|
||||
private val valueField = JBTextField(32)
|
||||
private var loadedKey: String? = null
|
||||
|
||||
init {
|
||||
title = AgentSessionsBundle.message("toolwindow.task.folder.metadata.set.dialog.title")
|
||||
val initialKey = initialMetadataKey(target.metadata)
|
||||
keyCombo.selectedItem = initialKey
|
||||
loadedKey = initialKey
|
||||
valueField.text = target.metadata[initialKey].orEmpty()
|
||||
keyCombo.addActionListener { handleKeyChanged() }
|
||||
init()
|
||||
initValidation()
|
||||
}
|
||||
|
||||
override fun createCenterPanel(): JComponent {
|
||||
return JPanel(GridBagLayout()).apply {
|
||||
border = JBUI.Borders.empty(8)
|
||||
addMetadataRow(0, AgentSessionsBundle.message("toolwindow.task.folder.metadata.key.dialog.message"), keyCombo)
|
||||
addMetadataRow(1, AgentSessionsBundle.message("toolwindow.task.folder.metadata.value.dialog.message"), valueField)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getPreferredFocusedComponent(): JComponent = keyCombo
|
||||
|
||||
override fun doValidate(): ValidationInfo? {
|
||||
if (metadataKey().isBlank()) {
|
||||
return ValidationInfo(AgentSessionsBundle.message("toolwindow.task.folder.metadata.key.validation.empty"), keyCombo)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun metadataEdit(): TaskFolderMetadataEdit {
|
||||
return TaskFolderMetadataEdit(
|
||||
key = metadataKey(),
|
||||
value = valueField.text,
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleKeyChanged() {
|
||||
val key = metadataKey()
|
||||
if (key == loadedKey) return
|
||||
target.metadata[key]?.let { value ->
|
||||
valueField.text = value
|
||||
}
|
||||
loadedKey = key
|
||||
}
|
||||
|
||||
private fun metadataKey(): @NlsSafe String {
|
||||
return keyCombo.editor.item?.toString().orEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
private fun JPanel.addMetadataRow(row: Int, labelText: @NlsContexts.Label String, component: JComponent) {
|
||||
val label = JBLabel(labelText).apply { setLabelFor(component) }
|
||||
add(label, GridBagConstraints().apply {
|
||||
gridx = 0
|
||||
gridy = row
|
||||
anchor = GridBagConstraints.WEST
|
||||
insets = JBUI.insets(0, 0, 8, 8)
|
||||
})
|
||||
add(component, GridBagConstraints().apply {
|
||||
gridx = 1
|
||||
gridy = row
|
||||
fill = GridBagConstraints.HORIZONTAL
|
||||
weightx = 1.0
|
||||
insets = JBUI.insetsBottom(8)
|
||||
})
|
||||
}
|
||||
|
||||
private fun taskFolderMetadataKeyOptions(metadata: Map<String, String>): Array<@NlsSafe String> {
|
||||
val keys = LinkedHashSet<@NlsSafe String>()
|
||||
keys.addAll(TASK_FOLDER_METADATA_KEY_PRESETS)
|
||||
metadata.keys.filterTo(keys) { key -> key.isNotBlank() }
|
||||
return keys.toTypedArray()
|
||||
}
|
||||
|
||||
private fun initialMetadataKey(metadata: Map<String, String>): @NlsSafe String {
|
||||
return TASK_FOLDER_METADATA_KEY_PRESETS.firstOrNull(metadata::containsKey)
|
||||
?: metadata.keys.firstOrNull { key -> key.isNotBlank() }
|
||||
?: TASK_FOLDER_METADATA_KEY_PRESETS.first()
|
||||
}
|
||||
|
||||
+219
@@ -0,0 +1,219 @@
|
||||
// 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.toolwindow.actions
|
||||
|
||||
import com.intellij.agent.workbench.prompt.core.AgentPromptInitialMessageRequest
|
||||
import com.intellij.agent.workbench.prompt.core.AgentPromptLaunchProfile
|
||||
import com.intellij.agent.workbench.sessions.AgentSessionLaunchProfileSelection
|
||||
import com.intellij.agent.workbench.sessions.AgentSessionsBundle
|
||||
import com.intellij.agent.workbench.sessions.buildAgentSessionLaunchProfileMenuActions
|
||||
import com.intellij.agent.workbench.sessions.buildAgentSessionLaunchProfileMenuModel
|
||||
import com.intellij.agent.workbench.sessions.launchQuickStartProfile
|
||||
import com.intellij.agent.workbench.sessions.providerItemIconWithMode
|
||||
import com.intellij.agent.workbench.sessions.resolveAgentSessionLaunchProfileSelection
|
||||
import com.intellij.agent.workbench.sessions.service.AgentPreparedNewSessionLaunchContext
|
||||
import com.intellij.agent.workbench.sessions.service.AgentSessionLaunchService
|
||||
import com.intellij.agent.workbench.sessions.state.AgentSessionUiPreferencesStateService
|
||||
import com.intellij.agent.workbench.sessions.statistics.AgentWorkbenchEntryPoint
|
||||
import com.intellij.openapi.actionSystem.ActionGroup
|
||||
import com.intellij.openapi.actionSystem.ActionUpdateThread
|
||||
import com.intellij.openapi.actionSystem.AnAction
|
||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.project.DumbAware
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.platform.ai.agent.core.session.AgentSessionLaunchMode
|
||||
import com.intellij.platform.ai.agent.core.session.AgentSessionProvider
|
||||
import com.intellij.platform.ai.agent.sessions.core.SessionActionTarget
|
||||
import com.intellij.platform.ai.agent.sessions.core.folders.AgentTaskFolder
|
||||
import com.intellij.platform.ai.agent.sessions.core.folders.AgentTaskFolderService
|
||||
import com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionProviderDescriptor
|
||||
import com.intellij.platform.ai.agent.sessions.core.providers.AgentSessionProviders
|
||||
import com.intellij.platform.ai.agent.sessions.core.providers.builtInLaunchProfileId
|
||||
import com.intellij.platform.ai.agent.sessions.core.providers.hasEntries
|
||||
|
||||
internal class AgentSessionsTreePopupTaskFolderAgentGroup @JvmOverloads constructor(
|
||||
private val resolveContext: (AnActionEvent) -> AgentSessionsTreePopupActionContext? =
|
||||
::resolveAgentSessionsTreePopupActionContext,
|
||||
private val allBridges: () -> List<AgentSessionProviderDescriptor> = AgentSessionProviders::allProviders,
|
||||
private val userLaunchProfiles: () -> List<AgentPromptLaunchProfile> = { service<AgentSessionUiPreferencesStateService>().getUserLaunchProfiles() },
|
||||
private val taskFolderAgentLaunchProfileId: () -> String? = {
|
||||
service<AgentSessionUiPreferencesStateService>().getTaskFolderAgentLaunchProfileId()
|
||||
},
|
||||
private val setTaskFolderAgentLaunchProfileId: (String?) -> Unit = { profileId ->
|
||||
service<AgentSessionUiPreferencesStateService>().setTaskFolderAgentLaunchProfileId(profileId)
|
||||
},
|
||||
private val createTaskFolderAgent: (String, AgentPromptLaunchProfile, Project, AgentWorkbenchEntryPoint) -> Unit =
|
||||
{ path, profile, project, entryPoint -> createTaskFolderAgentViaService(path, profile, project, entryPoint, folder = null) },
|
||||
) : ActionGroup(), DumbAware {
|
||||
|
||||
override fun update(e: AnActionEvent) {
|
||||
val menu = resolveTaskFolderAgentMenu(e)
|
||||
if (menu == null) {
|
||||
e.presentation.isEnabledAndVisible = false
|
||||
return
|
||||
}
|
||||
|
||||
val quickStartItem = menu.selection.quickStartItem
|
||||
val enabled = quickStartItem?.menuItem?.isEnabled == true
|
||||
e.presentation.isVisible = true
|
||||
e.presentation.isEnabled = enabled
|
||||
e.presentation.isPopupGroup = true
|
||||
e.presentation.isPerformGroup = enabled
|
||||
e.presentation.icon = quickStartItem?.let { providerItemIconWithMode(it.menuItem) }
|
||||
if (!enabled) {
|
||||
e.presentation.description = AgentSessionsBundle.message("action.AgentWorkbenchSessions.TreePopup.TaskFolderAgent.disabled.description")
|
||||
}
|
||||
}
|
||||
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
val menu = resolveTaskFolderAgentMenu(e) ?: return
|
||||
launchQuickStartProfile(
|
||||
path = menu.path,
|
||||
project = menu.context.project,
|
||||
quickStartItem = menu.selection.quickStartItem,
|
||||
entryPoint = AgentWorkbenchEntryPoint.TREE_POPUP,
|
||||
createNewSession = ::launchTaskFolderAgent,
|
||||
)
|
||||
}
|
||||
|
||||
override fun getChildren(e: AnActionEvent?): Array<AnAction> {
|
||||
val menu = e?.let(::resolveTaskFolderAgentMenu) ?: return emptyArray()
|
||||
if (menu.selection.profiles.isEmpty()) return emptyArray()
|
||||
return buildAgentSessionLaunchProfileMenuActions(
|
||||
path = menu.path,
|
||||
project = menu.context.project,
|
||||
selection = menu.selection,
|
||||
entryPoint = AgentWorkbenchEntryPoint.TREE_POPUP,
|
||||
createNewSession = ::launchTaskFolderAgent,
|
||||
includeManageAction = false,
|
||||
)
|
||||
}
|
||||
|
||||
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT
|
||||
|
||||
private fun resolveTaskFolderAgentMenu(e: AnActionEvent): TaskFolderAgentMenu? {
|
||||
val context = resolveContext(e) ?: return null
|
||||
val path = taskFolderAgentPathFromTarget(context.target) ?: return null
|
||||
val selection = resolveTaskFolderAgentLaunchProfileSelection(
|
||||
project = context.project,
|
||||
allBridges = allBridges(),
|
||||
userLaunchProfiles = userLaunchProfiles(),
|
||||
preferredProfileId = taskFolderAgentLaunchProfileId(),
|
||||
)
|
||||
return TaskFolderAgentMenu(context = context, path = path, selection = selection)
|
||||
}
|
||||
|
||||
private fun launchTaskFolderAgent(
|
||||
path: String,
|
||||
profile: AgentPromptLaunchProfile,
|
||||
project: Project,
|
||||
entryPoint: AgentWorkbenchEntryPoint,
|
||||
) {
|
||||
setTaskFolderAgentLaunchProfileId(profile.id)
|
||||
createTaskFolderAgent(path, profile, project, entryPoint)
|
||||
}
|
||||
|
||||
private data class TaskFolderAgentMenu(
|
||||
val context: AgentSessionsTreePopupActionContext,
|
||||
val path: String,
|
||||
val selection: AgentSessionLaunchProfileSelection,
|
||||
)
|
||||
}
|
||||
|
||||
internal fun resolveTaskFolderAgentQuickStartProfile(project: Project): AgentPromptLaunchProfile? {
|
||||
val selection = resolveTaskFolderAgentLaunchProfileSelection(
|
||||
project = project,
|
||||
allBridges = AgentSessionProviders.allProviders(),
|
||||
userLaunchProfiles = service<AgentSessionUiPreferencesStateService>().getUserLaunchProfiles(),
|
||||
preferredProfileId = service<AgentSessionUiPreferencesStateService>().getTaskFolderAgentLaunchProfileId(),
|
||||
)
|
||||
return selection.quickStartItem
|
||||
?.takeIf { item -> item.menuItem.isEnabled }
|
||||
?.profile
|
||||
}
|
||||
|
||||
internal fun createTaskFolderAgentViaService(
|
||||
path: String,
|
||||
profile: AgentPromptLaunchProfile,
|
||||
project: Project,
|
||||
entryPoint: AgentWorkbenchEntryPoint,
|
||||
folder: AgentTaskFolder?,
|
||||
) {
|
||||
service<AgentSessionUiPreferencesStateService>().setTaskFolderAgentLaunchProfileId(profile.id)
|
||||
service<AgentSessionLaunchService>().createNewSession(
|
||||
path = path,
|
||||
launchProfileId = profile.id,
|
||||
entryPoint = entryPoint,
|
||||
currentProject = project,
|
||||
initialMessageRequestBuilder = { context ->
|
||||
AgentPromptInitialMessageRequest(prompt = buildTaskFolderAgentPrompt(path = path, folder = folder, context = context))
|
||||
},
|
||||
preparedLaunchHandler = folder?.let { taskFolder ->
|
||||
{ context ->
|
||||
service<AgentTaskFolderService>().assignThread(path, context.provider, context.threadId, taskFolder.id)
|
||||
}
|
||||
},
|
||||
singleFlightDiscriminator = "task-folder-agent:${folder?.id ?: "new"}",
|
||||
updateGeneralProviderPreferences = false,
|
||||
threadTitle = folder?.name,
|
||||
)
|
||||
}
|
||||
|
||||
private fun resolveTaskFolderAgentLaunchProfileSelection(
|
||||
project: Project,
|
||||
allBridges: List<AgentSessionProviderDescriptor>,
|
||||
userLaunchProfiles: List<AgentPromptLaunchProfile>,
|
||||
preferredProfileId: String?,
|
||||
): AgentSessionLaunchProfileSelection {
|
||||
val piBridges = allBridges.filter { descriptor -> descriptor.provider == TASK_FOLDER_AGENT_PROVIDER }
|
||||
val menuModel = buildAgentSessionLaunchProfileMenuModel(piBridges, project)
|
||||
if (!menuModel.hasEntries()) {
|
||||
return AgentSessionLaunchProfileSelection(profiles = emptyList(), quickStartItem = null)
|
||||
}
|
||||
return resolveAgentSessionLaunchProfileSelection(
|
||||
menuModel = menuModel,
|
||||
userProfiles = userLaunchProfiles.filter { profile -> profile.providerId == TASK_FOLDER_AGENT_PROVIDER.value },
|
||||
preferredProfileId = preferredProfileId,
|
||||
fallbackProfileIds = listOf(builtInLaunchProfileId(TASK_FOLDER_AGENT_PROVIDER, AgentSessionLaunchMode.STANDARD)),
|
||||
)
|
||||
}
|
||||
|
||||
private fun buildTaskFolderAgentPrompt(
|
||||
path: String,
|
||||
folder: AgentTaskFolder?,
|
||||
context: AgentPreparedNewSessionLaunchContext,
|
||||
): String {
|
||||
if (folder != null) {
|
||||
return """
|
||||
Continue this Agent Workbench task folder.
|
||||
|
||||
Project path: $path
|
||||
Task folder name: ${folder.name}
|
||||
Task folder id: ${folder.id}
|
||||
Current thread id: ${context.threadId}
|
||||
|
||||
The IDE has already assigned this thread to the task folder. If the work needs issue tracker association, use task folder metadata key "issue".
|
||||
Ask for any missing task details, then proceed with the task.
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
return """
|
||||
Start an Agent Workbench task-folder workflow for this project.
|
||||
|
||||
Project path: $path
|
||||
Current thread id: ${context.threadId}
|
||||
|
||||
When you know the task folder name, call the agent_workbench_create_task_folder tool. If the user mentions an issue tracker id, pass it as the issue parameter.
|
||||
If the task details are missing, ask one concise follow-up before creating the folder.
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
private fun taskFolderAgentPathFromTarget(target: SessionActionTarget): String? {
|
||||
return when (target) {
|
||||
is SessionActionTarget.Project -> target.path
|
||||
is SessionActionTarget.Worktree -> target.path
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private val TASK_FOLDER_AGENT_PROVIDER: AgentSessionProvider = AgentSessionProvider.from("pi")
|
||||
+244
-2
@@ -18,6 +18,7 @@ import com.intellij.agent.workbench.sessions.service.AgentSessionProviderAvailab
|
||||
import com.intellij.agent.workbench.sessions.toolwindow.actions.AgentSessionsTreePopupActionContext
|
||||
import com.intellij.agent.workbench.sessions.toolwindow.actions.AgentSessionsTreePopupArchiveThreadAction
|
||||
import com.intellij.agent.workbench.sessions.toolwindow.actions.AgentSessionsTreePopupCopyThreadIdAction
|
||||
import com.intellij.agent.workbench.sessions.toolwindow.actions.AgentSessionsTreePopupTaskFolderAgentGroup
|
||||
import com.intellij.agent.workbench.sessions.toolwindow.actions.AgentSessionsTreePopupCreateTaskFolderAction
|
||||
import com.intellij.agent.workbench.sessions.toolwindow.actions.AgentSessionsTreePopupDataKeys
|
||||
import com.intellij.agent.workbench.sessions.toolwindow.actions.AgentSessionsTreePopupMarkTaskFolderDoneAction
|
||||
@@ -27,8 +28,11 @@ import com.intellij.agent.workbench.sessions.toolwindow.actions.AgentSessionsTre
|
||||
import com.intellij.agent.workbench.sessions.toolwindow.actions.AgentSessionsTreePopupOpenAction
|
||||
import com.intellij.agent.workbench.sessions.toolwindow.actions.AgentSessionsTreePopupRemoveFromTaskFolderAction
|
||||
import com.intellij.agent.workbench.sessions.toolwindow.actions.AgentSessionsTreePopupRenameThreadAction
|
||||
import com.intellij.agent.workbench.sessions.toolwindow.actions.AgentSessionsTreePopupSetTaskFolderMetadataAction
|
||||
import com.intellij.agent.workbench.sessions.toolwindow.actions.AgentSessionsTreePopupToggleThreadPinAction
|
||||
import com.intellij.agent.workbench.sessions.toolwindow.actions.AgentSessionsTreePopupUnarchiveThreadAction
|
||||
import com.intellij.agent.workbench.sessions.toolwindow.actions.CreateTaskFolderRequest
|
||||
import com.intellij.agent.workbench.sessions.toolwindow.actions.TaskFolderMetadataEdit
|
||||
import com.intellij.agent.workbench.sessions.toolwindow.actions.canMoveThreadsToTaskFolder
|
||||
import com.intellij.agent.workbench.sessions.toolwindow.actions.createAgentSessionsTreePopupActionContext
|
||||
import com.intellij.agent.workbench.sessions.toolwindow.actions.resolveAgentSessionsTreePopupActionContext
|
||||
@@ -503,10 +507,14 @@ class AgentSessionsTreePopupActionsTest {
|
||||
var createdName: String? = null
|
||||
val action = AgentSessionsTreePopupCreateTaskFolderAction(
|
||||
resolveContext = { event -> resolveAgentSessionsTreePopupActionContext(event) },
|
||||
promptForName = { "Authentication rewrite" },
|
||||
promptForCreateRequest = { _, canCreateWithAgent ->
|
||||
assertThat(canCreateWithAgent).isFalse()
|
||||
CreateTaskFolderRequest(name = "Authentication rewrite", createWithAgent = false)
|
||||
},
|
||||
createFolder = { path, name ->
|
||||
createdPath = path
|
||||
createdName = name
|
||||
taskFolder(path = path, name = name)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -535,6 +543,124 @@ class AgentSessionsTreePopupActionsTest {
|
||||
assertThat(threadEvent.presentation.isEnabledAndVisible).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun createTaskFolderActionCanOpenCreatedFolderWithAgent() {
|
||||
val profile = AgentPromptLaunchProfile(
|
||||
id = "user:task-pi",
|
||||
name = "Task Pi",
|
||||
providerId = AgentSessionProvider.from("pi").value,
|
||||
launchMode = AgentSessionLaunchMode.STANDARD,
|
||||
)
|
||||
var openedPath: String? = null
|
||||
var openedFolder: AgentTaskFolder? = null
|
||||
var openedProfile: AgentPromptLaunchProfile? = null
|
||||
var openedProject: Project? = null
|
||||
val action = AgentSessionsTreePopupCreateTaskFolderAction(
|
||||
resolveContext = { event -> resolveAgentSessionsTreePopupActionContext(event) },
|
||||
promptForCreateRequest = { _, canCreateWithAgent ->
|
||||
assertThat(canCreateWithAgent).isTrue()
|
||||
CreateTaskFolderRequest(name = "Authentication rewrite", createWithAgent = true)
|
||||
},
|
||||
createFolder = { path, name -> taskFolder(path = path, name = name) },
|
||||
taskFolderAgentProfile = { profile },
|
||||
openTaskFolderAgent = { path, folder, launchProfile, project ->
|
||||
openedPath = path
|
||||
openedFolder = folder
|
||||
openedProfile = launchProfile
|
||||
openedProject = project
|
||||
},
|
||||
)
|
||||
val context = popupContext(
|
||||
nodeId = SessionTreeId.Project("/work/project-a"),
|
||||
node = SessionTreeNode.Project(AgentProjectSessions(path = "/work/project-a", name = "Project A", isOpen = true)),
|
||||
)
|
||||
|
||||
action.actionPerformed(popupEvent(action, context))
|
||||
|
||||
assertThat(openedPath).isEqualTo("/work/project-a")
|
||||
assertThat(openedFolder?.name).isEqualTo("Authentication rewrite")
|
||||
assertThat(openedProfile).isEqualTo(profile)
|
||||
assertThat(openedProject).isSameAs(context.project)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun setTaskFolderMetadataActionStoresIssuePreset() {
|
||||
val appliedMetadata = mutableListOf<Triple<String, String, String>>()
|
||||
val action = setTaskFolderMetadataAction(
|
||||
edit = TaskFolderMetadataEdit(key = "issue", value = "IJPL-248623"),
|
||||
appliedMetadata = appliedMetadata,
|
||||
)
|
||||
val event = popupEvent(action, taskFolderPopupContext())
|
||||
|
||||
action.update(event)
|
||||
assertThat(event.presentation.isEnabledAndVisible).isTrue()
|
||||
action.actionPerformed(event)
|
||||
|
||||
assertThat(appliedMetadata).containsExactly(Triple("folder1", "issue", "IJPL-248623"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun setTaskFolderMetadataActionStoresReviewPreset() {
|
||||
val appliedMetadata = mutableListOf<Triple<String, String, String>>()
|
||||
val action = setTaskFolderMetadataAction(
|
||||
edit = TaskFolderMetadataEdit(key = "review", value = "backend"),
|
||||
appliedMetadata = appliedMetadata,
|
||||
)
|
||||
|
||||
action.actionPerformed(popupEvent(action, taskFolderPopupContext()))
|
||||
|
||||
assertThat(appliedMetadata).containsExactly(Triple("folder1", "review", "backend"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun setTaskFolderMetadataActionStoresTrimmedCustomKey() {
|
||||
val appliedMetadata = mutableListOf<Triple<String, String, String>>()
|
||||
val action = setTaskFolderMetadataAction(
|
||||
edit = TaskFolderMetadataEdit(key = " owner ", value = "platform"),
|
||||
appliedMetadata = appliedMetadata,
|
||||
)
|
||||
|
||||
action.actionPerformed(popupEvent(action, taskFolderPopupContext()))
|
||||
|
||||
assertThat(appliedMetadata).containsExactly(Triple("folder1", "owner", "platform"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun setTaskFolderMetadataActionRejectsBlankCustomKey() {
|
||||
val appliedMetadata = mutableListOf<Triple<String, String, String>>()
|
||||
val action = setTaskFolderMetadataAction(
|
||||
edit = TaskFolderMetadataEdit(key = " ", value = "platform"),
|
||||
appliedMetadata = appliedMetadata,
|
||||
)
|
||||
|
||||
action.actionPerformed(popupEvent(action, taskFolderPopupContext()))
|
||||
|
||||
assertThat(appliedMetadata).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun setTaskFolderMetadataActionVisibleOnlyForTaskFolders() {
|
||||
val action = setTaskFolderMetadataAction(edit = null, appliedMetadata = mutableListOf())
|
||||
val folderEvent = popupEvent(action, taskFolderPopupContext())
|
||||
|
||||
action.update(folderEvent)
|
||||
|
||||
assertThat(folderEvent.presentation.isEnabledAndVisible).isTrue()
|
||||
|
||||
val threadContext = popupContext(
|
||||
nodeId = SessionTreeId.Thread("/work/project-a", AgentSessionProvider.from("codex"), "thread-1"),
|
||||
node = SessionTreeNode.Thread(
|
||||
project = AgentProjectSessions(path = "/work/project-a", name = "Project A", isOpen = true),
|
||||
thread = thread(id = "thread-1", provider = AgentSessionProvider.from("codex")),
|
||||
),
|
||||
)
|
||||
val threadEvent = popupEvent(action, threadContext)
|
||||
|
||||
action.update(threadEvent)
|
||||
|
||||
assertThat(threadEvent.presentation.isEnabledAndVisible).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun moveToTaskFolderGroupAssignsSelectedThreads() {
|
||||
val provider = AgentSessionProvider.from("codex")
|
||||
@@ -1172,6 +1298,97 @@ class AgentSessionsTreePopupActionsTest {
|
||||
assertThat(entryPoint).isEqualTo(AgentWorkbenchEntryPoint.TREE_POPUP)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun taskFolderAgentGroupFiltersToPiAndRemembersSelectedProfile() {
|
||||
var launchedPath: String? = null
|
||||
var launchedProfile: AgentPromptLaunchProfile? = null
|
||||
var launchedProject: Project? = null
|
||||
var entryPoint: AgentWorkbenchEntryPoint? = null
|
||||
var rememberedProfileId: String? = null
|
||||
val piProfile = AgentPromptLaunchProfile(
|
||||
id = "user:pi-task-profile",
|
||||
name = "Pi Task Profile",
|
||||
providerId = AgentSessionProvider.from("pi").value,
|
||||
launchMode = AgentSessionLaunchMode.STANDARD,
|
||||
)
|
||||
val codexProfile = AgentPromptLaunchProfile(
|
||||
id = "user:codex-task-profile",
|
||||
name = "Codex Task Profile",
|
||||
providerId = AgentSessionProvider.from("codex").value,
|
||||
launchMode = AgentSessionLaunchMode.STANDARD,
|
||||
)
|
||||
val piBridge = TestAgentSessionProviderDescriptor(
|
||||
provider = AgentSessionProvider.from("pi"),
|
||||
supportedModes = setOf(AgentSessionLaunchMode.STANDARD),
|
||||
cliAvailable = true,
|
||||
)
|
||||
val codexBridge = TestAgentSessionProviderDescriptor(
|
||||
provider = AgentSessionProvider.from("codex"),
|
||||
supportedModes = setOf(AgentSessionLaunchMode.STANDARD),
|
||||
cliAvailable = true,
|
||||
)
|
||||
val group = AgentSessionsTreePopupTaskFolderAgentGroup(
|
||||
resolveContext = { event -> resolveAgentSessionsTreePopupActionContext(event) },
|
||||
allBridges = { listOf(codexBridge, piBridge) },
|
||||
userLaunchProfiles = { listOf(codexProfile, piProfile) },
|
||||
taskFolderAgentLaunchProfileId = { piProfile.id },
|
||||
setTaskFolderAgentLaunchProfileId = { profileId -> rememberedProfileId = profileId },
|
||||
createTaskFolderAgent = { path, profile, project, capturedEntryPoint ->
|
||||
launchedPath = path
|
||||
launchedProfile = profile
|
||||
launchedProject = project
|
||||
entryPoint = capturedEntryPoint
|
||||
},
|
||||
)
|
||||
val projectContext = popupContext(
|
||||
nodeId = SessionTreeId.Project("/work/project-a"),
|
||||
node = SessionTreeNode.Project(AgentProjectSessions(path = "/work/project-a", name = "Project A", isOpen = true)),
|
||||
)
|
||||
val event = popupEvent(group, projectContext)
|
||||
|
||||
group.update(event)
|
||||
assertThat(event.presentation.isEnabledAndVisible).isTrue()
|
||||
assertThat(event.presentation.isPerformGroup).isTrue()
|
||||
|
||||
group.actionPerformed(event)
|
||||
|
||||
assertThat(rememberedProfileId).isEqualTo(piProfile.id)
|
||||
assertThat(launchedPath).isEqualTo("/work/project-a")
|
||||
assertThat(launchedProfile).isEqualTo(piProfile)
|
||||
assertThat(launchedProject).isSameAs(projectContext.project)
|
||||
assertThat(entryPoint).isEqualTo(AgentWorkbenchEntryPoint.TREE_POPUP)
|
||||
|
||||
val children = group.getChildren(event).filterNot { action -> action is Separator }
|
||||
assertThat(children.map { action -> action.templatePresentation.text }).contains(piProfile.name)
|
||||
assertThat(children.map { action -> action.templatePresentation.text }).doesNotContain(codexProfile.name)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun taskFolderAgentGroupIsVisibleButDisabledWithoutPi() {
|
||||
val codexBridge = TestAgentSessionProviderDescriptor(
|
||||
provider = AgentSessionProvider.from("codex"),
|
||||
supportedModes = setOf(AgentSessionLaunchMode.STANDARD),
|
||||
cliAvailable = true,
|
||||
)
|
||||
val group = AgentSessionsTreePopupTaskFolderAgentGroup(
|
||||
resolveContext = { event -> resolveAgentSessionsTreePopupActionContext(event) },
|
||||
allBridges = { listOf(codexBridge) },
|
||||
userLaunchProfiles = { emptyList() },
|
||||
)
|
||||
val projectContext = popupContext(
|
||||
nodeId = SessionTreeId.Project("/work/project-a"),
|
||||
node = SessionTreeNode.Project(AgentProjectSessions(path = "/work/project-a", name = "Project A", isOpen = true)),
|
||||
)
|
||||
val event = popupEvent(group, projectContext)
|
||||
|
||||
group.update(event)
|
||||
|
||||
assertThat(event.presentation.isVisible).isTrue()
|
||||
assertThat(event.presentation.isEnabled).isFalse()
|
||||
assertThat(event.presentation.description)
|
||||
.isEqualTo(AgentSessionsBundle.message("action.AgentWorkbenchSessions.TreePopup.TaskFolderAgent.disabled.description"))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun assertTogglePinHidden(action: AgentSessionsTreePopupToggleThreadPinAction, context: AgentSessionsTreePopupActionContext) {
|
||||
@@ -1222,15 +1439,40 @@ private fun threadActionTarget(
|
||||
)
|
||||
}
|
||||
|
||||
private fun taskFolderPopupContext(metadata: Map<String, String> = emptyMap()): AgentSessionsTreePopupActionContext {
|
||||
return popupContext(
|
||||
nodeId = SessionTreeId.TaskFolder("/work/project-a", "/work/project-a", "folder1"),
|
||||
node = SessionTreeNode.TaskFolder(
|
||||
project = AgentProjectSessions(path = "/work/project-a", name = "Project A", isOpen = true),
|
||||
folder = taskFolder(path = "/work/project-a", metadata = metadata),
|
||||
assignedThreadCount = 0,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun setTaskFolderMetadataAction(
|
||||
edit: TaskFolderMetadataEdit?,
|
||||
appliedMetadata: MutableList<Triple<String, String, String>>,
|
||||
): AgentSessionsTreePopupSetTaskFolderMetadataAction {
|
||||
return AgentSessionsTreePopupSetTaskFolderMetadataAction(
|
||||
resolveContext = { event -> resolveAgentSessionsTreePopupActionContext(event) },
|
||||
promptForMetadata = { _, _ -> edit },
|
||||
setMetadata = { target, key, value -> appliedMetadata += Triple(target.folderId, key, value) },
|
||||
)
|
||||
}
|
||||
|
||||
private fun taskFolder(
|
||||
path: String,
|
||||
name: String = "Research",
|
||||
status: AgentTaskFolderStatus = AgentTaskFolderStatus.IN_PROGRESS,
|
||||
metadata: Map<String, String> = emptyMap(),
|
||||
): AgentTaskFolder {
|
||||
return AgentTaskFolder(
|
||||
path = path,
|
||||
id = "folder1",
|
||||
name = "Research",
|
||||
name = name,
|
||||
status = status,
|
||||
metadata = metadata,
|
||||
createdAt = 1,
|
||||
updatedAt = 1,
|
||||
)
|
||||
|
||||
@@ -74,6 +74,8 @@ toolwindow.thread.status.done=Done
|
||||
toolwindow.task.folder.thread.count={0,choice,1#1 thread|2#{0} threads}
|
||||
toolwindow.task.folder.create.dialog.title=New Task Folder
|
||||
toolwindow.task.folder.create.dialog.message=Folder name:
|
||||
toolwindow.task.folder.create.dialog.create=Create
|
||||
toolwindow.task.folder.create.dialog.with.agent=Create with Agent
|
||||
toolwindow.task.folder.rename.dialog.title=Rename Task Folder
|
||||
toolwindow.task.folder.rename.dialog.message=Folder name:
|
||||
toolwindow.task.folder.delete.dialog.title=Delete Task Folder
|
||||
@@ -82,6 +84,7 @@ toolwindow.task.folder.dialog.validation.empty=Folder name must not be empty.
|
||||
toolwindow.task.folder.metadata.set.dialog.title=Set Task Metadata
|
||||
toolwindow.task.folder.metadata.key.dialog.message=Metadata key:
|
||||
toolwindow.task.folder.metadata.value.dialog.message=Metadata value:
|
||||
toolwindow.task.folder.metadata.key.validation.empty=Metadata key must not be empty.
|
||||
toolwindow.task.folder.metadata.delete.dialog.title=Delete Task Metadata
|
||||
toolwindow.task.folder.metadata.delete.dialog.message=Metadata key:
|
||||
toolwindow.activity.action.attention.tooltip=Threads waiting for input or review
|
||||
@@ -156,6 +159,9 @@ group.AgentWorkbenchSessions.TreePopup.NewThread.text=New Thread
|
||||
group.AgentWorkbenchSessions.TreePopup.NewThread.description=Start a new thread
|
||||
action.AgentWorkbenchSessions.TreePopup.CreateTaskFolder.text=New Task Folder
|
||||
action.AgentWorkbenchSessions.TreePopup.CreateTaskFolder.description=Create a task folder for this project or worktree
|
||||
group.AgentWorkbenchSessions.TreePopup.TaskFolderAgent.text=New Task Folder with Agent
|
||||
group.AgentWorkbenchSessions.TreePopup.TaskFolderAgent.description=Create a task folder with an agent
|
||||
action.AgentWorkbenchSessions.TreePopup.TaskFolderAgent.disabled.description=Pi is required to create a task folder with an agent.
|
||||
group.AgentWorkbenchSessions.TreePopup.MoveToTaskFolder.text=Move to Task Folder
|
||||
group.AgentWorkbenchSessions.TreePopup.MoveToTaskFolder.description=Move selected threads to a task folder
|
||||
action.AgentWorkbenchSessions.TreePopup.RemoveFromTaskFolder.text=Remove from Task Folder
|
||||
|
||||
@@ -221,6 +221,14 @@ data class AgentDeferredNewSessionLaunchResult(
|
||||
@JvmField val error: AgentPromptLaunchError? = null,
|
||||
)
|
||||
|
||||
data class AgentPreparedNewSessionLaunchContext(
|
||||
@JvmField val projectPath: String,
|
||||
val provider: AgentSessionProvider,
|
||||
@JvmField val threadId: String,
|
||||
@JvmField val identity: String,
|
||||
@JvmField val launchProfileId: String?,
|
||||
)
|
||||
|
||||
interface AgentDeferredNewSessionHandle {
|
||||
val file: VirtualFile
|
||||
|
||||
@@ -793,6 +801,8 @@ class AgentSessionLaunchService internal constructor(
|
||||
entryPoint: AgentWorkbenchEntryPoint,
|
||||
currentProject: Project? = null,
|
||||
initialMessageRequest: AgentPromptInitialMessageRequest? = null,
|
||||
initialMessageRequestBuilder: ((AgentPreparedNewSessionLaunchContext) -> AgentPromptInitialMessageRequest?)? = null,
|
||||
preparedLaunchHandler: ((AgentPreparedNewSessionLaunchContext) -> Unit)? = null,
|
||||
preferredDedicatedFrame: Boolean? = null,
|
||||
openedChatHandler: (suspend (Project, VirtualFile) -> Unit)? = null,
|
||||
promptLaunchResolved: ((AgentPromptLaunchResult) -> Unit)? = null,
|
||||
@@ -817,6 +827,8 @@ class AgentSessionLaunchService internal constructor(
|
||||
entryPoint = entryPoint,
|
||||
currentProject = currentProject,
|
||||
initialMessageRequest = initialMessageRequest,
|
||||
initialMessageRequestBuilder = initialMessageRequestBuilder,
|
||||
preparedLaunchHandler = preparedLaunchHandler,
|
||||
preferredDedicatedFrame = preferredDedicatedFrame,
|
||||
openedChatHandler = openedChatHandler,
|
||||
promptLaunchResolved = promptLaunchResolved,
|
||||
@@ -839,6 +851,8 @@ class AgentSessionLaunchService internal constructor(
|
||||
entryPoint: AgentWorkbenchEntryPoint,
|
||||
currentProject: Project? = null,
|
||||
initialMessageRequest: AgentPromptInitialMessageRequest? = null,
|
||||
initialMessageRequestBuilder: ((AgentPreparedNewSessionLaunchContext) -> AgentPromptInitialMessageRequest?)? = null,
|
||||
preparedLaunchHandler: ((AgentPreparedNewSessionLaunchContext) -> Unit)? = null,
|
||||
preferredDedicatedFrame: Boolean? = null,
|
||||
openedChatHandler: (suspend (Project, VirtualFile) -> Unit)? = null,
|
||||
promptLaunchResolved: ((AgentPromptLaunchResult) -> Unit)? = null,
|
||||
@@ -898,6 +912,8 @@ class AgentSessionLaunchService internal constructor(
|
||||
launchProfileId = effectiveLaunchProfileId,
|
||||
currentProject = currentProject,
|
||||
initialMessageRequest = initialMessageRequest,
|
||||
initialMessageRequestBuilder = initialMessageRequestBuilder,
|
||||
preparedLaunchHandler = preparedLaunchHandler,
|
||||
updateGeneralProviderPreferences = updateGeneralProviderPreferences,
|
||||
generationSettings = effectiveGenerationSettings,
|
||||
generationModelCatalog = generationModelCatalog,
|
||||
@@ -1194,6 +1210,8 @@ class AgentSessionLaunchService internal constructor(
|
||||
launchProfileId: String?,
|
||||
currentProject: Project?,
|
||||
initialMessageRequest: AgentPromptInitialMessageRequest?,
|
||||
initialMessageRequestBuilder: ((AgentPreparedNewSessionLaunchContext) -> AgentPromptInitialMessageRequest?)? = null,
|
||||
preparedLaunchHandler: ((AgentPreparedNewSessionLaunchContext) -> Unit)? = null,
|
||||
updateGeneralProviderPreferences: Boolean,
|
||||
generationSettings: AgentPromptGenerationSettings,
|
||||
generationModelCatalog: List<AgentPromptGenerationModel> = emptyList(),
|
||||
@@ -1220,13 +1238,12 @@ class AgentSessionLaunchService internal constructor(
|
||||
return NewSessionLaunchPreparationResult.Failed(AgentPromptLaunchError.PROVIDER_UNAVAILABLE)
|
||||
}
|
||||
notifyAgentSessionConversationOpened(descriptor)
|
||||
if (updateGeneralProviderPreferences && descriptor.supportsPromptLaunch) {
|
||||
uiPreferencesState.updateProviderOptionsOnLaunch(provider.value, initialMessageRequest)
|
||||
val staticInitialMessagePlan = if (initialMessageRequestBuilder == null) {
|
||||
initialMessageRequest?.let(descriptor::buildInitialMessagePlan) ?: AgentInitialMessagePlan.EMPTY
|
||||
}
|
||||
else {
|
||||
AgentInitialMessagePlan.EMPTY
|
||||
}
|
||||
|
||||
val initialMessagePlan = initialMessageRequest
|
||||
?.let(descriptor::buildInitialMessagePlan)
|
||||
?: AgentInitialMessagePlan.EMPTY
|
||||
val projectDirectory = resolveLaunchProjectDirectory(path = normalizedPath, currentProject = currentProject, stateStore = stateStore)
|
||||
val plannedLaunch = AgentSessionLaunchPlanner.plan(
|
||||
intent = AgentSessionLaunchIntent(
|
||||
@@ -1238,13 +1255,35 @@ class AgentSessionLaunchService internal constructor(
|
||||
generationSettings = generationSettings,
|
||||
),
|
||||
project = currentProject,
|
||||
initialMessagePlan = initialMessagePlan,
|
||||
initialMessagePlan = staticInitialMessagePlan,
|
||||
generationModelCatalog = generationModelCatalog,
|
||||
extraEnvVariables = extraEnvVariables,
|
||||
extraCommandArgs = extraCommandArgs,
|
||||
)
|
||||
val baseLaunchSpec = plannedLaunch.baseLaunchSpec
|
||||
val plannedLaunchSpec = plannedLaunch.launchSpec
|
||||
val preliminaryIdentity = buildNewSessionIdentity(
|
||||
provider = provider,
|
||||
launchSpec = plannedLaunchSpec,
|
||||
fallbackPendingIdentity = fallbackPendingIdentity,
|
||||
)
|
||||
val preliminaryContext = AgentPreparedNewSessionLaunchContext(
|
||||
projectPath = normalizedPath,
|
||||
provider = provider,
|
||||
threadId = resolveAgentSessionId(preliminaryIdentity),
|
||||
identity = preliminaryIdentity,
|
||||
launchProfileId = launchProfileId,
|
||||
)
|
||||
val effectiveInitialMessageRequest = initialMessageRequestBuilder?.invoke(preliminaryContext) ?: initialMessageRequest
|
||||
if (updateGeneralProviderPreferences && descriptor.supportsPromptLaunch) {
|
||||
uiPreferencesState.updateProviderOptionsOnLaunch(provider.value, effectiveInitialMessageRequest)
|
||||
}
|
||||
val initialMessagePlan = if (initialMessageRequestBuilder == null) {
|
||||
staticInitialMessagePlan
|
||||
}
|
||||
else {
|
||||
effectiveInitialMessageRequest?.let(descriptor::buildInitialMessagePlan) ?: AgentInitialMessagePlan.EMPTY
|
||||
}
|
||||
val prestartedLaunch = descriptor.prestartNewSessionLaunch(
|
||||
projectPath = normalizedPath,
|
||||
launchMode = mode,
|
||||
@@ -1255,8 +1294,21 @@ class AgentSessionLaunchService internal constructor(
|
||||
)
|
||||
val launchSpec = prestartedLaunch?.launchSpec ?: plannedLaunchSpec
|
||||
val identity = buildNewSessionIdentity(provider = provider, launchSpec = launchSpec, fallbackPendingIdentity = fallbackPendingIdentity)
|
||||
val preparedContext = if (identity == preliminaryContext.identity) {
|
||||
preliminaryContext
|
||||
}
|
||||
else {
|
||||
AgentPreparedNewSessionLaunchContext(
|
||||
projectPath = normalizedPath,
|
||||
provider = provider,
|
||||
threadId = resolveAgentSessionId(identity),
|
||||
identity = identity,
|
||||
launchProfileId = launchProfileId,
|
||||
)
|
||||
}
|
||||
preparedLaunchHandler?.invoke(preparedContext)
|
||||
val initialMessageDispatchPlan = prestartedLaunch?.initialMessageDispatchPlan
|
||||
?: buildInitialMessageDispatchPlan(
|
||||
?: buildInitialMessageDispatchPlan(
|
||||
descriptor = descriptor,
|
||||
baseLaunchSpec = launchSpec,
|
||||
identity = identity,
|
||||
|
||||
@@ -27,6 +27,16 @@ class AgentSessionUiPreferencesStateService(
|
||||
return launchProfileStateService.getDefaultLaunchProfileId()
|
||||
}
|
||||
|
||||
fun getTaskFolderAgentLaunchProfileId(): String? {
|
||||
return state.taskFolderAgentLaunchProfileId
|
||||
}
|
||||
|
||||
fun setTaskFolderAgentLaunchProfileId(profileId: String?) {
|
||||
updateState { current ->
|
||||
current.copy(taskFolderAgentLaunchProfileId = profileId?.trim()?.takeIf { it.isNotEmpty() })
|
||||
}
|
||||
}
|
||||
|
||||
fun getProviderPreferences(): AgentPromptLauncherBridge.ProviderPreferences {
|
||||
return AgentPromptLauncherBridge.ProviderPreferences(
|
||||
providerOptionsByProviderId = state.providerOptionsByProviderId,
|
||||
@@ -72,5 +82,6 @@ class AgentSessionUiPreferencesStateService(
|
||||
data class UiPreferencesState(
|
||||
@JvmField val providerOptionsByProviderId: Map<String, Set<String>> = emptyMap(),
|
||||
@JvmField val containerModeEnabled: Boolean = false,
|
||||
@JvmField val taskFolderAgentLaunchProfileId: String? = null,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -581,7 +581,7 @@ class AgentSessionLaunchServiceTest {
|
||||
provider = AgentSessionProvider.from("codex"),
|
||||
supportedModes = setOf(AgentSessionLaunchMode.STANDARD),
|
||||
cliAvailable = true,
|
||||
newSessionLaunchSpecProvider = {
|
||||
newSessionLaunchSpecProvider = { _ ->
|
||||
val attempt = launchSpecAttempts.incrementAndGet()
|
||||
AgentSessionTerminalLaunchSpec(command = listOf("test", "retry", attempt.toString()))
|
||||
},
|
||||
@@ -807,6 +807,57 @@ class AgentSessionLaunchServiceTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun createNewSessionBuilderAndPreparedHandlerUsePreallocatedLaunchSpecSessionId() {
|
||||
val provider = AgentSessionProvider.from("pi")
|
||||
val preallocatedSessionId = "f174b4df-e942-49fe-bb30-8b5f8e7f4857"
|
||||
val builderThreadIds = CopyOnWriteArrayList<String>()
|
||||
val preparedThreadIds = CopyOnWriteArrayList<String>()
|
||||
val descriptor = TestAgentSessionProviderDescriptor(
|
||||
provider = provider,
|
||||
supportedModes = setOf(AgentSessionLaunchMode.STANDARD),
|
||||
cliAvailable = true,
|
||||
newSessionLaunchSpecProvider = {
|
||||
AgentSessionTerminalLaunchSpec(
|
||||
command = listOf("test", "new"),
|
||||
preallocatedSessionId = preallocatedSessionId,
|
||||
)
|
||||
},
|
||||
)
|
||||
val chatOpenExecutor = RecordingChatOpenExecutor()
|
||||
|
||||
AgentSessionProviders.withRegistryForTest(InMemoryAgentSessionProviderRegistry(listOf(descriptor))) {
|
||||
runBlocking(Dispatchers.Default) {
|
||||
withTestServiceAndLaunch(
|
||||
sessionSourcesProvider = { listOf(ScriptedSessionSource(provider = provider)) },
|
||||
projectEntriesProvider = { listOf(openTestProjectEntry(PROJECT_PATH, "Project A")) },
|
||||
chatOpenExecutor = chatOpenExecutor,
|
||||
) { _, launchService ->
|
||||
launchService.createNewSession(
|
||||
path = PROJECT_PATH,
|
||||
provider = provider,
|
||||
entryPoint = AgentWorkbenchEntryPoint.TREE_POPUP,
|
||||
initialMessageRequestBuilder = { context ->
|
||||
builderThreadIds += context.threadId
|
||||
AgentPromptInitialMessageRequest(prompt = "Start task folder ${context.threadId}")
|
||||
},
|
||||
preparedLaunchHandler = { context ->
|
||||
preparedThreadIds += context.threadId
|
||||
},
|
||||
)
|
||||
|
||||
waitForCondition { chatOpenExecutor.openNewChatCalls.get() == 1 }
|
||||
|
||||
val openRequest = checkNotNull(chatOpenExecutor.lastOpenNewChatRequest.get())
|
||||
assertThat(openRequest.identity).isEqualTo(buildAgentSessionIdentity(provider, preallocatedSessionId))
|
||||
assertThat(openRequest.initialComposedMessage).isEqualTo("Start task folder $preallocatedSessionId")
|
||||
assertThat(builderThreadIds).containsExactly(preallocatedSessionId)
|
||||
assertThat(preparedThreadIds).containsExactly(preallocatedSessionId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun openCodexThreadRebindsMatchingPendingTabBeforeOpening() {
|
||||
assertOpenThreadRebindsMatchingPendingTabBeforeOpening(AgentSessionProvider.from("codex"))
|
||||
|
||||
@@ -91,8 +91,8 @@ Agent Workbench treats Pi as a first-class terminal-backed provider. Pi sessions
|
||||
[@test] ../../lib-agent/providers/pi/sessions/testSrc/PiExtensionControlWebSocketHandlerTest.kt
|
||||
|
||||
- The same authenticated control WebSocket may carry explicit task-folder capability requests from the bundled Pi extension to the IDE:
|
||||
`getCurrentTaskFolder`, `listTaskFolderThreads`, `getTaskFolderMetadata`, `setTaskFolderMetadata`, and
|
||||
`deleteTaskFolderMetadata`. These requests must use the bound cwd/session id to resolve current-session context, use global folder id
|
||||
`getCurrentTaskFolder`, `listTaskFolderThreads`, `createAndAssignTaskFolder`, `getTaskFolderMetadata`,
|
||||
`setTaskFolderMetadata`, and `deleteTaskFolderMetadata`. These requests must use the bound cwd/session id to resolve current-session context, use global folder id
|
||||
for explicit folder mutations, must not inject prompt context automatically, and must return normal `response` frames with `ok`,
|
||||
`requestId`, and the requested folder, assignment, metadata, or mutation result fields.
|
||||
[@test] ../../lib-agent/providers/pi/sessions/testSrc/PiExtensionControlWebSocketHandlerTest.kt
|
||||
|
||||
@@ -51,7 +51,9 @@ create a new top-level Agent Workbench entity and do not inject context into pro
|
||||
|
||||
- Project and worktree rows must offer `New Task Folder`. Thread rows must offer `Move to Task Folder` for in-progress folders on the same
|
||||
path and `Remove from Task Folder` for assigned threads. Folder rows must offer rename, delete, explicit metadata set/delete, and mark
|
||||
done actions. User-visible action text and dialog text must live in `AgentSessionsBundle.properties`.
|
||||
done actions. The metadata set dialog must expose an editable metadata key combo with `issue` and `review` key presets while persisting
|
||||
ordinary string key/value metadata. Custom typed keys are stored as entered after trimming.
|
||||
User-visible action text and dialog text must live in `AgentSessionsBundle.properties`.
|
||||
[@test] ../../sessions-toolwindow/testSrc/AgentSessionsTreePopupActionsTest.kt
|
||||
|
||||
- Dragging active thread rows onto an in-progress task folder on the same path must perform the same move as `Move to Task Folder`. If the
|
||||
|
||||
Reference in New Issue
Block a user