diff --git a/src/ClaudeDev.ts b/src/ClaudeDev.ts index fac5310..55889e5 100644 --- a/src/ClaudeDev.ts +++ b/src/ClaudeDev.ts @@ -345,7 +345,7 @@ ${activeEditorContents}` return `Changes applied to ${filePath}:\n${diffResult}` } else { - this.say("tool", JSON.stringify({ tool: "editedExistingFile", path: filePath } as ClaudeSayTool)) + this.say("tool", JSON.stringify({ tool: "editedExistingFile", path: filePath, content: " " } as ClaudeSayTool)) return `Tool succeeded, however there were no changes detected to ${filePath}` } } else { diff --git a/webview-ui/src/App.tsx b/webview-ui/src/App.tsx index ac949ba..fca9a18 100644 --- a/webview-ui/src/App.tsx +++ b/webview-ui/src/App.tsx @@ -6,6 +6,7 @@ import SettingsView from "./components/SettingsView" import { ClaudeMessage, ExtensionMessage } from "@shared/ExtensionMessage" import WelcomeView from "./components/WelcomeView" import { vscode } from "./utilities/vscode" +import { mockMessages } from "./utilities/mockMessages" /* The contents of webviews however are created when the webview becomes visible and destroyed when the webview is moved into the background. Any state inside the webview will be lost when the webview is moved to a background tab. @@ -60,163 +61,6 @@ const App: React.FC = () => { } }, []) - // dummy data for messages - const generateRandomTimestamp = (baseDate: Date, rangeInDays: number): number => { - const rangeInMs = rangeInDays * 24 * 60 * 60 * 1000 // convert days to milliseconds - const randomOffset = Math.floor(Math.random() * rangeInMs * 2) - rangeInMs // rangeInMs * 2 to have offset in both directions - return baseDate.getTime() + randomOffset - } - - const baseDate = new Date("2024-07-08T00:00:00Z") - - const messages: ClaudeMessage[] = [ - { - type: "say", - say: "task", - text: "Starting task, this is my requeirements", - ts: generateRandomTimestamp(baseDate, 1), - }, - { - type: "ask", - ask: "request_limit_reached", - text: "Request limit reached", - ts: generateRandomTimestamp(baseDate, 2), - }, - { type: "ask", ask: "followup", text: "Any additional questions?", ts: generateRandomTimestamp(baseDate, 3) }, - { type: "say", say: "error", text: "An error occurred", ts: generateRandomTimestamp(baseDate, 4) }, - - { type: "say", say: "text", text: "Some general text", ts: generateRandomTimestamp(baseDate, 7) }, - { type: "say", say: "tool", text: "Using a tool", ts: generateRandomTimestamp(baseDate, 8) }, - - // First command sequence - { type: "ask", ask: "command", text: "ls -l", ts: generateRandomTimestamp(baseDate, 9) }, - { type: "say", say: "command_output", text: "file1.txt", ts: generateRandomTimestamp(baseDate, 10) }, - { - type: "say", - say: "api_req_started", - text: JSON.stringify({ request: "GET /api/data" }), - ts: generateRandomTimestamp(baseDate, 5), - }, - { type: "say", say: "command_output", text: "file2.txt", ts: generateRandomTimestamp(baseDate, 11) }, - { type: "say", say: "command_output", text: "directory1", ts: generateRandomTimestamp(baseDate, 12) }, - - { type: "say", say: "text", text: "Interrupting text", ts: generateRandomTimestamp(baseDate, 13) }, - { - type: "say", - say: "api_req_finished", - text: JSON.stringify({ cost: "GET /api/data" }), - ts: generateRandomTimestamp(baseDate, 6), - }, - // Second command sequence - { type: "ask", ask: "command", text: "pwd", ts: generateRandomTimestamp(baseDate, 14) }, - { type: "say", say: "command_output", text: "/home/user", ts: generateRandomTimestamp(baseDate, 15) }, - - { type: "ask", ask: "completion_result", text: "Task completed", ts: generateRandomTimestamp(baseDate, 16) }, - - // Third command sequence (no output) - { type: "ask", ask: "command", text: "echo Hello", ts: generateRandomTimestamp(baseDate, 17) }, - - // Testing combineApiRequests - { type: "say", say: "text", text: "Final message", ts: generateRandomTimestamp(baseDate, 18) }, - { type: "ask", ask: "command", text: "ls -l", ts: generateRandomTimestamp(baseDate, 19) }, - { type: "say", say: "command_output", text: "file1.txt", ts: generateRandomTimestamp(baseDate, 20) }, - { - type: "say", - say: "api_req_started", - text: JSON.stringify({ request: "GET /api/data" }), - ts: generateRandomTimestamp(baseDate, 23), - }, - { type: "say", say: "command_output", text: "file2.txt", ts: generateRandomTimestamp(baseDate, 24) }, - { type: "say", say: "text", text: "Some random text", ts: generateRandomTimestamp(baseDate, 25) }, - { - type: "say", - say: "api_req_finished", - text: JSON.stringify({ cost: 0.005 }), - ts: generateRandomTimestamp(baseDate, 26), - }, - { type: "ask", ask: "command", text: "pwd", ts: generateRandomTimestamp(baseDate, 27) }, - { type: "say", say: "command_output", text: "/home/user", ts: generateRandomTimestamp(baseDate, 28) }, - { - type: "say", - say: "api_req_started", - text: JSON.stringify({ request: "POST /api/update" }), - ts: generateRandomTimestamp(baseDate, 29), - }, - { type: "say", say: "text", text: "Final message", ts: generateRandomTimestamp(baseDate, 30) }, - { - type: "say", - say: "text", - text: "Starting API requests", - ts: Date.now(), - }, - { - type: "say", - say: "api_req_started", - text: JSON.stringify({ - request: "GET /api/data1", - }), - ts: Date.now() + 1000, - }, - { - type: "say", - say: "api_req_finished", - text: JSON.stringify({ - tokensIn: 10, - tokensOut: 20, - cost: 0.002, - }), - ts: Date.now() + 2000, - }, - { - type: "say", - say: "text", - text: "Processing data...", - ts: Date.now() + 3000, - }, - { - type: "say", - say: "api_req_started", - text: JSON.stringify({ - request: "POST /api/update1", - }), - ts: Date.now() + 4000, - }, - { - type: "say", - say: "api_req_finished", - text: JSON.stringify({ - tokensIn: 15, - tokensOut: 25, - cost: 0.003, - }), - ts: Date.now() + 5000, - }, - { - type: "say", - say: "text", - text: "More processing...", - ts: Date.now() + 6000, - }, - { - type: "say", - say: "api_req_started", - text: JSON.stringify({ - request: "GET /api/data2", - }), - ts: Date.now() + 7000, - }, - { - type: "say", - say: "api_req_finished", - text: JSON.stringify({ - tokensIn: 5, - tokensOut: 15, - cost: 0.001, - }), - ts: Date.now() + 8000, - }, - ] - return ( <> {showWelcome ? ( @@ -230,7 +74,7 @@ const App: React.FC = () => { onDone={() => setShowSettings(false)} /> ) : ( - + )} ) diff --git a/webview-ui/src/components/ChatRow.tsx b/webview-ui/src/components/ChatRow.tsx index 03b7b0a..5c0a19d 100644 --- a/webview-ui/src/components/ChatRow.tsx +++ b/webview-ui/src/components/ChatRow.tsx @@ -114,11 +114,7 @@ const ChatRow: React.FC = ({ message }) => { case "api_req_finished": return null // Hide this message type case "tool": - //const tool = JSON.parse(message.text || "{}") as ClaudeSayTool - const tool: ClaudeSayTool = { - tool: "editedExistingFile", - path: "/path/to/file", - } + const tool = JSON.parse(message.text || "{}") as ClaudeSayTool const toolIcon = (name: string) => ( { const backgroundColor = oneDark['pre[class*="language-"]'].background as string + /* + We need to remove leading non-alphanumeric characters from the path in order for our leading ellipses trick to work. + + ^: Anchors the match to the start of the string. + [^a-zA-Z0-9]+: Matches one or more characters that are not alphanumeric. + The replace method removes these matched characters, effectively trimming the string up to the first alphanumeric character. + */ + const removeLeadingNonAlphanumeric = (path: string): string => path.replace(/^[^a-zA-Z0-9]+/, "") + + const inferredLanguage = useMemo(() => language ?? (path ? getLanguageFromPath(path) : undefined), [path, language]) + + console.log(inferredLanguage) + return (
@@ -94,7 +109,7 @@ const CodeBlock = ({ code, diff, language, path }: CodeBlockProps) => { direction: "rtl", textAlign: "left", }}> - {path} + {removeLeadingNonAlphanumeric(path) + "\u200E"} setIsExpanded(!isExpanded)}> @@ -111,12 +126,14 @@ const CodeBlock = ({ code, diff, language, path }: CodeBlockProps) => { }}> ${extensionToLanguage[extension]}`) + return extensionToLanguage[extension] +} diff --git a/webview-ui/src/utilities/mockMessages.ts b/webview-ui/src/utilities/mockMessages.ts new file mode 100644 index 0000000..61e7d79 --- /dev/null +++ b/webview-ui/src/utilities/mockMessages.ts @@ -0,0 +1,316 @@ +import { ClaudeMessage } from "@shared/ExtensionMessage"; + +export const mockMessages: ClaudeMessage[] = [ + { + ts: Date.now() - 3600000, + type: "say", + say: "task", + text: "Create a React component for a todo list application", + }, + { + ts: Date.now() - 3500000, + type: "say", + say: "api_req_started", + text: JSON.stringify({ + request: { + text: "Create a React component for a todo list application", + type: "text", + }, + tokensIn: 10, + tokensOut: 250, + cost: 0.0002, + }), + }, + { + ts: Date.now() - 3300000, + type: "say", + say: "text", + text: "Here's a basic React component for a todo list application:", + }, + { + ts: Date.now() - 3200000, + type: "say", + say: "tool", + text: JSON.stringify({ + tool: "newFileCreated", + path: "/src/components/TodoList.tsx", + content: `import React, { useState } from 'react'; + + interface Todo { + id: number; + text: string; + completed: boolean; + } + + const TodoList: React.FC = () => { + const [todos, setTodos] = useState([]); + const [inputValue, setInputValue] = useState(''); + + const addTodo = () => { + if (inputValue.trim() !== '') { + setTodos([...todos, { id: Date.now(), text: inputValue, completed: false }]); + setInputValue(''); + } + }; + + const toggleTodo = (id: number) => { + setTodos(todos.map(todo => + todo.id === id ? { ...todo, completed: !todo.completed } : todo + )); + }; + + return ( +
+

Todo List

+ setInputValue(e.target.value)} + placeholder="Add a new todo" + /> + +
    + {todos.map(todo => ( +
  • toggleTodo(todo.id)} + style={{ textDecoration: todo.completed ? 'line-through' : 'none' }} + > + {todo.text} +
  • + ))} +
+
+ ); + }; + + export default TodoList;`, + }), + }, + { + ts: Date.now() - 3100000, + type: "say", + say: "text", + text: "I've created a new file 'TodoList.tsx' in the '/src/components/' directory. This component includes basic functionality for adding and toggling todos. You can further customize and style it as needed.", + }, + { + ts: Date.now() - 3000000, + type: "ask", + ask: "followup", + text: "Do you want me to explain the code or add any additional features to the todo list?", + }, + { + ts: Date.now() - 2900000, + type: "say", + say: "text", + text: "Let's add a feature to delete todos from the list.", + }, + { + ts: Date.now() - 2800000, + type: "say", + say: "api_req_started", + text: JSON.stringify({ + request: { + text: "Add a feature to delete todos from the list", + type: "text", + }, + tokensIn: 8, + tokensOut: 180, + cost: 0.0003, + }), + }, + { + ts: Date.now() - 2600000, + type: "say", + say: "tool", + text: JSON.stringify({ + tool: "editedExistingFile", + path: "/src/components/TodoList.tsx", + diff: `@@ -14,6 +14,11 @@ + } + }; + + + const deleteTodo = (id: number) => { + + setTodos(todos.filter(todo => todo.id !== id)); + + }; + + + const toggleTodo = (id: number) => { + setTodos(todos.map(todo => + todo.id === id ? { ...todo, completed: !todo.completed } : todo + @@ -35,6 +40,7 @@ + style={{ textDecoration: todo.completed ? 'line-through' : 'none' }} + > + {todo.text} + + + + ))} + `, + }), + }, + { + ts: Date.now() - 2500000, + type: "say", + say: "text", + text: "I've updated the TodoList component to include a delete feature. Each todo item now has a 'Delete' button that removes the todo from the list when clicked.", + }, + { + ts: Date.now() - 2400000, + type: "ask", + ask: "command", + text: "npm run test\n\nOutput:\nPASS src/__tests__/TodoList.test.tsx\n TodoList Component\n ✓ renders without crashing (23 ms)\n ✓ adds a new todo when the add button is clicked (34 ms)\n ✓ toggles a todo when clicked (45 ms)\n ✓ deletes a todo when the delete button is clicked (28 ms)\n\nTest Suites: 1 passed, 1 total\nTests: 4 passed, 4 total\nSnapshots: 0 total\nTime: 1.234 s\nRan all test suites.", + }, + { + ts: Date.now() - 2300000, + type: "say", + say: "text", + text: "Great! The tests for the TodoList component have passed. All functionalities, including the new delete feature, are working as expected.", + }, + { + ts: Date.now() - 2200000, + type: "ask", + ask: "request_limit_reached", + text: "You've reached the maximum number of requests for this task. Would you like to continue or start a new task?", + }, + { + ts: Date.now() - 2100000, + type: "say", + say: "text", + text: "Let's start a new task. What would you like to work on next?", + }, + { + ts: Date.now() - 2000000, + type: "say", + say: "task", + text: "Create a simple API using Express.js", + }, + { + ts: Date.now() - 1900000, + type: "say", + say: "api_req_started", + text: JSON.stringify({ + request: { + text: "Create a simple API using Express.js", + type: "text", + }, + tokensIn: 7, + tokensOut: 220, + cost: 0.0002, + }), + }, + { + ts: Date.now() - 1700000, + type: "say", + say: "tool", + text: JSON.stringify({ + tool: "newFileCreated", + path: "/src/app.js", + content: `const express = require('express'); + const app = express(); + const port = 3000; + + app.use(express.json()); + + let items = []; + + app.get('/items', (req, res) => { + res.json(items); + }); + + app.post('/items', (req, res) => { + const newItem = req.body; + items.push(newItem); + res.status(201).json(newItem); + }); + + app.get('/items/:id', (req, res) => { + const item = items.find(i => i.id === parseInt(req.params.id)); + if (item) { + res.json(item); + } else { + res.status(404).send('Item not found'); + } + }); + + app.listen(port, () => { + console.log(\`API running on http://localhost:\${port}\`); + });`, + }), + }, + { + ts: Date.now() - 1600000, + type: "say", + say: "text", + text: "I've created a simple Express.js API with endpoints for getting all items, adding a new item, and getting a specific item by ID. The API is set up to run on port 3000.", + }, + { + ts: Date.now() - 1500000, + type: "ask", + ask: "command", + text: "npm install express\n\nOutput:\nadded 57 packages, and audited 58 packages in 3s\n\n7 packages are looking for funding\n run `npm fund` for details\n\nfound 0 vulnerabilities", + }, + { + ts: Date.now() - 1400000, + type: "say", + say: "text", + text: "Express has been successfully installed. You can now run the API using 'node app.js' in the terminal.", + }, + { + ts: Date.now() - 1300000, + type: "ask", + ask: "completion_result", + text: "The API has been successfully created and set up. Is there anything else you'd like me to do with this API?", + }, + { + ts: Date.now() - 1200000, + type: "say", + say: "error", + text: "An error occurred while trying to start the server: EADDRINUSE: address already in use :::3000", + }, + { + ts: Date.now() - 1100000, + type: "say", + say: "text", + text: "It seems that port 3000 is already in use. Let's modify the code to use a different port.", + }, + { + ts: Date.now() - 1000000, + type: "say", + say: "tool", + text: JSON.stringify({ + tool: "editedExistingFile", + path: "/src/app.js", + diff: `@@ -1,6 +1,6 @@ + const express = require('express'); + const app = express(); + -const port = 3000; + +const port = process.env.PORT || 3001; + + app.use(express.json()); + `, + }), + }, + { + ts: Date.now() - 900000, + type: "say", + say: "text", + text: "I've updated the code to use port 3001 if port 3000 is not available. You can now try running the server again.", + }, + { + ts: Date.now() - 800000, + type: "ask", + ask: "command", + text: "node app.js\n\nOutput:\nAPI running on http://localhost:3001", + }, + { + ts: Date.now() - 700000, + type: "say", + say: "text", + text: "Great! The API is now running successfully on port 3001.", + }, + { + ts: Date.now() - 600000, + type: "ask", + ask: "completion_result", + text: "The API has been created, set up, and is now running successfully. Is there anything else you'd like me to do?", + }, +]