mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
feat: enrich subtask handoff with structured context summaries
Phase 1 of #12330 - improves context handoff visibility between parent and child tasks during delegation. Changes: - Add SubtaskSummary type to @roo-code/types for structured handoff data - Create buildSubtaskSummary utility that extracts files modified/read, commands executed, tool usage, and todo stats from task history - Modify AttemptCompletionTool to build structured summary on completion - Update reopenParentFromDelegation to format enriched API history text so the parent LLM gets better context about what the subtask did - Update ChatRow UI to render structured summaries with mode badge, file lists, command lists, and todo progress - Add i18n translation keys for new UI elements - Add 19 tests for buildSubtaskSummary and formatSubtaskSummaryForApi - Backward compatible: plain-text summaries still work as before
This commit is contained in:
parent
22d845cecb
commit
09a5570b58
7 changed files with 659 additions and 8 deletions
|
|
@ -29,3 +29,34 @@ export const historyItemSchema = z.object({
|
|||
})
|
||||
|
||||
export type HistoryItem = z.infer<typeof historyItemSchema>
|
||||
|
||||
/**
|
||||
* SubtaskSummary
|
||||
*
|
||||
* Structured metadata produced when a subtask completes via attempt_completion
|
||||
* and hands off context back to its parent task. This enriches the handoff
|
||||
* with visibility into what the subtask actually did.
|
||||
*/
|
||||
export const subtaskSummarySchema = z.object({
|
||||
/** The completion result text from attempt_completion */
|
||||
result: z.string(),
|
||||
/** Mode slug the subtask ran in (e.g. "code", "architect") */
|
||||
mode: z.string().optional(),
|
||||
/** Files that were created or modified (write_to_file, apply_diff, insert_content) */
|
||||
filesModified: z.array(z.string()).optional(),
|
||||
/** Files that were read during the subtask */
|
||||
filesRead: z.array(z.string()).optional(),
|
||||
/** Shell commands that were executed */
|
||||
commandsExecuted: z.array(z.string()).optional(),
|
||||
/** Summary of tool usage counts: tool name -> number of attempts */
|
||||
toolUsageSummary: z.record(z.string(), z.number()).optional(),
|
||||
/** Todo list status at completion: [completed, total] */
|
||||
todoStats: z
|
||||
.object({
|
||||
completed: z.number(),
|
||||
total: z.number(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
|
||||
export type SubtaskSummary = z.infer<typeof subtaskSummarySchema>
|
||||
|
|
|
|||
308
src/core/task/__tests__/buildSubtaskSummary.spec.ts
Normal file
308
src/core/task/__tests__/buildSubtaskSummary.spec.ts
Normal file
|
|
@ -0,0 +1,308 @@
|
|||
import { buildSubtaskSummary, formatSubtaskSummaryForApi, type SubtaskContext } from "../buildSubtaskSummary"
|
||||
|
||||
function createContext(overrides: Partial<SubtaskContext> = {}): SubtaskContext {
|
||||
return {
|
||||
apiConversationHistory: [],
|
||||
toolUsage: {},
|
||||
todoList: undefined,
|
||||
taskMode: "code",
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
describe("buildSubtaskSummary", () => {
|
||||
it("should return a minimal summary with just result and mode", () => {
|
||||
const context = createContext()
|
||||
const summary = buildSubtaskSummary(context, "Task completed successfully")
|
||||
|
||||
expect(summary.result).toBe("Task completed successfully")
|
||||
expect(summary.mode).toBe("code")
|
||||
expect(summary.filesModified).toBeUndefined()
|
||||
expect(summary.filesRead).toBeUndefined()
|
||||
expect(summary.commandsExecuted).toBeUndefined()
|
||||
expect(summary.toolUsageSummary).toBeUndefined()
|
||||
expect(summary.todoStats).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should extract files modified from write_to_file tool_use blocks", () => {
|
||||
const context = createContext({
|
||||
apiConversationHistory: [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "tool_use",
|
||||
id: "toolu_1",
|
||||
name: "write_to_file",
|
||||
input: { path: "src/index.ts", content: "hello" },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "tool_result", tool_use_id: "toolu_1", content: "ok" }],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const summary = buildSubtaskSummary(context, "Done")
|
||||
expect(summary.filesModified).toEqual(["src/index.ts"])
|
||||
})
|
||||
|
||||
it("should extract files modified from apply_diff tool_use blocks", () => {
|
||||
const context = createContext({
|
||||
apiConversationHistory: [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "tool_use",
|
||||
id: "toolu_2",
|
||||
name: "apply_diff",
|
||||
input: { path: "src/utils.ts", diff: "--- a\n+++ b" },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const summary = buildSubtaskSummary(context, "Done")
|
||||
expect(summary.filesModified).toEqual(["src/utils.ts"])
|
||||
})
|
||||
|
||||
it("should extract files read from read_file tool_use blocks", () => {
|
||||
const context = createContext({
|
||||
apiConversationHistory: [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "tool_use",
|
||||
id: "toolu_3",
|
||||
name: "read_file",
|
||||
input: { path: "package.json" },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const summary = buildSubtaskSummary(context, "Done")
|
||||
expect(summary.filesRead).toEqual(["package.json"])
|
||||
})
|
||||
|
||||
it("should extract commands from execute_command tool_use blocks", () => {
|
||||
const context = createContext({
|
||||
apiConversationHistory: [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "tool_use",
|
||||
id: "toolu_4",
|
||||
name: "execute_command",
|
||||
input: { command: "npm test" },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const summary = buildSubtaskSummary(context, "Done")
|
||||
expect(summary.commandsExecuted).toEqual(["npm test"])
|
||||
})
|
||||
|
||||
it("should truncate very long commands", () => {
|
||||
const longCmd = "a".repeat(200)
|
||||
const context = createContext({
|
||||
apiConversationHistory: [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "tool_use",
|
||||
id: "toolu_5",
|
||||
name: "execute_command",
|
||||
input: { command: longCmd },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const summary = buildSubtaskSummary(context, "Done")
|
||||
expect(summary.commandsExecuted![0].length).toBeLessThanOrEqual(120)
|
||||
expect(summary.commandsExecuted![0].endsWith("...")).toBe(true)
|
||||
})
|
||||
|
||||
it("should deduplicate modified files", () => {
|
||||
const context = createContext({
|
||||
apiConversationHistory: [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "tool_use",
|
||||
id: "toolu_6",
|
||||
name: "write_to_file",
|
||||
input: { path: "src/index.ts", content: "v1" },
|
||||
},
|
||||
],
|
||||
},
|
||||
{ role: "user", content: [{ type: "tool_result", tool_use_id: "toolu_6", content: "ok" }] },
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "tool_use",
|
||||
id: "toolu_7",
|
||||
name: "apply_diff",
|
||||
input: { path: "src/index.ts", diff: "diff" },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const summary = buildSubtaskSummary(context, "Done")
|
||||
expect(summary.filesModified).toEqual(["src/index.ts"])
|
||||
})
|
||||
|
||||
it("should include tool usage summary from toolUsage", () => {
|
||||
const context = createContext({
|
||||
toolUsage: {
|
||||
write_to_file: { attempts: 3, failures: 0 },
|
||||
read_file: { attempts: 5, failures: 1 },
|
||||
} as any,
|
||||
})
|
||||
|
||||
const summary = buildSubtaskSummary(context, "Done")
|
||||
expect(summary.toolUsageSummary).toEqual({
|
||||
write_to_file: 3,
|
||||
read_file: 5,
|
||||
})
|
||||
})
|
||||
|
||||
it("should include todo stats when todoList is present", () => {
|
||||
const context = createContext({
|
||||
todoList: [
|
||||
{ id: "1", task: "Do A", status: "completed" },
|
||||
{ id: "2", task: "Do B", status: "completed" },
|
||||
{ id: "3", task: "Do C", status: "pending" },
|
||||
] as any,
|
||||
})
|
||||
|
||||
const summary = buildSubtaskSummary(context, "Done")
|
||||
expect(summary.todoStats).toEqual({ completed: 2, total: 3 })
|
||||
})
|
||||
|
||||
it("should skip user messages when scanning for tool_use blocks", () => {
|
||||
const context = createContext({
|
||||
apiConversationHistory: [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "tool_result" as any,
|
||||
tool_use_id: "toolu_x",
|
||||
content: "ok",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const summary = buildSubtaskSummary(context, "Done")
|
||||
expect(summary.filesModified).toBeUndefined()
|
||||
expect(summary.commandsExecuted).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should handle empty conversation history", () => {
|
||||
const context = createContext({ apiConversationHistory: [] })
|
||||
const summary = buildSubtaskSummary(context, "Nothing happened")
|
||||
|
||||
expect(summary.result).toBe("Nothing happened")
|
||||
expect(summary.mode).toBe("code")
|
||||
})
|
||||
|
||||
it("should handle messages with non-array content (string content)", () => {
|
||||
const context = createContext({
|
||||
apiConversationHistory: [
|
||||
{
|
||||
role: "assistant",
|
||||
content: "Just text response",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const summary = buildSubtaskSummary(context, "Done")
|
||||
expect(summary.filesModified).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("formatSubtaskSummaryForApi", () => {
|
||||
it("should format a minimal summary", () => {
|
||||
const text = formatSubtaskSummaryForApi({ result: "All done" })
|
||||
expect(text).toContain("## Result\nAll done")
|
||||
})
|
||||
|
||||
it("should include mode section", () => {
|
||||
const text = formatSubtaskSummaryForApi({ result: "Done", mode: "architect" })
|
||||
expect(text).toContain("## Mode\narchitect")
|
||||
})
|
||||
|
||||
it("should include files modified section", () => {
|
||||
const text = formatSubtaskSummaryForApi({
|
||||
result: "Done",
|
||||
filesModified: ["src/a.ts", "src/b.ts"],
|
||||
})
|
||||
expect(text).toContain("## Files Modified")
|
||||
expect(text).toContain("- src/a.ts")
|
||||
expect(text).toContain("- src/b.ts")
|
||||
})
|
||||
|
||||
it("should include files read section", () => {
|
||||
const text = formatSubtaskSummaryForApi({
|
||||
result: "Done",
|
||||
filesRead: ["package.json"],
|
||||
})
|
||||
expect(text).toContain("## Files Read")
|
||||
expect(text).toContain("- package.json")
|
||||
})
|
||||
|
||||
it("should include commands section", () => {
|
||||
const text = formatSubtaskSummaryForApi({
|
||||
result: "Done",
|
||||
commandsExecuted: ["npm test", "npm build"],
|
||||
})
|
||||
expect(text).toContain("## Commands Executed")
|
||||
expect(text).toContain("- `npm test`")
|
||||
expect(text).toContain("- `npm build`")
|
||||
})
|
||||
|
||||
it("should include todo stats", () => {
|
||||
const text = formatSubtaskSummaryForApi({
|
||||
result: "Done",
|
||||
todoStats: { completed: 3, total: 5 },
|
||||
})
|
||||
expect(text).toContain("## Todos\n3/5 completed")
|
||||
})
|
||||
|
||||
it("should format a comprehensive summary with all sections", () => {
|
||||
const text = formatSubtaskSummaryForApi({
|
||||
result: "Implemented the feature",
|
||||
mode: "code",
|
||||
filesModified: ["src/feature.ts"],
|
||||
filesRead: ["src/config.ts"],
|
||||
commandsExecuted: ["npm test"],
|
||||
todoStats: { completed: 2, total: 2 },
|
||||
})
|
||||
|
||||
expect(text).toContain("## Result")
|
||||
expect(text).toContain("## Mode")
|
||||
expect(text).toContain("## Files Modified")
|
||||
expect(text).toContain("## Files Read")
|
||||
expect(text).toContain("## Commands Executed")
|
||||
expect(text).toContain("## Todos")
|
||||
})
|
||||
})
|
||||
189
src/core/task/buildSubtaskSummary.ts
Normal file
189
src/core/task/buildSubtaskSummary.ts
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
import type { SubtaskSummary } from "@roo-code/types"
|
||||
import type { ToolUsage } from "@roo-code/types"
|
||||
import type { TodoItem } from "@roo-code/types"
|
||||
import type Anthropic from "@anthropic-ai/sdk"
|
||||
|
||||
/**
|
||||
* File-modifying tool names. When these appear as tool_use blocks in the
|
||||
* API conversation history, the first positional argument (typically `path`)
|
||||
* is extracted as a modified file.
|
||||
*/
|
||||
const FILE_WRITE_TOOLS = new Set(["write_to_file", "apply_diff", "insert_content"])
|
||||
|
||||
/**
|
||||
* File-reading tool names.
|
||||
*/
|
||||
const FILE_READ_TOOLS = new Set(["read_file", "search_files", "list_files", "list_code_definition_names"])
|
||||
|
||||
/**
|
||||
* Extract a file path from a tool_use input object.
|
||||
* Native tool calls store params as structured objects with a `path` field.
|
||||
*/
|
||||
function extractPath(input: Record<string, unknown>): string | undefined {
|
||||
if (typeof input.path === "string" && input.path.length > 0) {
|
||||
return input.path
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a command string from a tool_use input for execute_command.
|
||||
*/
|
||||
function extractCommand(input: Record<string, unknown>): string | undefined {
|
||||
if (typeof input.command === "string" && input.command.length > 0) {
|
||||
const cmd = input.command
|
||||
return cmd.length > 120 ? cmd.slice(0, 117) + "..." : cmd
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimal interface representing the data we need from a Task instance.
|
||||
* Using an interface avoids importing the full Task class (circular deps).
|
||||
*/
|
||||
export interface SubtaskContext {
|
||||
apiConversationHistory: Anthropic.MessageParam[]
|
||||
toolUsage: ToolUsage
|
||||
todoList?: TodoItem[]
|
||||
taskMode: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a structured SubtaskSummary from task context.
|
||||
*
|
||||
* This scans the task's API conversation history to extract:
|
||||
* - Files modified (write_to_file, apply_diff, insert_content)
|
||||
* - Files read (read_file, search_files, etc.)
|
||||
* - Commands executed (execute_command)
|
||||
* - Tool usage summary (from toolUsage)
|
||||
* - Todo completion stats (from todoList)
|
||||
*
|
||||
* The result text comes from attempt_completion and is passed in separately.
|
||||
*/
|
||||
export function buildSubtaskSummary(context: SubtaskContext, completionResult: string): SubtaskSummary {
|
||||
const filesModified = new Set<string>()
|
||||
const filesRead = new Set<string>()
|
||||
const commandsExecuted: string[] = []
|
||||
|
||||
// Scan API conversation history for tool_use blocks
|
||||
for (const message of context.apiConversationHistory) {
|
||||
if (message.role !== "assistant" || !Array.isArray(message.content)) {
|
||||
continue
|
||||
}
|
||||
|
||||
for (const block of message.content as Anthropic.ContentBlockParam[]) {
|
||||
if (block.type !== "tool_use") {
|
||||
continue
|
||||
}
|
||||
|
||||
const toolBlock = block as Anthropic.ToolUseBlockParam
|
||||
const toolName = toolBlock.name
|
||||
const input = (toolBlock.input ?? {}) as Record<string, unknown>
|
||||
|
||||
if (FILE_WRITE_TOOLS.has(toolName)) {
|
||||
const path = extractPath(input)
|
||||
if (path) {
|
||||
filesModified.add(path)
|
||||
}
|
||||
} else if (FILE_READ_TOOLS.has(toolName)) {
|
||||
const path = extractPath(input)
|
||||
if (path) {
|
||||
filesRead.add(path)
|
||||
}
|
||||
} else if (toolName === "execute_command") {
|
||||
const cmd = extractCommand(input)
|
||||
if (cmd) {
|
||||
commandsExecuted.push(cmd)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build tool usage summary from toolUsage
|
||||
const toolUsageSummary: Record<string, number> = {}
|
||||
if (context.toolUsage) {
|
||||
for (const [toolName, usage] of Object.entries(context.toolUsage)) {
|
||||
const u = usage as { attempts: number; failures: number } | undefined
|
||||
if (u && u.attempts > 0) {
|
||||
toolUsageSummary[toolName] = u.attempts
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build todo stats
|
||||
let todoStats: SubtaskSummary["todoStats"]
|
||||
if (context.todoList && context.todoList.length > 0) {
|
||||
const completed = context.todoList.filter((t: TodoItem) => t.status === "completed").length
|
||||
todoStats = { completed, total: context.todoList.length }
|
||||
}
|
||||
|
||||
const summary: SubtaskSummary = {
|
||||
result: completionResult,
|
||||
mode: context.taskMode,
|
||||
}
|
||||
|
||||
if (filesModified.size > 0) {
|
||||
summary.filesModified = Array.from(filesModified)
|
||||
}
|
||||
|
||||
if (filesRead.size > 0) {
|
||||
summary.filesRead = Array.from(filesRead)
|
||||
}
|
||||
|
||||
if (commandsExecuted.length > 0) {
|
||||
summary.commandsExecuted = commandsExecuted
|
||||
}
|
||||
|
||||
if (Object.keys(toolUsageSummary).length > 0) {
|
||||
summary.toolUsageSummary = toolUsageSummary
|
||||
}
|
||||
|
||||
if (todoStats) {
|
||||
summary.todoStats = todoStats
|
||||
}
|
||||
|
||||
return summary
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a SubtaskSummary into a human-readable string suitable for
|
||||
* injection into the parent's API history (tool_result content).
|
||||
* This enriched format gives the parent LLM much better context about
|
||||
* what the subtask accomplished.
|
||||
*/
|
||||
export function formatSubtaskSummaryForApi(summary: SubtaskSummary): string {
|
||||
const sections: string[] = []
|
||||
|
||||
// Result section (always present)
|
||||
sections.push(`## Result\n${summary.result}`)
|
||||
|
||||
// Mode
|
||||
if (summary.mode) {
|
||||
sections.push(`## Mode\n${summary.mode}`)
|
||||
}
|
||||
|
||||
// Files modified
|
||||
if (summary.filesModified && summary.filesModified.length > 0) {
|
||||
const fileList = summary.filesModified.map((f: string) => `- ${f}`).join("\n")
|
||||
sections.push(`## Files Modified\n${fileList}`)
|
||||
}
|
||||
|
||||
// Files read
|
||||
if (summary.filesRead && summary.filesRead.length > 0) {
|
||||
const fileList = summary.filesRead.map((f: string) => `- ${f}`).join("\n")
|
||||
sections.push(`## Files Read\n${fileList}`)
|
||||
}
|
||||
|
||||
// Commands executed
|
||||
if (summary.commandsExecuted && summary.commandsExecuted.length > 0) {
|
||||
const cmdList = summary.commandsExecuted.map((c: string) => `- \`${c}\``).join("\n")
|
||||
sections.push(`## Commands Executed\n${cmdList}`)
|
||||
}
|
||||
|
||||
// Todo stats
|
||||
if (summary.todoStats) {
|
||||
sections.push(`## Todos\n${summary.todoStats.completed}/${summary.todoStats.total} completed`)
|
||||
}
|
||||
|
||||
return sections.join("\n\n")
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import { formatResponse } from "../prompts/responses"
|
|||
import { Package } from "../../shared/package"
|
||||
import type { ToolUse } from "../../shared/tools"
|
||||
import { t } from "../../i18n"
|
||||
import { buildSubtaskSummary } from "../task/buildSubtaskSummary"
|
||||
|
||||
import { BaseTool, ToolCallbacks } from "./BaseTool"
|
||||
|
||||
|
|
@ -168,10 +169,31 @@ export class AttemptCompletionTool extends BaseTool<"attempt_completion"> {
|
|||
|
||||
pushToolResult("")
|
||||
|
||||
// Build a structured summary of what this subtask accomplished.
|
||||
// This enriches the handoff with files changed, tools used, etc.
|
||||
// Wrapped in try/catch: if summary building fails (e.g. mode not initialized),
|
||||
// we fall back to the plain result string for backward compatibility.
|
||||
let completionResultSummary: string
|
||||
try {
|
||||
const summary = buildSubtaskSummary(
|
||||
{
|
||||
apiConversationHistory: task.apiConversationHistory,
|
||||
toolUsage: task.toolUsage,
|
||||
todoList: task.todoList ?? undefined,
|
||||
taskMode: task.taskMode,
|
||||
},
|
||||
result,
|
||||
)
|
||||
completionResultSummary = JSON.stringify(summary)
|
||||
} catch {
|
||||
// Fallback: use plain result text if structured summary cannot be built
|
||||
completionResultSummary = result
|
||||
}
|
||||
|
||||
await provider.reopenParentFromDelegation({
|
||||
parentTaskId: task.parentTaskId!,
|
||||
childTaskId: task.taskId,
|
||||
completionResultSummary: result,
|
||||
completionResultSummary,
|
||||
})
|
||||
|
||||
return "delegated"
|
||||
|
|
|
|||
|
|
@ -92,6 +92,8 @@ import { getNonce } from "./getNonce"
|
|||
import { getUri } from "./getUri"
|
||||
import { REQUESTY_BASE_URL } from "../../shared/utils/requesty"
|
||||
import { validateAndFixToolResultIds } from "../task/validateToolResultIds"
|
||||
import { formatSubtaskSummaryForApi } from "../task/buildSubtaskSummary"
|
||||
import type { SubtaskSummary } from "@roo-code/types"
|
||||
|
||||
/**
|
||||
* https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
|
||||
|
|
@ -3183,6 +3185,21 @@ export class ClineProvider
|
|||
if (!Array.isArray(parentClineMessages)) parentClineMessages = []
|
||||
if (!Array.isArray(parentApiMessages)) parentApiMessages = []
|
||||
|
||||
// Try to parse completionResultSummary as a structured SubtaskSummary (JSON).
|
||||
// If it's not valid JSON, treat it as a plain-text result for backward compatibility.
|
||||
let parsedSummary: SubtaskSummary | undefined
|
||||
let apiResultText: string
|
||||
try {
|
||||
parsedSummary = JSON.parse(completionResultSummary) as SubtaskSummary
|
||||
// Use the enriched format for API history so the parent LLM gets structured context
|
||||
apiResultText = `Subtask ${childTaskId} completed.\n\n${formatSubtaskSummaryForApi(parsedSummary)}`
|
||||
} catch {
|
||||
// Not JSON - plain text result (backward compatible path)
|
||||
apiResultText = `Subtask ${childTaskId} completed.\n\nResult:\n${completionResultSummary}`
|
||||
}
|
||||
|
||||
// For the UI message, pass the raw completionResultSummary (JSON or plain text).
|
||||
// The webview ChatRow component will detect JSON and render structured data.
|
||||
const subtaskUiMessage: ClineMessage = {
|
||||
type: "say",
|
||||
say: "subtask_result",
|
||||
|
|
@ -3218,8 +3235,8 @@ export class ClineProvider
|
|||
if (lastMsg?.role === "user" && Array.isArray(lastMsg.content)) {
|
||||
for (const block of lastMsg.content) {
|
||||
if (block.type === "tool_result" && block.tool_use_id === toolUseId) {
|
||||
// Update the existing tool_result content
|
||||
block.content = `Subtask ${childTaskId} completed.\n\nResult:\n${completionResultSummary}`
|
||||
// Update the existing tool_result content with enriched summary
|
||||
block.content = apiResultText
|
||||
alreadyHasToolResult = true
|
||||
break
|
||||
}
|
||||
|
|
@ -3234,7 +3251,7 @@ export class ClineProvider
|
|||
{
|
||||
type: "tool_result" as const,
|
||||
tool_use_id: toolUseId,
|
||||
content: `Subtask ${childTaskId} completed.\n\nResult:\n${completionResultSummary}`,
|
||||
content: apiResultText,
|
||||
},
|
||||
],
|
||||
ts,
|
||||
|
|
@ -3257,7 +3274,7 @@ export class ClineProvider
|
|||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: `Subtask ${childTaskId} completed.\n\nResult:\n${completionResultSummary}`,
|
||||
text: apiResultText,
|
||||
},
|
||||
],
|
||||
ts,
|
||||
|
|
|
|||
|
|
@ -1020,16 +1020,95 @@ export const ChatRowContent = ({
|
|||
showCopyButton={true}
|
||||
/>
|
||||
)
|
||||
case "subtask_result":
|
||||
case "subtask_result": {
|
||||
// Get the child task ID that produced this result
|
||||
const completedChildTaskId = currentTaskItem?.completedByChildId
|
||||
|
||||
// Try to parse structured summary (JSON). Falls back to plain text.
|
||||
let structuredSummary: {
|
||||
result?: string
|
||||
mode?: string
|
||||
filesModified?: string[]
|
||||
filesRead?: string[]
|
||||
commandsExecuted?: string[]
|
||||
toolUsageSummary?: Record<string, number>
|
||||
todoStats?: { completed: number; total: number }
|
||||
} | null = null
|
||||
try {
|
||||
if (message.text?.startsWith("{")) {
|
||||
structuredSummary = JSON.parse(message.text)
|
||||
}
|
||||
} catch {
|
||||
// Not JSON, use plain text rendering
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="border-l border-muted-foreground/80 ml-2 pl-4 pt-2 pb-1 -mt-5">
|
||||
<div style={headerStyle}>
|
||||
<span style={{ fontWeight: "bold" }}>{t("chat:subtasks.resultContent")}</span>
|
||||
<Check className="size-3" />
|
||||
</div>
|
||||
<MarkdownBlock markdown={message.text} />
|
||||
|
||||
{structuredSummary ? (
|
||||
<div className="text-sm">
|
||||
{structuredSummary.mode && (
|
||||
<div className="mb-2">
|
||||
<span className="inline-block text-xs px-1.5 py-0.5 rounded border border-vscode-dropdown-border/50 text-vscode-descriptionForeground">
|
||||
{structuredSummary.mode}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{structuredSummary.result && <MarkdownBlock markdown={structuredSummary.result} />}
|
||||
|
||||
{structuredSummary.filesModified && structuredSummary.filesModified.length > 0 && (
|
||||
<div className="mt-2">
|
||||
<div className="text-xs font-semibold text-vscode-descriptionForeground mb-1">
|
||||
{t("chat:subtasks.filesModified")}
|
||||
</div>
|
||||
<ul className="list-none m-0 p-0">
|
||||
{structuredSummary.filesModified.map((f: string, i: number) => (
|
||||
<li
|
||||
key={i}
|
||||
className="text-xs text-vscode-descriptionForeground pl-2">
|
||||
{f}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{structuredSummary.commandsExecuted &&
|
||||
structuredSummary.commandsExecuted.length > 0 && (
|
||||
<div className="mt-2">
|
||||
<div className="text-xs font-semibold text-vscode-descriptionForeground mb-1">
|
||||
{t("chat:subtasks.commandsExecuted")}
|
||||
</div>
|
||||
<ul className="list-none m-0 p-0">
|
||||
{structuredSummary.commandsExecuted.map((c: string, i: number) => (
|
||||
<li
|
||||
key={i}
|
||||
className="text-xs text-vscode-descriptionForeground pl-2 font-mono">
|
||||
{c}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{structuredSummary.todoStats && (
|
||||
<div className="mt-2 text-xs text-vscode-descriptionForeground">
|
||||
{t("chat:subtasks.todoStats", {
|
||||
completed: structuredSummary.todoStats.completed,
|
||||
total: structuredSummary.todoStats.total,
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<MarkdownBlock markdown={message.text} />
|
||||
)}
|
||||
|
||||
{completedChildTaskId && (
|
||||
<button
|
||||
className="cursor-pointer flex gap-1 items-center mt-2 text-vscode-descriptionForeground hover:text-vscode-descriptionForeground hover:underline font-normal"
|
||||
|
|
@ -1042,6 +1121,7 @@ export const ChatRowContent = ({
|
|||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
case "reasoning":
|
||||
return (
|
||||
<ReasoningBlock
|
||||
|
|
|
|||
|
|
@ -309,7 +309,11 @@
|
|||
"resultContent": "Subtask completed",
|
||||
"defaultResult": "Please continue to the next task.",
|
||||
"completionInstructions": "You can review the results and suggest any corrections or next steps. If everything looks good, confirm to return the result to the parent task.",
|
||||
"goToSubtask": "View task"
|
||||
"goToSubtask": "View task",
|
||||
"filesModified": "Files Modified",
|
||||
"filesRead": "Files Read",
|
||||
"commandsExecuted": "Commands Executed",
|
||||
"todoStats": "Todos: {{completed}}/{{total}} completed"
|
||||
},
|
||||
"questions": {
|
||||
"hasQuestion": "Roo has a question"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue