Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.internal.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ This changelog documents internal development changes, refactors, tooling update

## [Unreleased]

### Added
- Added parallel tool group tracking to `AgentSessionManager`: new `parallelToolGroups` Map state, `extractAllToolInfo()` and `extractAllToolResultInfo()` methods to detect multiple `tool_use`/`tool_result` blocks per message (previous `extractToolInfo()` used `.find()` which only captured the first block). Added `createSessionEntryForTool()`/`createSessionEntryForToolResult()` for individual block processing. Modified `handleClaudeMessage()` assistant/user cases to detect and route multi-block messages. Modified `syncEntryToActivitySink()` to suppress individual activities for parallel groups and post unified ephemeral activities via `postParallelGroupActivity()`. Added `formatParallelToolGroup()` to `IMessageFormatter` interface in `packages/core/src/agent-runner-types.ts` and implemented in all 4 runners (Claude, Codex, Cursor, Gemini) with tree-like markdown formatting (`├─`/`└─` with `⏳`/`✅`/`❌` status icons). Added 11 tests in `AgentSessionManager.parallel-tools.test.ts`. ([CYPACK-886](https://linear.app/ceedar/issue/CYPACK-886), [#937](https://github.com/ceedaragents/cyrus/pull/937))

## [0.2.28] - 2026-03-04

### Added
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

### Changed
- **Parallel tool calls now show as a single unified activity** - When the agent runs multiple tools in parallel, they are displayed as one compact tree-like view with live status updates (pending/completed/error) instead of flooding the timeline with separate activities for each tool. The unified activity is ephemeral and automatically replaced when the next action occurs. ([CYPACK-886](https://linear.app/ceedar/issue/CYPACK-886), [#937](https://github.com/ceedaragents/cyrus/pull/937))

## [0.2.28] - 2026-03-04

### Fixed
Expand Down
58 changes: 58 additions & 0 deletions packages/claude-runner/src/formatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,21 @@ export interface IMessageFormatter {
result: string,
isError: boolean,
): string;

/**
* Format a group of parallel tool calls as a unified view.
* Used for ephemeral activities that show all parallel tools in a tree-like structure.
* @param tools - Array of tool info with status
* @returns Formatted markdown string showing the parallel tools
*/
formatParallelToolGroup(
tools: Array<{
name: string;
input: any;
status: "pending" | "completed";
isError?: boolean;
}>,
): string;
}

/**
Expand Down Expand Up @@ -647,4 +662,47 @@ export class ClaudeMessageFormatter implements IMessageFormatter {
return result || "";
}
}

/**
* Format a group of parallel tool calls as a unified view.
* Produces a tree-like structure similar to Claude Code's native parallel agent display.
*/
formatParallelToolGroup(
tools: Array<{
name: string;
input: any;
status: "pending" | "completed";
isError?: boolean;
}>,
): string {
const totalTools = tools.length;
const completedCount = tools.filter((t) => t.status === "completed").length;

// Determine the dominant tool type for the header
const toolNames = tools.map((t) => t.name.replace("↪ ", ""));
const uniqueNames = [...new Set(toolNames)];
const headerToolName =
uniqueNames.length === 1 ? `${uniqueNames[0]} calls` : "parallel tools";

let body = `**Running ${totalTools} ${headerToolName}** (${completedCount}/${totalTools} complete)\n`;

for (let i = 0; i < tools.length; i++) {
const tool = tools[i]!;
const isLast = i === tools.length - 1;
const prefix = isLast ? "└─" : "├─";

const statusIcon =
tool.status === "completed" ? (tool.isError ? "❌" : "✅") : "⏳";

const displayName = tool.name.replace("↪ ", "");
const param = this.formatToolParameter(tool.name, tool.input);
// Truncate parameter for readability
const shortParam =
param.length > 80 ? `${param.substring(0, 77)}…` : param;

body += `${prefix} ${statusIcon} **${displayName}**: ${shortParam}\n`;
}

return body;
}
}
34 changes: 34 additions & 0 deletions packages/codex-runner/src/formatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,4 +189,38 @@ export class CodexMessageFormatter implements IMessageFormatter {
}
return normalized;
}

formatParallelToolGroup(
tools: Array<{
name: string;
input: any;
status: "pending" | "completed";
isError?: boolean;
}>,
): string {
const totalTools = tools.length;
const completedCount = tools.filter((t) => t.status === "completed").length;

const toolNames = tools.map((t) => t.name.replace("↪ ", ""));
const uniqueNames = [...new Set(toolNames)];
const headerToolName =
uniqueNames.length === 1 ? `${uniqueNames[0]} calls` : "parallel tools";

let body = `**Running ${totalTools} ${headerToolName}** (${completedCount}/${totalTools} complete)\n`;

for (let i = 0; i < tools.length; i++) {
const tool = tools[i]!;
const isLast = i === tools.length - 1;
const prefix = isLast ? "└─" : "├─";
const statusIcon =
tool.status === "completed" ? (tool.isError ? "❌" : "✅") : "⏳";
const displayName = tool.name.replace("↪ ", "");
const param = this.formatToolParameter(tool.name, tool.input);
const shortParam =
param.length > 80 ? `${param.substring(0, 77)}…` : param;
body += `${prefix} ${statusIcon} **${displayName}**: ${shortParam}\n`;
}

return body;
}
}
12 changes: 12 additions & 0 deletions packages/core/src/agent-runner-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,18 @@ export interface IMessageFormatter {
result: string,
isError: boolean,
): string;
/**
* Format a group of parallel tool calls as a unified view.
* Used for ephemeral activities that show all parallel tools in a tree-like structure.
*/
formatParallelToolGroup(
tools: Array<{
name: string;
input: any;
status: "pending" | "completed";
isError?: boolean;
}>,
): string;
}

/**
Expand Down
34 changes: 34 additions & 0 deletions packages/cursor-runner/src/formatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,4 +189,38 @@ export class CursorMessageFormatter implements IMessageFormatter {
}
return normalized;
}

formatParallelToolGroup(
tools: Array<{
name: string;
input: any;
status: "pending" | "completed";
isError?: boolean;
}>,
): string {
const totalTools = tools.length;
const completedCount = tools.filter((t) => t.status === "completed").length;

const toolNames = tools.map((t) => t.name.replace("↪ ", ""));
const uniqueNames = [...new Set(toolNames)];
const headerToolName =
uniqueNames.length === 1 ? `${uniqueNames[0]} calls` : "parallel tools";

let body = `**Running ${totalTools} ${headerToolName}** (${completedCount}/${totalTools} complete)\n`;

for (let i = 0; i < tools.length; i++) {
const tool = tools[i]!;
const isLast = i === tools.length - 1;
const prefix = isLast ? "└─" : "├─";
const statusIcon =
tool.status === "completed" ? (tool.isError ? "❌" : "✅") : "⏳";
const displayName = tool.name.replace("↪ ", "");
const param = this.formatToolParameter(tool.name, tool.input);
const shortParam =
param.length > 80 ? `${param.substring(0, 77)}…` : param;
body += `${prefix} ${statusIcon} **${displayName}**: ${shortParam}\n`;
}

return body;
}
}
Loading
Loading