Просмотр исходного кода

Add ability to export tasks as markdown

Saoud Rizwan 1 год назад
Родитель
Сommit
b23ecca086

+ 80 - 0
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<string | undefined>,

+ 1 - 0
src/shared/WebviewMessage.ts

@@ -7,6 +7,7 @@ export interface WebviewMessage {
 		| "askResponse"
 		| "clearTask"
 		| "didShowAnnouncement"
+		| "downloadTask"
 	text?: string
 	askResponse?: ClaudeAskResponse
 }

+ 16 - 0
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<TaskHeaderProps> = ({ taskText, tokensIn, tokensOut,
 
 	const toggleExpand = () => setIsExpanded(!isExpanded)
 
+	const handleDownload = () => {
+		vscode.postMessage({ type: "downloadTask" })
+	}
+
 	return (
 		<div style={{ padding: "15px 15px 10px 15px" }}>
 			<div
@@ -82,6 +87,7 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({ taskText, tokensIn, tokensOut,
 					display: "flex",
 					flexDirection: "column",
 					gap: "8px",
+					position: "relative",
 				}}>
 				<div
 					style={{
@@ -157,6 +163,16 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({ taskText, tokensIn, tokensOut,
 						<span>${totalCost.toFixed(4)}</span>
 					</div>
 				</div>
+				<VSCodeButton
+					appearance="icon"
+					onClick={handleDownload}
+					style={{
+						position: "absolute",
+						bottom: "9.5px",
+						right: "9px",
+					}}>
+					<div style={{ fontSize: "10.5px", fontWeight: "bold", opacity: 0.6 }}>EXPORT .MD</div>
+				</VSCodeButton>
 			</div>
 		</div>
 	)