From b23ecca0867e75bf5abf8e379ad12d19ed085fed Mon Sep 17 00:00:00 2001 From: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> Date: Mon, 29 Jul 2024 20:48:26 -0400 Subject: [PATCH] Add ability to export tasks as markdown --- src/providers/ClaudeDevProvider.ts | 80 ++++++++++++++++++++++++ src/shared/WebviewMessage.ts | 1 + webview-ui/src/components/TaskHeader.tsx | 16 +++++ 3 files changed, 97 insertions(+) diff --git a/src/providers/ClaudeDevProvider.ts b/src/providers/ClaudeDevProvider.ts index d810311..6005f3b 100644 --- a/src/providers/ClaudeDevProvider.ts +++ b/src/providers/ClaudeDevProvider.ts @@ -5,6 +5,8 @@ import { ClaudeDev } from "../ClaudeDev" import { ClaudeMessage, ExtensionMessage } from "../shared/ExtensionMessage" import { WebviewMessage } from "../shared/WebviewMessage" import { Anthropic } from "@anthropic-ai/sdk" +import * as path from "path" +import os from "os" /* https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts @@ -279,6 +281,9 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider { await this.updateGlobalState("lastShownAnnouncementId", this.latestAnnouncementId) await this.postStateToWebview() break + case "downloadTask": + this.downloadTask() + break // Add more switch case statements here as more webview message commands // are created within the webview context (i.e. inside media/main.js) } @@ -288,6 +293,81 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider { ) } + async downloadTask() { + // File name + const date = new Date() + const month = date.toLocaleString("en-US", { month: "short" }).toLowerCase() + const day = date.getDate() + const year = date.getFullYear() + let hours = date.getHours() + const minutes = date.getMinutes().toString().padStart(2, "0") + const ampm = hours >= 12 ? "PM" : "AM" + hours = hours % 12 + hours = hours ? hours : 12 // the hour '0' should be '12' + const fileName = `claude_dev_task_${month}-${day}-${year}_${hours}-${minutes}-${ampm}.md` + + // Generate markdown + const conversationHistory = await this.getApiConversationHistory() + const markdownContent = conversationHistory + .map((message) => { + const role = message.role === "user" ? "**User:**" : "**Assistant:**" + const content = Array.isArray(message.content) + ? message.content.map(this.formatContentBlockToMarkdown).join("\n") + : message.content + + return `${role}\n\n${content}\n\n` + }) + .join("---\n\n") + + // Prompt user for save location + const saveUri = await vscode.window.showSaveDialog({ + filters: { Markdown: ["md"] }, + defaultUri: vscode.Uri.file(path.join(os.homedir(), "Downloads", fileName)), + }) + + if (saveUri) { + // Write content to the selected location + await vscode.workspace.fs.writeFile(saveUri, Buffer.from(markdownContent)) + } + } + + private formatContentBlockToMarkdown( + block: + | Anthropic.TextBlockParam + | Anthropic.ImageBlockParam + | Anthropic.ToolUseBlockParam + | Anthropic.ToolResultBlockParam + ): string { + switch (block.type) { + case "text": + return block.text + case "image": + return `[Image: ${block.source.media_type}]` + case "tool_use": + let input: string + if (typeof block.input === "object" && block.input !== null) { + input = Object.entries(block.input) + .map(([key, value]) => `${key.charAt(0).toUpperCase() + key.slice(1)}: ${value}`) + .join("\n") + } else { + input = String(block.input) + } + return `[Tool Use: ${block.name}]\n${input}` + case "tool_result": + if (typeof block.content === "string") { + return `[Tool Result${block.is_error ? " (Error)" : ""}]\n${block.content}` + } else if (Array.isArray(block.content)) { + return `[Tool Result${block.is_error ? " (Error)" : ""}]\n${block.content + .map(this.formatContentBlockToMarkdown) + .join("\n")}` + } else { + return `[Tool Result${block.is_error ? " (Error)" : ""}]` + } + default: + return "[Unexpected content type]" + } + } + async postStateToWebview() { const [apiKey, maxRequestsPerTask, claudeMessages, lastShownAnnouncementId] = await Promise.all([ this.getSecret("apiKey") as Promise, diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 0a171e4..de093d1 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -7,6 +7,7 @@ export interface WebviewMessage { | "askResponse" | "clearTask" | "didShowAnnouncement" + | "downloadTask" text?: string askResponse?: ClaudeAskResponse } diff --git a/webview-ui/src/components/TaskHeader.tsx b/webview-ui/src/components/TaskHeader.tsx index 47048f2..4d1cd27 100644 --- a/webview-ui/src/components/TaskHeader.tsx +++ b/webview-ui/src/components/TaskHeader.tsx @@ -2,6 +2,7 @@ import { VSCodeButton } from "@vscode/webview-ui-toolkit/react" import React, { useEffect, useRef, useState } from "react" import TextTruncate from "react-text-truncate" import { useWindowSize } from "react-use" +import { vscode } from "../utilities/vscode" interface TaskHeaderProps { taskText: string @@ -71,6 +72,10 @@ const TaskHeader: React.FC = ({ taskText, tokensIn, tokensOut, const toggleExpand = () => setIsExpanded(!isExpanded) + const handleDownload = () => { + vscode.postMessage({ type: "downloadTask" }) + } + return (
= ({ taskText, tokensIn, tokensOut, display: "flex", flexDirection: "column", gap: "8px", + position: "relative", }}>
= ({ taskText, tokensIn, tokensOut, ${totalCost.toFixed(4)}
+ +
EXPORT .MD
+
)