The agent wants to: {interrupt.toolName}
+ +{JSON.stringify(interrupt.toolInput, null, 2)}
+ + The LLM generated invalid Mermaid syntax. The diagram could not be rendered. +
+{item.answer}
-([\s\S]*?)<\/code><\/pre>/gi,
+ /```mermaid\n?([\s\S]*?)```/gi,
+ ];
+
+ let lastIndex = 0;
+
+ interface MermaidMatch {
+ fullMatch: string;
+ code: string;
+ startIndex: number;
+ endIndex: number;
+ isBase64: boolean;
+ }
+
+ const mermaidMatches: MermaidMatch[] = [];
+
+ // First, find new-style (base64) mermaid blocks
+ let match;
+ for (const newStylePattern of newStylePatterns) {
+ newStylePattern.lastIndex = 0;
+ while ((match = newStylePattern.exec(htmlContent)) !== null) {
+ try {
+ // Decode base64 to get the original mermaid code
+ const decodedCode = atob(match[1]);
+ mermaidMatches.push({
+ fullMatch: match[0],
+ code: decodedCode,
+ startIndex: match.index,
+ endIndex: match.index + match[0].length,
+ isBase64: true,
+ });
+ } catch (e) {
+ console.error('Failed to decode mermaid base64:', e);
+ }
+ }
+ }
+
+ // If no new-style matches, try old-style patterns
+ if (mermaidMatches.length === 0) {
+ for (const pattern of oldStylePatterns) {
+ pattern.lastIndex = 0;
+ while ((match = pattern.exec(htmlContent)) !== null) {
+ let mermaidCode = match[1];
+
+ // Decode HTML entities
+ mermaidCode = mermaidCode
+ .replace(/</g, '<')
+ .replace(/>/g, '>')
+ .replace(/&/g, '&')
+ .replace(/"/g, '"')
+ .replace(/'/g, "'")
+ .replace(/ /g, ' ')
+ .replace(/
/gi, '\n')
+ .replace(/<[^>]+>/g, '')
+ .trim();
+
+ mermaidMatches.push({
+ fullMatch: match[0],
+ code: mermaidCode,
+ startIndex: match.index,
+ endIndex: match.index + match[0].length,
+ isBase64: false,
+ });
+ }
+ }
+ }
+
+ if (mermaidMatches.length === 0) {
+ // No mermaid matches found, return as regular HTML
+ return [];
+ }
+
+ // Sort matches by start index
+ mermaidMatches.sort((a, b) => a.startIndex - b.startIndex);
+
+ // Remove overlapping matches (keep the first one)
+ const uniqueMatches: MermaidMatch[] = [];
+ for (const m of mermaidMatches) {
+ const overlaps = uniqueMatches.some(
+ (existing) => m.startIndex < existing.endIndex && m.endIndex > existing.startIndex
+ );
+ if (!overlaps) {
+ uniqueMatches.push(m);
+ }
+ }
+
+ if (uniqueMatches.length === 0) {
+ // No mermaid content, return as regular HTML
+ return [];
+ }
+
+ // Process content with mermaid diagrams
+ for (const match of uniqueMatches) {
+ // Add content before this mermaid block
+ if (match.startIndex > lastIndex) {
+ const beforeContent = htmlContent.substring(lastIndex, match.startIndex);
+ if (beforeContent.trim()) {
+ elements.push(
+
+ );
+ }
+ }
+
+ // Add the mermaid diagram
+ elements.push(
+
+ );
+
+ lastIndex = match.endIndex;
+ }
+
+ // Add any remaining content after the last mermaid block
+ if (lastIndex < htmlContent.length) {
+ const afterContent = htmlContent.substring(lastIndex);
+ if (afterContent.trim()) {
+ elements.push(
+
+ );
+ }
+ }
+
+ return elements;
+ };
+ }, []);
+
const { thinkingContent, regularContent, hasThinking } = parseContent(content);
const sanitizedThinkingContent = DOMPurify.sanitize(thinkingContent);
- const sanitizedRegularContent = DOMPurify.sanitize(regularContent);
const toggleExpanded = () => {
setIsExpanded(!isExpanded);
@@ -55,51 +494,56 @@ export const ThinkingComponent: React.FC = ({ content })
setTimeout(() => setCopyStatus(""), 2000);
} catch (error) {
console.error("Failed to copy thinking content:", error);
- setCopyStatus("Error");
+ setCopyStatus("Failed");
setTimeout(() => setCopyStatus(""), 2000);
}
};
+ // Render regular content with Mermaid support
+ const renderedRegularContent = useMemo(() => {
+ return parseMermaidContent(regularContent);
+ }, [regularContent, parseMermaidContent]);
+
if (!hasThinking) {
- // If no thinking content, just return the regular content
- return ;
+ return <>{renderedRegularContent}>;
}
return (
<>
- {/* Thinking section */}
-
-
+
+ 💭
+ Thoughts
+
+
+ {isExpanded ? 'Visible' : 'Hidden'}
+ ▼
+
+
{isExpanded && (
-
-
- Model reasoning process
-
-
-
-
+
+
+ Model reasoning process
+
+ {copyStatus || "Copy"}
+
+
+
+
)}
-
+
- {/* Regular content */}
- {regularContent && }
+ {regularContent && renderedRegularContent}
>
);
-};
+};
\ No newline at end of file
diff --git a/webviewUi/src/components/webview.tsx b/webviewUi/src/components/webview.tsx
index f9fa7fd..8feac07 100644
--- a/webviewUi/src/components/webview.tsx
+++ b/webviewUi/src/components/webview.tsx
@@ -1,36 +1,25 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
-export interface Message {
- type: "user" | "bot";
- content: string;
- language?: string;
- alias?: string;
-}
-
-export interface ExtensionMessage {
- type: string;
- payload: any;
-}
-
import { VSCodePanels, VSCodePanelTab, VSCodePanelView } from "@vscode/webview-ui-toolkit/react";
import type hljs from "highlight.js";
-import { useCallback, useEffect, useRef, useState, useMemo } from "react"; // Use useCallback and useMemo
+import { useCallback, useEffect, useMemo, useState } from "react";
import { codeBuddyMode, faqItems, modelOptions, themeOptions } from "../constants/constant";
+import { IWebviewMessage, useStreamingChat } from "../hooks/useStreamingChat";
import { getChatCss } from "../themes/chat_css";
import { updateStyles } from "../utils/dynamicCss";
import { highlightCodeBlocks } from "../utils/highlightCode";
+import { FAQAccordion } from "./accordion";
import AttachmentIcon from "./attachmentIcon";
import { BotMessage } from "./botMessage";
-import { UserMessage } from "./personMessage";
-import { ModelDropdown } from "./select";
+import ChatInput from "./ChatInput";
+import { CommandFeedbackLoader } from "./commandFeedbackLoader";
import WorkspaceSelector from "./context";
-import TextInput from "./textInput";
-import ToggleButton from "./toggleButton";
-import Button from "./button";
-import { FAQAccordion } from "./accordion";
+import { Extensions } from "./extensions";
+import { FutureFeatures } from "./futureFeatures";
+import { UserMessage } from "./personMessage";
+import { Settings } from "./settings";
import { SkeletonLoader } from "./skeletonLoader";
-import { CommandFeedbackLoader } from "./commandFeedbackLoader";
-import ChatInput from "./ChatInput";
+import { WelcomeScreen } from "./welcomeUI";
const hljsApi = window["hljs" as any] as unknown as typeof hljs;
@@ -46,10 +35,10 @@ const vsCode = (() => {
};
})();
-// Define a type for configuration data for better type safety
interface ConfigData {
username?: string;
theme?: string;
+ enableStreaming?: boolean;
}
export const WebviewUI = () => {
@@ -57,8 +46,6 @@ export const WebviewUI = () => {
const [selectedTheme, setSelectedTheme] = useState("tokyo night");
const [selectedModel, setSelectedModel] = useState("Gemini");
const [selectedCodeBuddyMode, setSelectedCodeBuddyMode] = useState("Ask");
- const [messages, setMessages] = useState([]);
- const [isBotLoading, setIsBotLoading] = useState(false);
const [commandAction, setCommandAction] = useState("");
const [commandDescription, setCommandDescription] = useState("");
const [isCommandExecuting, setIsCommandExecuting] = useState(false);
@@ -68,70 +55,109 @@ export const WebviewUI = () => {
const [activeEditor, setActiveEditor] = useState("");
const [username, setUsername] = useState("");
const [darkMode, setDarkMode] = useState(false);
+ const [enableStreaming, setEnableStreaming] = useState(true);
// Ref for username input element
- const nameInputRef = useRef(null);
+ // const nameInputRef = useRef(null);
+
+ // Initialize streaming chat hook
+ const {
+ messages: streamedMessages,
+ isStreaming,
+ isLoading: isBotLoading,
+ sendMessage,
+ clearMessages,
+ setMessages,
+ } = useStreamingChat(vsCode, {
+ enableStreaming,
+ onLegacyMessage: (messages) => {
+ console.log("Legacy message received:", messages);
+ },
+ });
// Memoize the chat CSS to prevent unnecessary re-renders
const chatCss = useMemo(() => getChatCss(selectedTheme), [selectedTheme]);
- // Use useCallback to prevent unnecessary re-renders of the message handler
- const messageHandler = useCallback((event: any) => {
+ // Legacy message handler for non-streaming events
+ const legacyMessageHandler = useCallback((event: any) => {
const message = event.data;
- switch (message.type) {
- case "bot-response":
- // Use functional updates to ensure we're working with the most recent state
- setMessages((prevMessages) => [
- ...(prevMessages || []),
- {
- type: "bot",
- content: message.message,
- language: "Typescript",
- alias: "O",
- },
- ]);
- setIsBotLoading(false);
- setIsCommandExecuting(false);
- setCommandAction("");
- setCommandDescription("");
- break;
+ const messageType = message.command || message.type;
+
+ // Handle stream end to clear command execution state
+ // (the actual message handling is done by useStreamingChat hook)
+ if (messageType === 'onStreamEnd' || messageType === 'onStreamError') {
+ setIsCommandExecuting(false);
+ setCommandAction("");
+ setCommandDescription("");
+ return;
+ }
+
+ // Skip other streaming-related messages as they're handled by the hook
+ if (message.command?.includes('stream') || message.type?.includes('stream')) {
+ return;
+ }
+
+ switch (messageType) {
case "codebuddy-commands":
- // Handle command feedback - show what action is being performed
console.log("Command feedback received:", message.message);
setIsCommandExecuting(true);
if (typeof message.message === "object" && message.message.action && message.message.description) {
setCommandAction(message.message.action);
setCommandDescription(message.message.description);
} else {
- // Fallback for legacy string format
setCommandAction(message.message || "Processing request");
setCommandDescription("CodeBuddy is analyzing your code and generating a response...");
}
break;
+
case "bootstrap":
setFolders(message);
break;
+
case "chat-history":
try {
- // Parse the chat history efficiently using JSON.parse
const parsedMessages = JSON.parse(message.message);
- setMessages((prevMessages) => [...parsedMessages, ...(prevMessages || [])]);
+ const formattedMessages: IWebviewMessage[] = parsedMessages.map((msg: any) => ({
+ id: `history-${Date.now()}-${Math.random()}`,
+ type: msg.type,
+ content: msg.content,
+ language: msg.language,
+ alias: msg.alias,
+ timestamp: Date.now(),
+ }));
+ setMessages((prev: IWebviewMessage[]) => [...formattedMessages, ...prev]);
} catch (error: any) {
- console.log(error);
- throw new Error(error.message);
+ console.error("Error parsing chat history:", error);
}
break;
+
case "error":
console.error("Extension error", message.payload);
- setIsBotLoading(false);
+ setIsCommandExecuting(false);
+ setCommandAction("");
+ setCommandDescription("");
+ break;
+
+ case "bot-response":
+ // Clear command execution state when bot response is received
+ // The actual message handling is done by useStreamingChat hook
+ setIsCommandExecuting(false);
+ setCommandAction("");
+ setCommandDescription("");
break;
+
case "onActiveworkspaceUpdate":
setActiveEditor(message.message ?? "");
break;
+
case "onConfigurationChange": {
const data = JSON.parse(message.message);
- return data;
+ if (data.enableStreaming !== undefined) {
+ setEnableStreaming(data.enableStreaming);
+ }
+ break;
}
+
case "onGetUserPreferences": {
const data: ConfigData = JSON.parse(message.message);
if (data.username) {
@@ -140,41 +166,54 @@ export const WebviewUI = () => {
if (data.theme) {
setSelectedTheme(data.theme);
}
- return data;
+ if (data.enableStreaming !== undefined) {
+ setEnableStreaming(data.enableStreaming);
+ }
+ break;
}
+
case "theme-settings":
- // Handle theme settings from extension
if (message.theme) {
setSelectedTheme(message.theme);
}
break;
+
default:
- console.warn("Unknown message type", message.type);
+ // Ignore unknown message types
+ break;
}
- }, []);
+ }, [setMessages]);
- // Update CSS whenever theme changes using useEffect
+ // Update CSS whenever theme changes
useEffect(() => {
- // Update styles using dynamicCss utility
updateStyles(chatCss);
}, [chatCss]);
- // Initialize event listener for messages from VS Code
+ // Initialize legacy event listener
useEffect(() => {
- window.addEventListener("message", messageHandler);
+ window.addEventListener("message", legacyMessageHandler);
return () => {
- window.removeEventListener("message", messageHandler);
+ window.removeEventListener("message", legacyMessageHandler);
};
- }, [messageHandler]); // Dependency array includes messageHandler
+ }, [legacyMessageHandler]);
- // Separate effect for highlighting code blocks using highlightCodeBlocks utility
+ // Highlight code blocks when messages update
useEffect(() => {
- highlightCodeBlocks(hljsApi, messages);
- }, [messages]);
+ highlightCodeBlocks(hljsApi, streamedMessages);
+ }, [streamedMessages]);
+
+ // Clear command execution state when streaming completes
+ useEffect(() => {
+ if (!isStreaming && !isBotLoading) {
+ setIsCommandExecuting(false);
+ setCommandAction("");
+ setCommandDescription("");
+ }
+ }, [isStreaming, isBotLoading]);
const handleClearHistory = useCallback(() => {
- setMessages([]);
- }, []);
+ clearMessages();
+ }, [clearMessages]);
const handleUserPreferences = useCallback(() => {
vsCode.postMessage({
@@ -189,70 +228,26 @@ export const WebviewUI = () => {
setSelectedContext(value);
}, []);
- const handleNameChange = useCallback((e: React.ChangeEvent) => {
- setUsername(e.target.value);
- }, []);
+ // const handleNameChange = useCallback((e: React.ChangeEvent) => {
+ // setUsername(e.target.value);
+ // }, []);
const handleToggle = useCallback((isActive: boolean) => {
setDarkMode(isActive);
- // Apply theme change logic here
document.body.classList.toggle("dark-mode", isActive);
}, []);
- const handleModelChange = useCallback((e: any) => {
- const newValue = e.target.value;
- setSelectedModel(newValue);
- vsCode.postMessage({
- command: "update-model-event",
- message: newValue,
- });
- }, []);
-
- const handleCodeBuddyMode = useCallback((e: any) => {
- const newValue = e.target.value;
- setSelectedCodeBuddyMode(newValue);
- vsCode.postMessage({
- command: "codebuddy-model-change-event",
- message: newValue,
- });
- }, []);
-
- const handleThemeChange = useCallback((e: any) => {
- const newValue = e.target.value;
- setSelectedTheme(newValue);
- // Optionally save theme preference to extension storage
- vsCode.postMessage({
- command: "theme-change-event",
- message: newValue,
- });
- }, []);
-
- // Use useCallback to prevent unnecessary re-renders of the handleSend function
const handleSend = useCallback(
(message: string) => {
if (!message.trim()) return;
- setMessages((previousMessages) => [
- ...(previousMessages || []),
- {
- type: "user",
- content: message,
- alias: "O",
- },
- ]);
-
- setIsBotLoading(true);
-
- vsCode.postMessage({
- command: "user-input",
- message: message,
- metaData: {
- mode: selectedCodeBuddyMode,
- context: selectedContext.split("@"),
- },
+ sendMessage(message, {
+ mode: selectedCodeBuddyMode,
+ context: selectedContext.split("@"),
+ alias: "O",
});
},
- [selectedCodeBuddyMode, selectedContext]
+ [sendMessage, selectedCodeBuddyMode, selectedContext]
);
const handleGetContext = useCallback(() => {
@@ -268,14 +263,23 @@ export const WebviewUI = () => {
}, [selectedContext]);
const memoizedMessages = useMemo(() => {
- return messages.map((msg) =>
+ return streamedMessages.map((msg) =>
msg.type === "bot" ? (
-
+
) : (
-
+
)
);
- }, [messages]);
+ }, [streamedMessages]);
return (
@@ -287,72 +291,100 @@ export const WebviewUI = () => {
SETTINGS
setActiveTab("tab-3")}>
- FAQ
+ EXTENSIONS
setActiveTab("tab-4")}>
- OTHERS
+ FAQ
+
+ setActiveTab("tab-5")}>
+ FUTURE
-
- {memoizedMessages}
- {isCommandExecuting && (
-
+ {/* Show welcome screen when there are no messages */}
+ {streamedMessages.length === 0 && !isBotLoading && !isCommandExecuting ? (
+ {
+ // Optional: Focus on the input or trigger a sample prompt
+ console.log("User is ready to start!");
+ }}
+ />
+ ) : (
+ <>
+ {memoizedMessages}
+ {isCommandExecuting && (
+
+ )}
+ {isBotLoading && !isCommandExecuting && !isStreaming && }
+ >
)}
- {isBotLoading && !isCommandExecuting && }
-
-
- Nickname
-
-
-
-
-
-
-
-
- {" "}
- Index Codebase
-
- {" "}
-
-
-
-
+ {
+ setSelectedTheme(value);
+ vsCode.postMessage({ command: "theme-change-event", message: value });
+ }}
+ onModelChange={(value) => {
+ setSelectedModel(value);
+ vsCode.postMessage({ command: "update-model-event", message: value });
+ }}
+ onCodeBuddyModeChange={(value) => {
+ setSelectedCodeBuddyMode(value);
+ vsCode.postMessage({ command: "codebuddy-model-change-event", message: value });
+ }}
+ onStreamingChange={setEnableStreaming}
+ onDarkModeChange={handleToggle}
+ onClearHistory={handleClearHistory}
+ onSavePreferences={handleUserPreferences}
+ />
+
+ console.log('Add server:', server)}
+ onAddAgent={(agent) => console.log('Add agent:', agent)}
+ />
+
+
+
- ({
- question: i.question,
- answer: ,
- }))}
- />
+
+
+
+
+
+
+
+
+
- In Dev
+
+
+
+
{
- {/* Replace VSCodeTextArea with ChatInput */}
+
-
-
-
-