mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
feat: add getTaskAsMarkdown API method for programmatic markdown export
- Add formatTaskAsMarkdown utility to convert ClineMessage arrays to markdown - Implement getTaskAsMarkdown method in API class to retrieve task content as markdown - Support both current active task and historical tasks by ID - Add comprehensive tests for formatting and API functionality - Update RooCodeAPI interface to include new method - Fix ESLint warnings by wrapping case blocks with curly braces This addresses issue #9858 by providing a programmatic way to access the "Copy as markdown" functionality that was previously only available through the UI.
This commit is contained in:
parent
dd92453276
commit
93ffcee40d
5 changed files with 782 additions and 0 deletions
|
|
@ -133,6 +133,12 @@ export interface RooCodeAPI extends EventEmitter<RooCodeAPIEvents> {
|
|||
* @throws Error if the profile does not exist
|
||||
*/
|
||||
setActiveProfile(name: string): Promise<string | undefined>
|
||||
/**
|
||||
* Gets the task content formatted as markdown.
|
||||
* @param taskId Optional task ID. If not provided, uses the current active task.
|
||||
* @returns The task content formatted as markdown, or undefined if no task is found.
|
||||
*/
|
||||
getTaskAsMarkdown(taskId?: string): Promise<string | undefined>
|
||||
}
|
||||
|
||||
export interface RooCodeIpcServer extends EventEmitter<IpcServerEvents> {
|
||||
|
|
|
|||
344
src/core/task/__tests__/formatTaskAsMarkdown.spec.ts
Normal file
344
src/core/task/__tests__/formatTaskAsMarkdown.spec.ts
Normal file
|
|
@ -0,0 +1,344 @@
|
|||
import { describe, it, expect } from "vitest"
|
||||
import type { ClineMessage } from "@roo-code/types"
|
||||
import { formatTaskAsMarkdown } from "../formatTaskAsMarkdown"
|
||||
|
||||
describe("formatTaskAsMarkdown", () => {
|
||||
it("should return empty string for empty messages array", () => {
|
||||
const result = formatTaskAsMarkdown([])
|
||||
expect(result).toBe("")
|
||||
})
|
||||
|
||||
it("should return empty string for null/undefined messages", () => {
|
||||
expect(formatTaskAsMarkdown(null as any)).toBe("")
|
||||
expect(formatTaskAsMarkdown(undefined as any)).toBe("")
|
||||
})
|
||||
|
||||
it("should format user feedback messages", () => {
|
||||
const messages: ClineMessage[] = [
|
||||
{
|
||||
ts: 1000,
|
||||
type: "say",
|
||||
say: "user_feedback",
|
||||
text: "Hello, can you help me?",
|
||||
},
|
||||
]
|
||||
const result = formatTaskAsMarkdown(messages)
|
||||
expect(result).toContain("## Human")
|
||||
expect(result).toContain("Hello, can you help me?")
|
||||
})
|
||||
|
||||
it("should format assistant text messages", () => {
|
||||
const messages: ClineMessage[] = [
|
||||
{
|
||||
ts: 1000,
|
||||
type: "say",
|
||||
say: "text",
|
||||
text: "Sure, I can help you with that.",
|
||||
},
|
||||
]
|
||||
const result = formatTaskAsMarkdown(messages)
|
||||
expect(result).toContain("## Assistant")
|
||||
expect(result).toContain("Sure, I can help you with that.")
|
||||
})
|
||||
|
||||
it("should format error messages", () => {
|
||||
const messages: ClineMessage[] = [
|
||||
{
|
||||
ts: 1000,
|
||||
type: "say",
|
||||
say: "error",
|
||||
text: "An error occurred",
|
||||
},
|
||||
]
|
||||
const result = formatTaskAsMarkdown(messages)
|
||||
expect(result).toContain("## Error")
|
||||
expect(result).toContain("```\nAn error occurred\n```")
|
||||
})
|
||||
|
||||
it("should format completion result messages", () => {
|
||||
const messages: ClineMessage[] = [
|
||||
{
|
||||
ts: 1000,
|
||||
type: "say",
|
||||
say: "completion_result",
|
||||
text: "Task completed successfully!",
|
||||
},
|
||||
]
|
||||
const result = formatTaskAsMarkdown(messages)
|
||||
expect(result).toContain("## Task Completed")
|
||||
expect(result).toContain("Task completed successfully!")
|
||||
})
|
||||
|
||||
it("should format reasoning messages with blockquote", () => {
|
||||
const messages: ClineMessage[] = [
|
||||
{
|
||||
ts: 1000,
|
||||
type: "say",
|
||||
say: "reasoning",
|
||||
text: "I need to analyze this\nStep by step",
|
||||
},
|
||||
]
|
||||
const result = formatTaskAsMarkdown(messages)
|
||||
expect(result).toContain("## Reasoning")
|
||||
expect(result).toContain("> I need to analyze this\n> Step by step")
|
||||
})
|
||||
|
||||
it("should format command execution messages", () => {
|
||||
const messages: ClineMessage[] = [
|
||||
{
|
||||
ts: 1000,
|
||||
type: "ask",
|
||||
ask: "command",
|
||||
text: "npm install",
|
||||
},
|
||||
]
|
||||
const result = formatTaskAsMarkdown(messages)
|
||||
expect(result).toContain("## Command Execution")
|
||||
expect(result).toContain("```bash\nnpm install\n```")
|
||||
})
|
||||
|
||||
it("should format tool use messages for file operations", () => {
|
||||
const messages: ClineMessage[] = [
|
||||
{
|
||||
ts: 1000,
|
||||
type: "ask",
|
||||
ask: "tool",
|
||||
text: JSON.stringify({
|
||||
tool: "readFile",
|
||||
path: "src/index.ts",
|
||||
}),
|
||||
},
|
||||
{
|
||||
ts: 1001,
|
||||
type: "ask",
|
||||
ask: "tool",
|
||||
text: JSON.stringify({
|
||||
tool: "newFileCreated",
|
||||
path: "src/new.ts",
|
||||
content: "const x = 1;",
|
||||
}),
|
||||
},
|
||||
{
|
||||
ts: 1002,
|
||||
type: "ask",
|
||||
ask: "tool",
|
||||
text: JSON.stringify({
|
||||
tool: "editedExistingFile",
|
||||
path: "src/existing.ts",
|
||||
diff: "- old line\n+ new line",
|
||||
}),
|
||||
},
|
||||
]
|
||||
const result = formatTaskAsMarkdown(messages)
|
||||
expect(result).toContain("## Reading File")
|
||||
expect(result).toContain("**Path:** `src/index.ts`")
|
||||
expect(result).toContain("## Creating File")
|
||||
expect(result).toContain("**Path:** `src/new.ts`")
|
||||
expect(result).toContain("## Editing File")
|
||||
expect(result).toContain("```diff\n- old line\n+ new line\n```")
|
||||
})
|
||||
|
||||
it("should format updateTodoList tool messages", () => {
|
||||
const messages: ClineMessage[] = [
|
||||
{
|
||||
ts: 1000,
|
||||
type: "ask",
|
||||
ask: "tool",
|
||||
text: JSON.stringify({
|
||||
tool: "updateTodoList",
|
||||
todos: ["- [ ] Task 1", "- [x] Task 2", "- [ ] Task 3"],
|
||||
}),
|
||||
},
|
||||
]
|
||||
const result = formatTaskAsMarkdown(messages)
|
||||
expect(result).toContain("## Updated TODO List")
|
||||
expect(result).toContain("- [ ] Task 1")
|
||||
expect(result).toContain("- [x] Task 2")
|
||||
expect(result).toContain("- [ ] Task 3")
|
||||
})
|
||||
|
||||
it("should format follow-up questions", () => {
|
||||
const messages: ClineMessage[] = [
|
||||
{
|
||||
ts: 1000,
|
||||
type: "ask",
|
||||
ask: "followup",
|
||||
text: JSON.stringify({
|
||||
question: "Which framework would you like to use?",
|
||||
suggest: ["React", "Vue", "Angular"],
|
||||
}),
|
||||
},
|
||||
]
|
||||
const result = formatTaskAsMarkdown(messages)
|
||||
expect(result).toContain("## Question")
|
||||
expect(result).toContain("Which framework would you like to use?")
|
||||
})
|
||||
|
||||
it("should handle messages with images", () => {
|
||||
const messages: ClineMessage[] = [
|
||||
{
|
||||
ts: 1000,
|
||||
type: "say",
|
||||
say: "user_feedback",
|
||||
text: "Here's a screenshot",
|
||||
images: ["data:image/png;base64,abc123", "data:image/png;base64,def456"],
|
||||
},
|
||||
]
|
||||
const result = formatTaskAsMarkdown(messages)
|
||||
expect(result).toContain("## Human")
|
||||
expect(result).toContain("Here's a screenshot")
|
||||
expect(result).toContain("*[2 image(s) attached]*")
|
||||
})
|
||||
|
||||
it("should separate multiple messages with dividers", () => {
|
||||
const messages: ClineMessage[] = [
|
||||
{
|
||||
ts: 1000,
|
||||
type: "say",
|
||||
say: "user_feedback",
|
||||
text: "First message",
|
||||
},
|
||||
{
|
||||
ts: 1001,
|
||||
type: "say",
|
||||
say: "text",
|
||||
text: "Second message",
|
||||
},
|
||||
{
|
||||
ts: 1002,
|
||||
type: "say",
|
||||
say: "user_feedback",
|
||||
text: "Third message",
|
||||
},
|
||||
]
|
||||
const result = formatTaskAsMarkdown(messages)
|
||||
expect(result).toContain(
|
||||
"## Human\n\nFirst message\n\n---\n\n## Assistant\n\nSecond message\n\n---\n\n## Human\n\nThird message",
|
||||
)
|
||||
})
|
||||
|
||||
it("should skip api_req_started and api_req_finished messages", () => {
|
||||
const messages: ClineMessage[] = [
|
||||
{
|
||||
ts: 1000,
|
||||
type: "say",
|
||||
say: "api_req_started",
|
||||
text: '{"cost": 0.01}',
|
||||
},
|
||||
{
|
||||
ts: 1001,
|
||||
type: "say",
|
||||
say: "api_req_finished",
|
||||
text: "finished",
|
||||
},
|
||||
{
|
||||
ts: 1002,
|
||||
type: "say",
|
||||
say: "text",
|
||||
text: "Actual content",
|
||||
},
|
||||
]
|
||||
const result = formatTaskAsMarkdown(messages)
|
||||
expect(result).not.toContain("api_req_started")
|
||||
expect(result).not.toContain("api_req_finished")
|
||||
expect(result).toContain("## Assistant")
|
||||
expect(result).toContain("Actual content")
|
||||
})
|
||||
|
||||
it("should format file listing operations", () => {
|
||||
const messages: ClineMessage[] = [
|
||||
{
|
||||
ts: 1000,
|
||||
type: "ask",
|
||||
ask: "tool",
|
||||
text: JSON.stringify({
|
||||
tool: "listFilesTopLevel",
|
||||
path: "/src",
|
||||
content: "file1.ts\nfile2.ts",
|
||||
}),
|
||||
},
|
||||
{
|
||||
ts: 1001,
|
||||
type: "ask",
|
||||
ask: "tool",
|
||||
text: JSON.stringify({
|
||||
tool: "listFilesRecursive",
|
||||
path: "/src",
|
||||
content: "src/\n file1.ts\n subfolder/\n file2.ts",
|
||||
}),
|
||||
},
|
||||
]
|
||||
const result = formatTaskAsMarkdown(messages)
|
||||
expect(result).toContain("## Listing Files")
|
||||
expect(result).toContain("## Listing Files (Recursive)")
|
||||
expect(result).toContain("**Path:** `/src`")
|
||||
})
|
||||
|
||||
it("should format search operations", () => {
|
||||
const messages: ClineMessage[] = [
|
||||
{
|
||||
ts: 1000,
|
||||
type: "ask",
|
||||
ask: "tool",
|
||||
text: JSON.stringify({
|
||||
tool: "searchFiles",
|
||||
regex: "TODO",
|
||||
path: "/src",
|
||||
content: "Found 3 matches",
|
||||
}),
|
||||
},
|
||||
]
|
||||
const result = formatTaskAsMarkdown(messages)
|
||||
expect(result).toContain("## Searching Files")
|
||||
expect(result).toContain("**Pattern:** `TODO`")
|
||||
expect(result).toContain("**Path:** `/src`")
|
||||
expect(result).toContain("```\nFound 3 matches\n```")
|
||||
})
|
||||
|
||||
it("should format mode switch operations", () => {
|
||||
const messages: ClineMessage[] = [
|
||||
{
|
||||
ts: 1000,
|
||||
type: "ask",
|
||||
ask: "tool",
|
||||
text: JSON.stringify({
|
||||
tool: "switchMode",
|
||||
mode: "architect",
|
||||
reason: "Need to plan the architecture first",
|
||||
}),
|
||||
},
|
||||
]
|
||||
const result = formatTaskAsMarkdown(messages)
|
||||
expect(result).toContain("## Switching Mode")
|
||||
expect(result).toContain("**Mode:** architect")
|
||||
expect(result).toContain("**Reason:** Need to plan the architecture first")
|
||||
})
|
||||
|
||||
it("should handle malformed JSON gracefully", () => {
|
||||
const messages: ClineMessage[] = [
|
||||
{
|
||||
ts: 1000,
|
||||
type: "ask",
|
||||
ask: "tool",
|
||||
text: "{ invalid json }",
|
||||
},
|
||||
]
|
||||
const result = formatTaskAsMarkdown(messages)
|
||||
// Should not throw and should return empty for this message
|
||||
expect(result).toBe("")
|
||||
})
|
||||
|
||||
it("should handle unknown message types with generic formatting", () => {
|
||||
const messages: ClineMessage[] = [
|
||||
{
|
||||
ts: 1000,
|
||||
type: "say",
|
||||
say: "unknown_type" as any,
|
||||
text: "Some unknown content",
|
||||
},
|
||||
]
|
||||
const result = formatTaskAsMarkdown(messages)
|
||||
expect(result).toContain("## unknown_type")
|
||||
expect(result).toContain("Some unknown content")
|
||||
})
|
||||
})
|
||||
222
src/core/task/formatTaskAsMarkdown.ts
Normal file
222
src/core/task/formatTaskAsMarkdown.ts
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
import type { ClineMessage } from "@roo-code/types"
|
||||
import { safeJsonParse } from "../../shared/safeJsonParse"
|
||||
|
||||
/**
|
||||
* Formats task messages as markdown, similar to how the UI displays them.
|
||||
* This follows the same formatting logic used in the webview UI components.
|
||||
* @param messages Array of ClineMessage objects from a task
|
||||
* @returns Formatted markdown string
|
||||
*/
|
||||
export function formatTaskAsMarkdown(messages: ClineMessage[]): string {
|
||||
if (!messages || messages.length === 0) {
|
||||
return ""
|
||||
}
|
||||
|
||||
const markdownParts: string[] = []
|
||||
|
||||
for (const message of messages) {
|
||||
const messageMarkdown = formatMessageAsMarkdown(message)
|
||||
if (messageMarkdown) {
|
||||
markdownParts.push(messageMarkdown)
|
||||
}
|
||||
}
|
||||
|
||||
return markdownParts.join("\n\n---\n\n")
|
||||
}
|
||||
|
||||
function formatMessageAsMarkdown(message: ClineMessage): string {
|
||||
const parts: string[] = []
|
||||
|
||||
// Handle different message types
|
||||
if (message.type === "say") {
|
||||
switch (message.say) {
|
||||
case "user_feedback":
|
||||
parts.push("## Human")
|
||||
if (message.text) {
|
||||
parts.push(message.text)
|
||||
}
|
||||
if (message.images && message.images.length > 0) {
|
||||
parts.push(`*[${message.images.length} image(s) attached]*`)
|
||||
}
|
||||
break
|
||||
|
||||
case "text":
|
||||
parts.push("## Assistant")
|
||||
if (message.text) {
|
||||
parts.push(message.text)
|
||||
}
|
||||
break
|
||||
|
||||
case "error":
|
||||
parts.push("## Error")
|
||||
if (message.text) {
|
||||
parts.push(`\`\`\`\n${message.text}\n\`\`\``)
|
||||
}
|
||||
break
|
||||
|
||||
case "completion_result":
|
||||
parts.push("## Task Completed")
|
||||
if (message.text) {
|
||||
parts.push(message.text)
|
||||
}
|
||||
break
|
||||
|
||||
case "api_req_started":
|
||||
// Skip API request started messages in markdown export
|
||||
return ""
|
||||
|
||||
case "api_req_finished":
|
||||
// Skip API request finished messages in markdown export
|
||||
return ""
|
||||
|
||||
case "reasoning":
|
||||
parts.push("## Reasoning")
|
||||
if (message.text) {
|
||||
parts.push(`> ${message.text.split("\n").join("\n> ")}`)
|
||||
}
|
||||
break
|
||||
|
||||
default:
|
||||
// For other message types, include them with a generic header
|
||||
if (message.text) {
|
||||
parts.push(`## ${message.say}`)
|
||||
parts.push(message.text)
|
||||
}
|
||||
}
|
||||
} else if (message.type === "ask") {
|
||||
switch (message.ask) {
|
||||
case "tool": {
|
||||
const tool = safeJsonParse<any>(message.text)
|
||||
if (tool) {
|
||||
parts.push(formatToolUseAsMarkdown(tool, "ask"))
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case "command":
|
||||
parts.push("## Command Execution")
|
||||
if (message.text) {
|
||||
parts.push(`\`\`\`bash\n${message.text}\n\`\`\``)
|
||||
}
|
||||
break
|
||||
|
||||
case "completion_result":
|
||||
parts.push("## Task Completion Request")
|
||||
if (message.text) {
|
||||
parts.push(message.text)
|
||||
}
|
||||
break
|
||||
|
||||
case "followup":
|
||||
parts.push("## Question")
|
||||
if (message.text) {
|
||||
const followUpData = safeJsonParse<any>(message.text)
|
||||
if (followUpData?.question) {
|
||||
parts.push(followUpData.question)
|
||||
} else {
|
||||
parts.push(message.text)
|
||||
}
|
||||
}
|
||||
break
|
||||
|
||||
default:
|
||||
// For other ask types, include them with a generic header
|
||||
if (message.text) {
|
||||
parts.push(`## ${message.ask}`)
|
||||
parts.push(message.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return parts.join("\n\n")
|
||||
}
|
||||
|
||||
function formatToolUseAsMarkdown(tool: any, messageType: "ask" | "say"): string {
|
||||
const parts: string[] = []
|
||||
const isRequest = messageType === "ask"
|
||||
|
||||
switch (tool.tool) {
|
||||
case "readFile":
|
||||
parts.push(`## ${isRequest ? "Reading" : "Read"} File`)
|
||||
if (tool.path) {
|
||||
parts.push(`**Path:** \`${tool.path}\``)
|
||||
}
|
||||
if (!isRequest && tool.content) {
|
||||
parts.push(`\`\`\`\n${tool.content}\n\`\`\``)
|
||||
}
|
||||
break
|
||||
|
||||
case "newFileCreated":
|
||||
parts.push(`## ${isRequest ? "Creating" : "Created"} File`)
|
||||
if (tool.path) {
|
||||
parts.push(`**Path:** \`${tool.path}\``)
|
||||
}
|
||||
if (tool.content) {
|
||||
parts.push(`\`\`\`\n${tool.content}\n\`\`\``)
|
||||
}
|
||||
break
|
||||
|
||||
case "editedExistingFile":
|
||||
case "appliedDiff":
|
||||
parts.push(`## ${isRequest ? "Editing" : "Edited"} File`)
|
||||
if (tool.path) {
|
||||
parts.push(`**Path:** \`${tool.path}\``)
|
||||
}
|
||||
if (tool.diff || tool.content) {
|
||||
parts.push(`\`\`\`diff\n${tool.diff || tool.content}\n\`\`\``)
|
||||
}
|
||||
break
|
||||
|
||||
case "listFilesTopLevel":
|
||||
case "listFilesRecursive": {
|
||||
const recursive = tool.tool === "listFilesRecursive"
|
||||
parts.push(`## ${isRequest ? "Listing" : "Listed"} Files${recursive ? " (Recursive)" : ""}`)
|
||||
if (tool.path) {
|
||||
parts.push(`**Path:** \`${tool.path}\``)
|
||||
}
|
||||
if (tool.content) {
|
||||
parts.push(`\`\`\`\n${tool.content}\n\`\`\``)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case "searchFiles":
|
||||
parts.push(`## ${isRequest ? "Searching" : "Searched"} Files`)
|
||||
if (tool.regex) {
|
||||
parts.push(`**Pattern:** \`${tool.regex}\``)
|
||||
}
|
||||
if (tool.path) {
|
||||
parts.push(`**Path:** \`${tool.path}\``)
|
||||
}
|
||||
if (tool.content) {
|
||||
parts.push(`\`\`\`\n${tool.content}\n\`\`\``)
|
||||
}
|
||||
break
|
||||
|
||||
case "updateTodoList":
|
||||
parts.push(`## Updated TODO List`)
|
||||
if (tool.todos && Array.isArray(tool.todos)) {
|
||||
parts.push(tool.todos.join("\n"))
|
||||
}
|
||||
break
|
||||
|
||||
case "switchMode":
|
||||
parts.push(`## ${isRequest ? "Switching" : "Switched"} Mode`)
|
||||
if (tool.mode) {
|
||||
parts.push(`**Mode:** ${tool.mode}`)
|
||||
}
|
||||
if (tool.reason) {
|
||||
parts.push(`**Reason:** ${tool.reason}`)
|
||||
}
|
||||
break
|
||||
|
||||
default:
|
||||
// For other tools, include a generic format
|
||||
parts.push(`## Tool: ${tool.tool}`)
|
||||
if (tool.content) {
|
||||
parts.push(`\`\`\`\n${tool.content}\n\`\`\``)
|
||||
}
|
||||
}
|
||||
|
||||
return parts.join("\n\n")
|
||||
}
|
||||
167
src/extension/__tests__/api.getTaskAsMarkdown.spec.ts
Normal file
167
src/extension/__tests__/api.getTaskAsMarkdown.spec.ts
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest"
|
||||
import { API } from "../api"
|
||||
import type { ClineProvider } from "../../core/webview/ClineProvider"
|
||||
|
||||
// Mock the formatTaskAsMarkdown function
|
||||
vi.mock("../../core/task/formatTaskAsMarkdown", () => ({
|
||||
formatTaskAsMarkdown: vi.fn((messages) => {
|
||||
if (!messages || messages.length === 0) return ""
|
||||
return messages.map((m: any) => `${m.type}: ${m.text}`).join("\n")
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock the readTaskMessages function
|
||||
vi.mock("../../core/task-persistence/taskMessages", () => ({
|
||||
readTaskMessages: vi.fn(),
|
||||
}))
|
||||
|
||||
describe("API.getTaskAsMarkdown", () => {
|
||||
let api: API
|
||||
let mockProvider: Partial<ClineProvider>
|
||||
let mockOutputChannel: any
|
||||
let mockReadTaskMessages: any
|
||||
|
||||
beforeEach(async () => {
|
||||
// Import and get the mocked function
|
||||
const { readTaskMessages } = await import("../../core/task-persistence/taskMessages")
|
||||
mockReadTaskMessages = vi.mocked(readTaskMessages)
|
||||
|
||||
mockOutputChannel = {
|
||||
appendLine: vi.fn(),
|
||||
}
|
||||
|
||||
mockProvider = {
|
||||
context: {} as any,
|
||||
getCurrentTask: vi.fn(),
|
||||
getTaskWithId: vi.fn(),
|
||||
contextProxy: {
|
||||
globalStorageUri: {
|
||||
fsPath: "/mock/storage/path",
|
||||
},
|
||||
} as any,
|
||||
// Mock the event emitter methods that API constructor uses
|
||||
on: vi.fn(),
|
||||
off: vi.fn(),
|
||||
}
|
||||
|
||||
api = new API(mockOutputChannel, mockProvider as ClineProvider)
|
||||
})
|
||||
|
||||
it("should return undefined when no taskId provided and no current task", async () => {
|
||||
vi.mocked(mockProvider.getCurrentTask!).mockReturnValue(undefined)
|
||||
|
||||
const result = await api.getTaskAsMarkdown()
|
||||
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should return formatted markdown for current task when no taskId provided", async () => {
|
||||
const mockMessages = [
|
||||
{ ts: 1000, type: "say", say: "user_feedback", text: "Hello" },
|
||||
{ ts: 1001, type: "say", say: "text", text: "Hi there" },
|
||||
]
|
||||
|
||||
vi.mocked(mockProvider.getCurrentTask!).mockReturnValue({
|
||||
clineMessages: mockMessages,
|
||||
} as any)
|
||||
|
||||
const result = await api.getTaskAsMarkdown()
|
||||
|
||||
expect(result).toBeDefined()
|
||||
expect(result).toContain("say: Hello")
|
||||
expect(result).toContain("say: Hi there")
|
||||
})
|
||||
|
||||
it("should return formatted markdown for specific task when taskId provided", async () => {
|
||||
const mockMessages = [{ ts: 1000, type: "say", say: "user_feedback", text: "Task specific message" }]
|
||||
|
||||
// Mock readTaskMessages to return the messages
|
||||
mockReadTaskMessages.mockResolvedValue(mockMessages)
|
||||
|
||||
const result = await api.getTaskAsMarkdown("task-123")
|
||||
|
||||
expect(result).toBeDefined()
|
||||
expect(result).toContain("say: Task specific message")
|
||||
expect(mockReadTaskMessages).toHaveBeenCalledWith({
|
||||
taskId: "task-123",
|
||||
globalStoragePath: "/mock/storage/path",
|
||||
})
|
||||
})
|
||||
|
||||
it("should return undefined when specific task messages not found", async () => {
|
||||
// Mock readTaskMessages to throw an error
|
||||
mockReadTaskMessages.mockRejectedValue(new Error("Task messages not found"))
|
||||
|
||||
const result = await api.getTaskAsMarkdown("non-existent-task")
|
||||
|
||||
expect(result).toBeUndefined()
|
||||
// Note: log method won't be called because logging is disabled in tests
|
||||
expect(mockReadTaskMessages).toHaveBeenCalledWith({
|
||||
taskId: "non-existent-task",
|
||||
globalStoragePath: "/mock/storage/path",
|
||||
})
|
||||
})
|
||||
|
||||
it("should return undefined when task has no messages", async () => {
|
||||
// Mock readTaskMessages to return empty array
|
||||
mockReadTaskMessages.mockResolvedValue([])
|
||||
|
||||
const result = await api.getTaskAsMarkdown("task-123")
|
||||
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should handle errors gracefully and return undefined", async () => {
|
||||
// Mock readTaskMessages to throw an unexpected error
|
||||
mockReadTaskMessages.mockImplementation(() => {
|
||||
throw new Error("Unexpected error")
|
||||
})
|
||||
|
||||
const result = await api.getTaskAsMarkdown("task-123")
|
||||
|
||||
expect(result).toBeUndefined()
|
||||
// Note: log method won't be called because logging is disabled in tests
|
||||
// Just verify the method handled the error gracefully
|
||||
expect(mockReadTaskMessages).toHaveBeenCalledWith({
|
||||
taskId: "task-123",
|
||||
globalStoragePath: "/mock/storage/path",
|
||||
})
|
||||
})
|
||||
|
||||
it("should work with current task that has many messages", async () => {
|
||||
const mockMessages = Array.from({ length: 50 }, (_, i) => ({
|
||||
ts: 1000 + i,
|
||||
type: "say",
|
||||
say: i % 2 === 0 ? "user_feedback" : "text",
|
||||
text: `Message ${i}`,
|
||||
}))
|
||||
|
||||
vi.mocked(mockProvider.getCurrentTask!).mockReturnValue({
|
||||
clineMessages: mockMessages,
|
||||
} as any)
|
||||
|
||||
const result = await api.getTaskAsMarkdown()
|
||||
|
||||
expect(result).toBeDefined()
|
||||
expect(result).toContain("Message 0")
|
||||
expect(result).toContain("Message 49")
|
||||
})
|
||||
|
||||
it("should work with task loaded from disk with many messages", async () => {
|
||||
const mockMessages = Array.from({ length: 100 }, (_, i) => ({
|
||||
ts: 2000 + i,
|
||||
type: "say",
|
||||
say: "text",
|
||||
text: `Stored message ${i}`,
|
||||
}))
|
||||
|
||||
// Mock readTaskMessages to return many messages
|
||||
mockReadTaskMessages.mockResolvedValue(mockMessages)
|
||||
|
||||
const result = await api.getTaskAsMarkdown("task-with-many-messages")
|
||||
|
||||
expect(result).toBeDefined()
|
||||
expect(result).toContain("Stored message 0")
|
||||
expect(result).toContain("Stored message 99")
|
||||
})
|
||||
})
|
||||
|
|
@ -24,6 +24,7 @@ import { IpcServer } from "@roo-code/ipc"
|
|||
import { Package } from "../shared/package"
|
||||
import { ClineProvider } from "../core/webview/ClineProvider"
|
||||
import { openClineInNewTab } from "../activate/registerCommands"
|
||||
import { formatTaskAsMarkdown } from "../core/task/formatTaskAsMarkdown"
|
||||
|
||||
export class API extends EventEmitter<RooCodeEvents> implements RooCodeAPI {
|
||||
private readonly outputChannel: vscode.OutputChannel
|
||||
|
|
@ -460,4 +461,46 @@ export class API extends EventEmitter<RooCodeEvents> implements RooCodeAPI {
|
|||
await this.sidebarProvider.activateProviderProfile({ name })
|
||||
return this.getActiveProfile()
|
||||
}
|
||||
|
||||
// Task Content Management
|
||||
|
||||
public async getTaskAsMarkdown(taskId?: string): Promise<string | undefined> {
|
||||
try {
|
||||
let messages: any[] | undefined
|
||||
|
||||
if (taskId) {
|
||||
// Get messages for a specific task from history
|
||||
// The messages are stored separately on disk, we need to load them
|
||||
const { readTaskMessages } = await import("../core/task-persistence/taskMessages")
|
||||
const globalStoragePath = this.sidebarProvider.contextProxy.globalStorageUri.fsPath
|
||||
|
||||
try {
|
||||
messages = await readTaskMessages({
|
||||
taskId,
|
||||
globalStoragePath,
|
||||
})
|
||||
} catch (error) {
|
||||
// Task messages not found or error reading them
|
||||
this.log(`[API] getTaskAsMarkdown: Failed to read messages for task ${taskId}: ${error}`)
|
||||
return undefined
|
||||
}
|
||||
} else {
|
||||
// Get messages from the current active task
|
||||
const currentTask = this.sidebarProvider.getCurrentTask()
|
||||
if (currentTask) {
|
||||
messages = currentTask.clineMessages
|
||||
}
|
||||
}
|
||||
|
||||
if (!messages || messages.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
// Format the messages as markdown
|
||||
return formatTaskAsMarkdown(messages)
|
||||
} catch (error) {
|
||||
this.log(`[API] getTaskAsMarkdown failed: ${error instanceof Error ? error.message : String(error)}`)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue