diff --git a/desktop/src-tauri/src/commands/agents.rs b/desktop/src-tauri/src/commands/agents.rs
index ef2654851d..fc31acb9f1 100644
--- a/desktop/src-tauri/src/commands/agents.rs
+++ b/desktop/src-tauri/src/commands/agents.rs
@@ -13,7 +13,7 @@ use crate::{
DiscoverManagedAgentPrereqsRequest, ManagedAgentLogResponse, ManagedAgentPrereqsInfo,
ManagedAgentSummary, MintManagedAgentTokenRequest, MintManagedAgentTokenResponse,
RelayAgentInfo, DEFAULT_ACP_COMMAND, DEFAULT_AGENT_ARG, DEFAULT_AGENT_COMMAND,
- DEFAULT_AGENT_TURN_TIMEOUT_SECONDS, DEFAULT_MCP_COMMAND,
+ DEFAULT_AGENT_PARALLELISM, DEFAULT_AGENT_TURN_TIMEOUT_SECONDS, DEFAULT_MCP_COMMAND,
},
relay::{
build_authed_request, managed_agent_owner_pubkey, relay_ws_url, send_json_request,
@@ -94,6 +94,11 @@ pub async fn create_managed_agent(
if name.is_empty() {
return Err("agent name is required".to_string());
}
+ if let Some(parallelism) = input.parallelism {
+ if !(1..=32).contains(¶llelism) {
+ return Err("parallelism must be between 1 and 32".to_string());
+ }
+ }
let owner_pubkey = if input.mint_token {
Some(managed_agent_owner_pubkey(&state).await?)
@@ -209,6 +214,16 @@ pub async fn create_managed_agent(
.turn_timeout_seconds
.filter(|seconds| *seconds > 0)
.unwrap_or(DEFAULT_AGENT_TURN_TIMEOUT_SECONDS),
+ parallelism: input
+ .parallelism
+ .filter(|count| (1..=32).contains(count))
+ .unwrap_or(DEFAULT_AGENT_PARALLELISM),
+ system_prompt: input
+ .system_prompt
+ .as_deref()
+ .map(str::trim)
+ .filter(|value| !value.is_empty())
+ .map(str::to_string),
created_at: now_iso(),
updated_at: now_iso(),
last_started_at: None,
diff --git a/desktop/src-tauri/src/managed_agents/runtime.rs b/desktop/src-tauri/src/managed_agents/runtime.rs
index b0e27381a5..66bef77bca 100644
--- a/desktop/src-tauri/src/managed_agents/runtime.rs
+++ b/desktop/src-tauri/src/managed_agents/runtime.rs
@@ -88,6 +88,8 @@ pub fn build_managed_agent_summary(
agent_args: record.agent_args.clone(),
mcp_command: record.mcp_command.clone(),
turn_timeout_seconds: record.turn_timeout_seconds,
+ parallelism: record.parallelism,
+ system_prompt: record.system_prompt.clone(),
has_api_token: record.api_token.is_some(),
status,
pid,
@@ -167,10 +169,16 @@ pub fn start_managed_agent_process(
"SPROUT_ACP_TURN_TIMEOUT",
record.turn_timeout_seconds.to_string(),
);
+ command.env("SPROUT_ACP_AGENTS", record.parallelism.to_string());
command.env(
"GOOSE_MODE",
std::env::var("GOOSE_MODE").unwrap_or_else(|_| "auto".to_string()),
);
+ if let Some(system_prompt) = &record.system_prompt {
+ command.env("SPROUT_ACP_SYSTEM_PROMPT", system_prompt);
+ } else {
+ command.env_remove("SPROUT_ACP_SYSTEM_PROMPT");
+ }
command.env_remove("SPROUT_ACP_PRIVATE_KEY");
command.env_remove("SPROUT_ACP_API_TOKEN");
diff --git a/desktop/src-tauri/src/managed_agents/types.rs b/desktop/src-tauri/src/managed_agents/types.rs
index 374b60ae1d..d7dea01b19 100644
--- a/desktop/src-tauri/src/managed_agents/types.rs
+++ b/desktop/src-tauri/src/managed_agents/types.rs
@@ -24,6 +24,9 @@ pub struct ManagedAgentRecord {
pub agent_args: Vec
- Relay overrides, raw commands, timeout, and doctor guidance. + Relay overrides, raw commands, timeout, parallelism, prompt + override, and doctor guidance.
@@ -298,14 +308,18 @@ export function CreateAgentDialog({ agentArgs={agentArgs} agentCommand={agentCommand} mcpCommand={mcpCommand} + onParallelismChange={setParallelism} onAcpCommandChange={setAcpCommand} onAgentArgsChange={setAgentArgs} onAgentCommandChange={setAgentCommand} onMcpCommandChange={setMcpCommand} onRelayUrlChange={setRelayUrl} + onSystemPromptChange={setSystemPrompt} onTurnTimeoutChange={setTurnTimeoutSeconds} + parallelism={parallelism} relayUrl={relayUrl} selectedProviderId={selectedProviderId} + systemPrompt={systemPrompt} turnTimeoutSeconds={turnTimeoutSeconds} /> diff --git a/desktop/src/features/agents/ui/CreateAgentDialogSections.tsx b/desktop/src/features/agents/ui/CreateAgentDialogSections.tsx index c787983877..3c17705695 100644 --- a/desktop/src/features/agents/ui/CreateAgentDialogSections.tsx +++ b/desktop/src/features/agents/ui/CreateAgentDialogSections.tsx @@ -6,6 +6,7 @@ import type { import { MANAGED_AGENT_SCOPE_OPTIONS } from "@/features/tokens/lib/scopeOptions"; import { cn } from "@/shared/lib/cn"; import { Input } from "@/shared/ui/input"; +import { Textarea } from "@/shared/ui/textarea"; import { describeResolvedCommand } from "./agentUi"; export function CreateAgentBasicsFields({ @@ -98,28 +99,36 @@ export function CreateAgentRuntimeFields({ agentArgs, agentCommand, mcpCommand, + parallelism, relayUrl, selectedProviderId, + systemPrompt, turnTimeoutSeconds, onAcpCommandChange, onAgentArgsChange, onAgentCommandChange, onMcpCommandChange, + onParallelismChange, onRelayUrlChange, + onSystemPromptChange, onTurnTimeoutChange, }: { acpCommand: string; agentArgs: string; agentCommand: string; mcpCommand: string; + parallelism: string; relayUrl: string; selectedProviderId: string; + systemPrompt: string; turnTimeoutSeconds: string; onAcpCommandChange: (value: string) => void; onAgentArgsChange: (value: string) => void; onAgentCommandChange: (value: string) => void; onMcpCommandChange: (value: string) => void; + onParallelismChange: (value: string) => void; onRelayUrlChange: (value: string) => void; + onSystemPromptChange: (value: string) => void; onTurnTimeoutChange: (value: string) => void; }) { return ( @@ -165,7 +174,7 @@ export function CreateAgentRuntimeFields({ ) : null} -+ Number of ACP worker subprocesses. `sprout-acp` allows 1-32. +
++{logContent?.trim() ? logContent : "No log output yet."}diff --git a/desktop/src/features/messages/lib/formatTimelineMessages.ts b/desktop/src/features/messages/lib/formatTimelineMessages.ts index 96b5d7781a..f641cb843e 100644 --- a/desktop/src/features/messages/lib/formatTimelineMessages.ts +++ b/desktop/src/features/messages/lib/formatTimelineMessages.ts @@ -239,10 +239,6 @@ export function formatTimelineMessages( const authorPubkey = authorPubkeyByEventId.get(event.id) ?? getEffectiveAuthorPubkey(event); const thread = getThreadReference(event.tags); - const parentEvent = thread.parentId - ? eventsById.get(thread.parentId) - : undefined; - return { id: event.id, createdAt: event.created_at, @@ -262,8 +258,6 @@ export function formatTimelineMessages( parentId: thread.parentId, rootId: thread.rootId, depth: getDepth(event), - replyToAuthor: parentEvent ? getAuthorLabel(parentEvent) : null, - replyToSnippet: parentEvent?.content ?? null, accent: currentPubkey === authorPubkey, pending: event.pending, kind: event.kind, diff --git a/desktop/src/features/messages/types.ts b/desktop/src/features/messages/types.ts index 7bfad3f995..db962d6379 100644 --- a/desktop/src/features/messages/types.ts +++ b/desktop/src/features/messages/types.ts @@ -16,8 +16,6 @@ export type TimelineMessage = { parentId?: string | null; rootId?: string | null; depth: number; - replyToAuthor?: string | null; - replyToSnippet?: string | null; accent?: boolean; pending?: boolean; highlighted?: boolean; diff --git a/desktop/src/features/messages/ui/MessageRow.tsx b/desktop/src/features/messages/ui/MessageRow.tsx index 3c2b3edb90..ecfc135a8e 100644 --- a/desktop/src/features/messages/ui/MessageRow.tsx +++ b/desktop/src/features/messages/ui/MessageRow.tsx @@ -37,7 +37,6 @@ export function MessageRow({ >(null); const [reactionPending, setReactionPending] = React.useState(false); const visibleDepth = Math.min(message.depth, 6); - const compressedDepth = Math.max(message.depth - visibleDepth, 0); const indentPx = visibleDepth * 28; const initials = message.author .split(" ") @@ -190,25 +189,6 @@ export function MessageRow({ )}- {message.parentId ? ( -- - Reply - - - {message.replyToAuthor - ? `to ${message.replyToAuthor}` - : "to an earlier message"} - - {compressedDepth > 0 ? ( - - +{compressedDepth} more level - {compressedDepth === 1 ? "" : "s"} - - ) : null} -- ) : null} -{message.pubkey ? (@@ -291,13 +271,6 @@ export function MessageRow({ {reactionErrorMessage} ) : null} - - {message.replyToSnippet ? ( -{expandedDiffId === message.id ? (- {message.replyToSnippet} -
- ) : null} -{ await page.getByTestId("message-input").fill(firstReply); await page.getByTestId("send-message").click(); await expect(rows.last()).toContainText(firstReply); + await expect(rows.last()).not.toContainText("Welcome to #general"); await rows.last().hover(); await replyButtons.last().click(); @@ -221,6 +222,7 @@ test("supports nested replies with visible indentation", async ({ page }) => { await page.getByTestId("message-input").fill(nestedReply); await page.getByTestId("send-message").click(); await expect(rows.last()).toContainText(nestedReply); + await expect(rows.last()).not.toContainText(firstReply); const rootBox = await rows.nth(0).boundingBox(); const firstReplyBox = await rows.nth(1).boundingBox(); diff --git a/desktop/tests/e2e/smoke.spec.ts b/desktop/tests/e2e/smoke.spec.ts index d2ff850f9d..e25d9b96b1 100644 --- a/desktop/tests/e2e/smoke.spec.ts +++ b/desktop/tests/e2e/smoke.spec.ts @@ -68,6 +68,36 @@ test("creates a new mocked stream", async ({ page }) => { await expect(page.getByTestId("chat-title")).toHaveText(channelName); }); +test("create agent supports parallelism and system prompt overrides", async ({ + page, +}) => { + const agentName = `Parallel agent ${Date.now()}`; + + await page.goto("/"); + await page.getByTestId("open-agents-view").click(); + await page.getByRole("button", { name: "Create agent" }).click(); + + await page.getByTestId("agent-name-input").fill(agentName); + await page.getByRole("button", { name: "Advanced setup" }).click(); + await page.getByTestId("agent-parallelism-input").fill("3"); + await page + .getByTestId("agent-system-prompt-input") + .fill("You are concise and parallelize independent work."); + await page.getByTestId("create-agent-submit").click(); + + await expect( + page.getByRole("heading", { name: "Agent created" }), + ).toBeVisible(); + await page.getByRole("button", { name: "Done" }).click(); + + await expect(page.getByTestId("managed-agent-log-content")).toContainText( + "parallelism=3", + ); + await expect(page.getByTestId("managed-agent-log-content")).toContainText( + "system prompt override configured", + ); +}); + test("opens a mocked channel from the home feed", async ({ page }) => { const mentionsSection = page.locator("section").filter({ has: page.getByRole("heading", { name: "@Mentions" }),