|
| 1 | +import * as vscode from "vscode" |
| 2 | +import * as fs from "fs/promises" |
| 3 | +import * as path from "path" |
| 4 | +import { ClineProvider } from "../../core/webview/ClineProvider" |
| 5 | +import { HistoryItem } from "../../shared/HistoryItem" |
| 6 | +import { ClineMessage } from "../../shared/ExtensionMessage" |
| 7 | + |
| 8 | +/** |
| 9 | + * Registers development-only commands for task manipulation. |
| 10 | + * These are only activated in development mode. |
| 11 | + */ |
| 12 | +export function registerTaskCommands(context: vscode.ExtensionContext, provider: ClineProvider): vscode.Disposable[] { |
| 13 | + return [ |
| 14 | + vscode.commands.registerCommand("cline.dev.createTestTasks", async () => { |
| 15 | + const count = await vscode.window.showInputBox({ |
| 16 | + title: "Test Tasks", |
| 17 | + prompt: "How many test tasks to create?", |
| 18 | + value: "10", |
| 19 | + }) |
| 20 | + |
| 21 | + if (!count) { |
| 22 | + return |
| 23 | + } |
| 24 | + |
| 25 | + const tasksCount = parseInt(count) |
| 26 | + const globalStoragePath = context.globalStorageUri.fsPath |
| 27 | + const tasksDir = path.join(globalStoragePath, "tasks") |
| 28 | + |
| 29 | + vscode.window.withProgress( |
| 30 | + { |
| 31 | + location: vscode.ProgressLocation.Notification, |
| 32 | + title: `Creating ${tasksCount} test tasks...`, |
| 33 | + cancellable: false, |
| 34 | + }, |
| 35 | + async (progress) => { |
| 36 | + for (let i = 0; i < tasksCount; i++) { |
| 37 | + // Generate a timestamp to ensure unique IDs |
| 38 | + const timestamp = Date.now() + i |
| 39 | + const taskId = `${timestamp}` |
| 40 | + const taskDir = path.join(tasksDir, taskId) |
| 41 | + |
| 42 | + await fs.mkdir(taskDir, { recursive: true }) |
| 43 | + |
| 44 | + // Generate a task prompt |
| 45 | + const taskName = getRandomTaskName(i) |
| 46 | + |
| 47 | + // Create realistic message sequence |
| 48 | + const messages = createRealisticMessageSequence(timestamp, taskName, i) |
| 49 | + |
| 50 | + // Create API conversation history file |
| 51 | + await fs.writeFile( |
| 52 | + path.join(taskDir, "api_conversation_history.json"), |
| 53 | + JSON.stringify( |
| 54 | + [ |
| 55 | + { |
| 56 | + role: "user", |
| 57 | + content: [{ type: "text", text: `<task>\n${taskName}\n</task>` }], |
| 58 | + }, |
| 59 | + { |
| 60 | + role: "assistant", |
| 61 | + content: [ |
| 62 | + { |
| 63 | + type: "text", |
| 64 | + text: `I'll help you ${taskName.toLowerCase()}. Let me break this down into steps.`, |
| 65 | + }, |
| 66 | + ], |
| 67 | + }, |
| 68 | + ], |
| 69 | + null, |
| 70 | + 2, |
| 71 | + ), |
| 72 | + ) |
| 73 | + |
| 74 | + // Create UI messages file with realistic message sequence |
| 75 | + await fs.writeFile(path.join(taskDir, "ui_messages.json"), JSON.stringify(messages, null, 2)) |
| 76 | + |
| 77 | + // Create history item to be shown in the HistoryView |
| 78 | + const historyItem: HistoryItem = { |
| 79 | + id: taskId, |
| 80 | + ts: timestamp, |
| 81 | + task: taskName, |
| 82 | + tokensIn: Math.floor(100 + Math.random() * 900), // Random token count from 100-1000 |
| 83 | + tokensOut: Math.floor(200 + Math.random() * 1800), // Random token count from 200-2000 |
| 84 | + cacheWrites: i % 3 === 0 ? Math.floor(50 + Math.random() * 150) : undefined, // Only add cache writes to every 3rd task |
| 85 | + cacheReads: i % 3 === 0 ? Math.floor(20 + Math.random() * 80) : undefined, // Only add cache reads to every 3rd task |
| 86 | + totalCost: Number((0.0001 + Math.random() * 0.01).toFixed(5)), // Random cost from $0.0001 to $0.0101 |
| 87 | + size: 1024 * 1024, // 1MB |
| 88 | + } |
| 89 | + |
| 90 | + // Update task history in global state |
| 91 | + await provider.updateTaskHistory(historyItem) |
| 92 | + |
| 93 | + progress.report({ increment: 100 / tasksCount }) |
| 94 | + } |
| 95 | + |
| 96 | + // Update the UI to show the new tasks |
| 97 | + await provider.postStateToWebview() |
| 98 | + |
| 99 | + vscode.window.showInformationMessage(`Created ${tasksCount} test tasks`) |
| 100 | + }, |
| 101 | + ) |
| 102 | + }), |
| 103 | + ] |
| 104 | +} |
| 105 | + |
| 106 | +/** |
| 107 | + * Creates a realistic sequence of messages that would occur in a typical task |
| 108 | + */ |
| 109 | +function createRealisticMessageSequence(baseTimestamp: number, taskPrompt: string, taskIndex: number): ClineMessage[] { |
| 110 | + // Use an incrementing timestamp to ensure messages appear in sequence |
| 111 | + let timestamp = baseTimestamp |
| 112 | + const getNextTimestamp = () => { |
| 113 | + timestamp += 1000 // Add 1 second between messages |
| 114 | + return timestamp |
| 115 | + } |
| 116 | + |
| 117 | + // Variables to make different test tasks look unique |
| 118 | + const fileName = getRandomFileName(taskIndex) |
| 119 | + const commitHash = `commit${taskIndex}${Math.floor(Math.random() * 1000000).toString(16)}` |
| 120 | + |
| 121 | + // Create a realistic message sequence |
| 122 | + const messages: ClineMessage[] = [ |
| 123 | + // Initial task message - uses "say" with "text" which is the format used in Cline.ts |
| 124 | + { |
| 125 | + ts: baseTimestamp, |
| 126 | + type: "say", |
| 127 | + say: "text", |
| 128 | + text: taskPrompt, |
| 129 | + }, |
| 130 | + |
| 131 | + // API request started |
| 132 | + { |
| 133 | + ts: getNextTimestamp(), |
| 134 | + type: "say", |
| 135 | + say: "api_req_started", |
| 136 | + text: JSON.stringify({ |
| 137 | + request: `<task>\n${taskPrompt}\n</task>`, |
| 138 | + tokensIn: Math.floor(100 + Math.random() * 200), |
| 139 | + tokensOut: Math.floor(300 + Math.random() * 500), |
| 140 | + }), |
| 141 | + }, |
| 142 | + |
| 143 | + // Reasoning message |
| 144 | + { |
| 145 | + ts: getNextTimestamp(), |
| 146 | + type: "say", |
| 147 | + say: "reasoning", |
| 148 | + text: `I'll approach this task by breaking it down into manageable steps. First, I'll analyze the requirements, then create a plan, and finally implement the solution systematically.`, |
| 149 | + }, |
| 150 | + |
| 151 | + // Text response |
| 152 | + { |
| 153 | + ts: getNextTimestamp(), |
| 154 | + type: "say", |
| 155 | + say: "text", |
| 156 | + text: `I'll help you with this task. Let me start by creating the necessary files and implementing the core functionality.`, |
| 157 | + }, |
| 158 | + ] |
| 159 | + |
| 160 | + // Add task-specific messages based on index modulo to create variety |
| 161 | + const messageType = taskIndex % 5 |
| 162 | + |
| 163 | + if (messageType === 0 || messageType === 2) { |
| 164 | + // Tool use - file operations |
| 165 | + messages.push({ |
| 166 | + ts: getNextTimestamp(), |
| 167 | + type: "say", |
| 168 | + say: "tool", |
| 169 | + text: JSON.stringify({ |
| 170 | + tool: "newFileCreated", |
| 171 | + path: fileName, |
| 172 | + content: `// Sample code for ${taskPrompt}`, |
| 173 | + }), |
| 174 | + }) |
| 175 | + } |
| 176 | + |
| 177 | + if (messageType === 1 || messageType === 3) { |
| 178 | + // Command execution |
| 179 | + messages.push( |
| 180 | + { |
| 181 | + ts: getNextTimestamp(), |
| 182 | + type: "ask", |
| 183 | + ask: "command", |
| 184 | + text: `ls -la`, |
| 185 | + }, |
| 186 | + { |
| 187 | + ts: getNextTimestamp(), |
| 188 | + type: "say", |
| 189 | + say: "command_output", |
| 190 | + text: `total 24\ndrwxr-xr-x 3 user staff 96 Mar 10 12:34 .\ndrwxr-xr-x 8 user staff 256 Mar 10 12:30 ..\n-rw-r--r-- 1 user staff 158 Mar 10 12:34 ${fileName}`, |
| 191 | + }, |
| 192 | + ) |
| 193 | + } |
| 194 | + |
| 195 | + if (messageType === 2 || messageType === 4) { |
| 196 | + // Browser actions |
| 197 | + messages.push( |
| 198 | + { |
| 199 | + ts: getNextTimestamp(), |
| 200 | + type: "ask", |
| 201 | + ask: "browser_action_launch", |
| 202 | + text: `https://example.com`, |
| 203 | + }, |
| 204 | + { |
| 205 | + ts: getNextTimestamp(), |
| 206 | + type: "say", |
| 207 | + say: "browser_action_result", |
| 208 | + text: JSON.stringify({ |
| 209 | + logs: "Page loaded successfully", |
| 210 | + screenshot: |
| 211 | + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==", |
| 212 | + }), |
| 213 | + }, |
| 214 | + { |
| 215 | + ts: getNextTimestamp(), |
| 216 | + type: "say", |
| 217 | + say: "browser_action", |
| 218 | + text: JSON.stringify({ |
| 219 | + action: "close", |
| 220 | + }), |
| 221 | + }, |
| 222 | + ) |
| 223 | + } |
| 224 | + |
| 225 | + // Add checkpoint |
| 226 | + messages.push({ |
| 227 | + ts: getNextTimestamp(), |
| 228 | + type: "say", |
| 229 | + say: "checkpoint_created", |
| 230 | + lastCheckpointHash: commitHash, |
| 231 | + }) |
| 232 | + |
| 233 | + // Add completion result (all tasks end with this) |
| 234 | + messages.push({ |
| 235 | + ts: getNextTimestamp(), |
| 236 | + type: "say", |
| 237 | + say: "completion_result", |
| 238 | + text: `I've completed the task to ${taskPrompt.toLowerCase()}. The implementation includes all the required functionality and meets the specifications. ${"x".repeat(1024 * 1024)}`, // 1MB file |
| 239 | + lastCheckpointHash: commitHash, |
| 240 | + }) |
| 241 | + |
| 242 | + return messages |
| 243 | +} |
| 244 | + |
| 245 | +/** |
| 246 | + * Returns a random task name for test data |
| 247 | + */ |
| 248 | +function getRandomTaskName(index: number): string { |
| 249 | + const tasks = [ |
| 250 | + "Create a simple todo application", |
| 251 | + "Build a weather forecast widget", |
| 252 | + "Implement a markdown parser", |
| 253 | + "Design a responsive landing page", |
| 254 | + "Develop a currency converter", |
| 255 | + "Create a file upload component", |
| 256 | + "Build a data visualization dashboard", |
| 257 | + "Implement a search functionality", |
| 258 | + "Create a user authentication system", |
| 259 | + "Design a dark mode toggle", |
| 260 | + "Build a countdown timer", |
| 261 | + "Create a drag and drop interface", |
| 262 | + "Implement form validation", |
| 263 | + "Design a multi-step wizard", |
| 264 | + "Create a notification system", |
| 265 | + ] |
| 266 | + |
| 267 | + return tasks[index % tasks.length] + ` (Test ${index + 1})` |
| 268 | +} |
| 269 | + |
| 270 | +/** |
| 271 | + * Returns a random file name for test data |
| 272 | + */ |
| 273 | +function getRandomFileName(index: number): string { |
| 274 | + const files = [ |
| 275 | + "index.html", |
| 276 | + "styles.css", |
| 277 | + "script.js", |
| 278 | + "app.jsx", |
| 279 | + "main.ts", |
| 280 | + "utils.py", |
| 281 | + "config.json", |
| 282 | + "server.js", |
| 283 | + "data.csv", |
| 284 | + "README.md", |
| 285 | + ] |
| 286 | + |
| 287 | + return files[index % files.length] |
| 288 | +} |
0 commit comments