[VSC-EXT] Thought process (#1798)

Co-authored-by: Sarthak Agarwal <sarthak.saga@gmail.com>
Co-authored-by: saga4 <saga4@codeflashs-MacBook-Air.local>
This commit is contained in:
mohammed ahmed 2025-09-18 21:48:00 +03:00 committed by GitHub
parent 5d71db60e6
commit 25c6da57a3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
54 changed files with 5525 additions and 1529 deletions

View file

@ -43,9 +43,9 @@ export default [
{
prefer: "type-imports",
disallowTypeAnnotations: false,
fixStyle: "separate-type-imports"
}
]
fixStyle: "separate-type-imports",
},
],
},
},
];
];

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,7 @@
"name": "codeflash",
"displayName": "Codeflash",
"description": "Optimize Your Python Code - Automatically",
"version": "0.0.4",
"version": "0.0.5",
"icon": "media/Codeflash_black_background.jpg",
"publisher": "codeflash",
"repository": {
@ -12,7 +12,9 @@
"engines": {
"vscode": "^1.94.0"
},
"activationEvents": [],
"activationEvents": [
"onLanguage:python"
],
"main": "./dist/extension.js",
"contributes": {
"viewsContainers": {
@ -40,12 +42,6 @@
"title": "Optimize Function with Codeflash",
"category": "Codeflash"
},
{
"command": "codeflash.refreshAnalysis",
"title": "Refresh Analysis",
"category": "Codeflash",
"icon": "$(refresh)"
},
{
"command": "codeflash.optimizeAll",
"title": "Optimize All Functions",
@ -79,10 +75,6 @@
"command": "codeflash.optimizeFunction",
"when": "editorLangId == python"
},
{
"command": "codeflash.refreshAnalysis",
"when": "editorLangId == python"
},
{
"command": "codeflash.optimizeAll",
"when": "editorLangId == python"
@ -103,28 +95,13 @@
"command": "codeflash.showRefactorDiff",
"when": "editorLangId == python"
}
],
"editor/context": [
{
"command": "codeflash.refreshAnalysis",
"when": "editorLangId == python",
"group": "codeflash@1"
}
]
},
"keybindings": [
{
"command": "codeflash.refreshAnalysis",
"key": "ctrl+shift+f5",
"mac": "cmd+shift+f5",
"when": "editorLangId == python"
}
]
}
},
"scripts": {
"vsce": "vsce package",
"vscode:prepublish": "npm run package",
"dev": "npm run compile && concurrently \"npm run watch\" \"npm run build:watch --prefix packages/sidebar-webview\"",
"dev": "concurrently \"npm run watch\" \"npm run build:watch --prefix packages/sidebar-webview\"",
"build:webview": "npm run build --prefix packages/sidebar-webview",
"build": "npm run check-types && npm run lint && npm run build:webview && node esbuild.js",
"compile": "npm run build",
@ -145,12 +122,14 @@
"pre-commit": "lint-staged"
},
"lint-staged": {
"*.{js,jsx,ts,tsx,html}": "prettier --write",
"*.{js,jsx,ts,tsx,html,css}": "prettier --write",
"*.{js,jsx,ts,tsx}": [
"eslint --fix"
]
},
"devDependencies": {
"@shikijs/langs": "^3.12.2",
"@shikijs/themes": "^3.12.2",
"@types/mocha": "^10.0.10",
"@types/node": "20.x",
"@types/vscode": "^1.94.0",
@ -158,24 +137,26 @@
"@typescript-eslint/parser": "^8.25.0",
"@vscode/test-cli": "^0.0.10",
"@vscode/test-electron": "^2.4.1",
"concurrently": "^9.2.0",
"@vscode/vsce": "^3.6.0",
"concurrently": "^9.2.0",
"debounce": "^2.2.0",
"esbuild": "^0.25.0",
"eslint": "^9.21.0",
"husky": "^9.1.7",
"lint-staged": "^15.5.2",
"npm-run-all": "^4.1.5",
"prettier": "^3.6.2",
"react-virtuoso": "^4.14.0",
"typescript": "^5.7.3",
"valtio": "^2.1.5"
},
"dependencies": {
"@codeflash/shared": "*",
"@codeflash/types": "*",
"@vscode/python-extension": "^1.0.5",
"marked": "^15.0.12",
"p-queue": "^8.1.0",
"vscode-languageclient": "^9.0.1",
"@codeflash/types": "*",
"@codeflash/shared": "*"
"vscode-languageclient": "^9.0.1"
},
"private": true,
"workspaces": [

View file

@ -11,3 +11,7 @@ export function canDeleteTaskInQueue(task: QueueTaskItem): boolean {
task.status === "skipped"
);
}
export function isTaskRunning(task: QueueTaskItem): boolean {
return task.status === "optimizing" || task.status === "initializing";
}

View file

@ -20,6 +20,10 @@
"@types/react-dom": "^19.1.7",
"@types/vscode-webview": "^1.57.5",
"@vitejs/plugin-react": "^4.7.0",
"react-markdown": "^10.1.0",
"react-virtuoso": "^4.14.0",
"remark-gfm": "^4.0.1",
"shiki": "^3.12.2",
"typescript": "^5.9.2",
"vite": "^6.3.5",
"vite-plugin-static-copy": "^3.1.2",

View file

@ -4,12 +4,13 @@ import ApiKeyForm from "./components/apiKeyError";
import { messageHandler } from "./utils/webviewMessageHandler";
// import OptimizeCurrentDiff from "./components/optimizeCurrentDiff";
import { vscode } from "./utils/vscode";
// import CurrentFileFunctions from "./components/currentFileFunctions";
import ChatView from "./components/chatView";
import Tabs from "./components/tabs";
import OptimizationQueue from "./components/optimizationQueue";
import CurrentFileFunctions from "./components/currentFileFunctions";
function App() {
const store = useStore();
useEffect(() => {
vscode.postMessage({
type: "webviewReady",
@ -29,9 +30,16 @@ function App() {
return (
<div className="app-container">
<Tabs />
<div className="content">
{store.activeTab === "optimization" && <ChatView />}
{store.activeTab === "tasks" && (
<OptimizationQueue queueTasks={store.queueTasks} />
)}
</div>
{/* <OptimizeCurrentDiff /> */}
<OptimizationQueue queueTasks={store.queueTasks} />
{store.functionsInCurrentFile.length > 0 && <CurrentFileFunctions />}
{/* {store.functionsInCurrentFile.length > 0 && <CurrentFileFunctions />} */}
</div>
);
}

View file

@ -1,10 +1,11 @@
import { useEffect, useRef } from "react";
import { vscode } from "../utils/vscode";
import type { WebviewMessage } from "@codeflash/types";
import { useStore } from "../store/root";
const ApiKeyForm = () => {
const inputRef = useRef<HTMLInputElement>(null);
const loading = useStore((state) => state.isValidatingApiKey);
useEffect(() => {
const onApiKeyInvalid = (message: MessageEvent) => {
const sidebarMessage = message.data as WebviewMessage;
@ -55,7 +56,9 @@ const ApiKeyForm = () => {
});
}
}}
disabled={loading}
/>
{loading && <span className="codicon codicon-loading spin"></span>}
</div>
<p className="hint">
You can generate an API key from your{" "}

View file

@ -0,0 +1,177 @@
.container {
display: flex;
flex-direction: column;
gap: 1rem;
padding: 1rem 0;
color: var(--vscode-editor-foreground);
font-size: 0.875rem;
height: 100%;
position: relative;
}
.inputForm {
width: 100%;
display: flex;
position: relative;
}
.wrapper {
z-index: 2 !important;
overflow: visible;
flex: 1;
display: flex;
flex-direction: column;
gap: 0.75rem;
padding: 0.4rem 1rem;
border: 1px solid var(--vscode-input-border, transparent);
border-radius: 12px;
background: var(--vscode-editor-background);
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
position: relative;
margin: 0 0.5rem;
}
.wrapper:focus-within {
box-shadow: 0 0 0 2px var(--vscode-input-background);
}
.inputWrapper {
display: flex;
align-items: center;
justify-content: space-between;
}
.input {
flex: 1;
width: 100%;
border: none;
outline: none !important;
background: transparent;
color: var(--vscode-input-foreground);
font-size: 0.875rem;
padding: 0.2rem 0.5rem;
line-height: 1.5;
}
.input::placeholder {
color: var(--vscode-input-placeholderForeground);
}
.chips {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.15rem; /* was 0.2rem */
}
.fileEntry,
.fileEntry div {
display: flex;
align-items: center;
gap: 0.2rem;
}
.path {
font-size: 0.7rem;
color: var(--vscode-editorWidget-foreground);
opacity: 0.5;
}
.button {
background-color: color-mix(
in srgb,
var(--vscode-editor-foreground) 80%,
transparent
);
color: var(--vscode-editor-background);
border: 1px solid var(--vscode-editorWidget-border, transparent);
border-radius: 50%;
cursor: pointer;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
opacity: 0.6;
transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.button span {
font-size: 13px;
line-height: 1;
}
.button:disabled {
opacity: 0.2;
cursor: default;
}
.button:hover:not(:disabled) {
opacity: 1;
background-color: var(--vscode-editor-foreground);
color: var(--vscode-editor-background);
}
/* Dropdown */
.dropdown {
position: absolute;
top: 100%;
min-width: 12rem;
max-width: calc(100% - 1rem);
max-height: 200px;
overflow-y: auto;
overflow-x: hidden;
list-style: none;
margin: 0.5rem 0 0 0;
padding: 0.5rem;
background: var(--vscode-editorWidget-background);
border: 1px solid var(--vscode-editorWidget-border, rgba(255, 255, 255, 0.1));
border-radius: 12px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
z-index: 100;
backdrop-filter: blur(8px);
animation: dropdownFadeIn 0.2s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
@keyframes dropdownFadeIn {
0% {
opacity: 0;
transform: translateY(-8px) scale(0.95);
}
100% {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.dropdown li {
display: flex;
align-items: center;
gap: 0.15rem;
padding: 0.4rem 0.2rem;
cursor: pointer;
color: var(--vscode-editorWidget-foreground);
font-size: 0.8rem;
border-radius: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.dropdown li span {
color: var(--vscode-editorWidget-foreground);
transition: color 0.2s ease;
}
.dropdown li:hover,
.dropdown li.active {
background: color-mix(
in srgb,
var(--vscode-list-hoverBackground) 80%,
var(--vscode-editorWidget-foreground)
);
color: var(--vscode-list-hoverForeground);
}

View file

@ -0,0 +1,375 @@
import React, { useEffect, useRef, useState } from "react";
import styles from "./chatView.module.css";
import { useStore } from "../store/root";
import { vscode } from "../utils/vscode";
import type { FileInWorkspace, QueueTaskItem } from "@codeflash/types";
import TaskLogging from "./tasksLogging";
import { isTaskRunning, canRetryTaskInQueue } from "@codeflash/shared";
import Chip from "./chip/chip";
import ChatViewBanner from "./chatViewBanner";
const ChatView: React.FC = () => {
const filesInWorkspace = useStore((state) => state.filesInWorkspace);
const functionsInCurrentContextFile = useStore(
(state) => state.functionsInCurrentContextFile,
);
const focusedTask = useStore((state) =>
state.queueTasks.find((t) => t.id === state.focusedTaskId),
);
const currentRunningTask = useStore((state) =>
state.queueTasks.find(isTaskRunning),
);
return (
<div className={styles.container}>
{/* {focusedTask?.status === "completed" && (
<div className={styles.foundOptimization}>
<span>Found the best optimization</span>
<button className={styles.viewOptimizationButton}>
<div className="glowEffect modePulse"></div>
<span>View Optimization</span>
</button>
</div>
)} */}
<ChatInput
fileSuggestions={filesInWorkspace}
functionSuggestions={functionsInCurrentContextFile}
currentOptimizationTask={
currentRunningTask && focusedTask?.id == currentRunningTask.id
? currentRunningTask
: undefined
}
/>
{focusedTask ? <TaskLogging task={focusedTask} /> : <ChatViewBanner />}
</div>
);
};
const getFileNameFromPath = (path: string) => {
const parts = path.split("/");
return parts[parts.length - 1];
};
const getMinimalPath = (path: string) => {
const parts = path.split("/");
return parts.length > 2 ? parts.slice(-2).join("/") : path;
};
interface ChatInputProps {
onSend?: (text: string) => void;
fileSuggestions?: FileInWorkspace[];
functionSuggestions?: { file: string; functions: string[] };
currentOptimizationTask: QueueTaskItem | undefined;
}
const ChatInput: React.FC<ChatInputProps> = ({
fileSuggestions = [],
functionSuggestions = { file: "", functions: [] },
currentOptimizationTask,
}) => {
const currentFocusedFile = useStore((state) => state.currentFilePath);
const queueTasks = useStore((state) => state.queueTasks);
const [value, setValue] = useState("");
const [showDropdown, setShowDropdown] = useState(false);
const [filtered, setFiltered] = useState<FileInWorkspace[] | string[]>([]);
const [selectedIndex, setSelectedIndex] = useState(0);
const [selectedFile, setSelectedFile] = useState<string>();
const [selectedFunction, setSelectedFunction] = useState<string>();
const [activeDropdown, setActiveDropdown] = useState<"file" | "function">(
"file",
);
const inputRef = useRef<HTMLInputElement>(null);
// Initialize with current file + request workspace files if not provided
useEffect(() => {
if (currentOptimizationTask) {
return;
}
if (currentFocusedFile) {
setSelectedFile(currentFocusedFile);
setSelectedFunction(undefined);
}
setShowDropdown(false);
}, [currentFocusedFile]);
useEffect(() => {
if (!fileSuggestions.length) {
vscode.postMessage({ type: "filesInWorkspace" });
}
}, []);
useEffect(() => {
if (currentOptimizationTask) {
setSelectedFile(currentOptimizationTask.filepath);
setSelectedFunction(currentOptimizationTask.functionName);
}
}, [currentOptimizationTask]);
// Ensure active item stays visible while navigating
useEffect(() => {
if (!showDropdown || !filtered.length) {
return;
}
const container = document.querySelector(`.${styles.dropdown}`);
if (!container) {
return;
}
const activeItem = container.children[selectedIndex] as HTMLElement;
if (!activeItem) {
return;
}
const containerTop = container.scrollTop;
const containerBottom = containerTop + container.clientHeight;
const itemTop = activeItem.offsetTop;
const itemBottom = itemTop + activeItem.offsetHeight;
if (itemTop < containerTop) {
container.scrollTop = itemTop;
} else if (itemBottom > containerBottom) {
container.scrollTop = itemBottom - container.clientHeight;
}
}, [selectedIndex, showDropdown, filtered]);
/** Handle input change and update dropdown */
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newVal = e.target.value;
setValue(newVal);
const fileMatch = newVal.match(/@([\w\/\.\-_]*)$/);
if (fileMatch) {
const query = fileMatch[1]?.toLowerCase();
const matches = query
? fileSuggestions.filter((f) =>
getFileNameFromPath(f.rel)?.toLowerCase()?.includes(query),
)
: fileSuggestions;
setFiltered(matches);
setActiveDropdown("file");
setShowDropdown(matches.length > 0);
setSelectedIndex(0);
return;
}
const query = newVal.toLowerCase().trim();
const matches = query
? functionSuggestions.functions.filter((f) =>
f.toLowerCase().includes(query),
)
: functionSuggestions.functions;
setFiltered(matches);
setActiveDropdown("function");
setShowDropdown(matches.length > 0);
setSelectedIndex(0);
};
/** Keyboard navigation */
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (!showDropdown) {
return;
}
if (e.key === "ArrowDown") {
e.preventDefault();
setSelectedIndex((prev) => (prev + 1) % filtered.length);
} else if (e.key === "ArrowUp") {
e.preventDefault();
setSelectedIndex(
(prev) => (prev - 1 + filtered.length) % filtered.length,
);
} else if (e.key === "Enter") {
e.preventDefault();
const choice = filtered[selectedIndex];
if (choice) {
handleSelect(choice as string);
}
} else if (e.key === "Escape") {
e.preventDefault();
setShowDropdown(false);
}
};
/** Handle selecting from dropdown */
const handleSelect = (choice: string | FileInWorkspace) => {
if (activeDropdown === "file") {
const file = choice as FileInWorkspace;
requestFunctionsFromFile(file.abs);
setSelectedFile(file.abs);
setSelectedFunction(undefined);
inputRef.current?.focus(); // focus the input to select the function
} else {
setSelectedFunction(choice as string);
}
setShowDropdown(false);
setValue("");
};
/** Handle form submit */
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!selectedFile || !selectedFunction) {
console.log("No selected file or function");
return;
}
const existingTask = queueTasks.find(
(task) =>
task.filepath === selectedFile &&
task.functionName === selectedFunction,
);
const allowOptimization = existingTask
? canRetryTaskInQueue(existingTask)
: true;
if (!allowOptimization) {
vscode.postMessage({
type: "showMessage",
payload: {
type: "warning",
message: `this function task is already ${existingTask?.status}, you can view it in the tasks tab`, // TODO: better message
},
});
return;
}
vscode.postMessage({
type: "optimizeFunction",
payload: {
filePath: selectedFile,
functionName: selectedFunction,
},
});
setValue("");
setShowDropdown(false);
};
const placeholderText = !selectedFile
? "type @ to choose the context file"
: !selectedFunction
? "type a function to optimize"
: "";
const locked = !!currentOptimizationTask;
const requestFunctionsFromFile = (file?: string) => {
const finalPath = file || selectedFile;
if (!finalPath) {
return;
}
vscode.postMessage({
type: "requestFunctions",
payload: {
filePath: finalPath,
},
});
};
return (
<form className={styles.inputForm} onSubmit={handleSubmit}>
<div className="glowEffect modePulse"></div>
<div
className={styles.wrapper}
style={
locked
? {
flexDirection: "row",
justifyContent: "space-between",
}
: {}
}
onClick={(e) => {
e.stopPropagation();
inputRef.current?.focus();
}}
>
{(selectedFile || selectedFunction) && (
<div className={styles.chips}>
{selectedFile && (
<Chip key={selectedFile} onClick={(e) => e.stopPropagation()}>
<span className="codicon codicon-python"></span>
{getFileNameFromPath(selectedFile)}
</Chip>
)}
{selectedFunction && (
<Chip key={selectedFunction} onClick={(e) => e.stopPropagation()}>
<span className="codicon codicon-symbol-method"></span>
{selectedFunction}
</Chip>
)}
</div>
)}
<div className={styles.inputWrapper}>
{!locked && (
<input
ref={inputRef}
className={styles.input}
type="text"
placeholder={placeholderText}
value={value}
onChange={handleChange}
onKeyDown={handleKeyDown}
readOnly={locked}
/>
)}
<button
type="submit"
className={styles.button}
disabled={!selectedFile || !selectedFunction || locked}
>
{locked ? (
<span className="codicon codicon-loading spin"></span>
) : (
<span className="codicon codicon-arrow-up"></span>
)}
</button>
</div>
</div>
{showDropdown && (
<ul className={styles.dropdown}>
{filtered.map((f, i) => {
const key = typeof f === "string" ? f : f.rel;
const isActive = i === selectedIndex;
return (
<li
key={key}
className={isActive ? styles.active : ""}
onClick={() => handleSelect(f)}
>
{activeDropdown === "function" && typeof f === "string" ? (
<>
<span className="codicon codicon-symbol-method"></span>
<span>{f}</span>
</>
) : (
<div className={styles.fileEntry}>
<div>
<span className="codicon codicon-python"></span>
<span>
{getFileNameFromPath((f as FileInWorkspace).rel)}
</span>
</div>
<span className={styles.path}>
{getMinimalPath((f as FileInWorkspace).rel)}
</span>
</div>
)}
</li>
);
})}
</ul>
)}
</form>
);
};
export default ChatView;

View file

@ -0,0 +1,17 @@
import styles from "../styles/chatViewBanner.module.css";
import CodeflashLogo from "./logo";
const ChatViewBanner = () => {
return (
<div className={styles.chatViewBanner}>
<div className={styles.logoWrapper}>
<div className={styles.logoContainer}>
<CodeflashLogo width="60%" height="100%" />
</div>
<p className={styles.description}>Optimize your python code with AI.</p>
</div>
</div>
);
};
export default ChatViewBanner;

View file

@ -0,0 +1,13 @@
.chip {
width: fit-content;
background: var(--vscode-editorWidget-background);
color: var(--vscode-editorWidget-foreground);
border: 1px solid var(--vscode-editorWidget-border, transparent);
border-radius: 7px;
padding: 0.15rem 0.4rem;
font-size: 0.75rem;
display: inline-flex;
align-items: center;
gap: 0.2rem;
margin-right: 2px;
}

View file

@ -0,0 +1,17 @@
import styles from "./chip.module.css";
interface Props extends React.ComponentProps<"span"> {
children: React.ReactNode;
}
const Chip = ({ children, ...props }: Props) => {
return (
<span
{...props}
className={styles.chip}
onClick={(e) => e.stopPropagation()}
>
{children}
</span>
);
};
export default Chip;

View file

@ -0,0 +1,108 @@
.codeBlockContainer {
position: relative;
overflow: hidden;
background-color: var(--code-block-bg);
border: 1px solid var(--vscode-editorGroup-border, transparent);
padding: 10px;
border-radius: 5px;
}
.codeBlockContainer .codeBlockButtonWrapper {
opacity: 0;
pointer-events: none;
transition: opacity 0.2s; /* Keep opacity transition for buttons */
}
.codeBlockContainer[data-partially-visible="true"]:hover
.codeBlockButtonWrapper {
opacity: 1;
pointer-events: all;
cursor: pointer;
}
.preWrapper {
background-color: var(--code-block-bg);
/* max-height: 400px; */
overflow-y: auto;
padding: 10px;
border-radius: 5px;
}
.preWrapper code {
background-color: transparent !important;
}
.preWrapper .hljs {
color: var(--vscode-editor-foreground, #fff);
background-color: var(--code-block-bg);
}
.preWrapper pre {
background-color: var(--code-block-bg);
border-radius: 5px;
margin: 0;
padding: 10px;
width: 100%;
box-sizing: border-box;
}
.preWrapper pre,
.preWrapper code {
/* Undefined wordwrap defaults to true (pre-wrap) behavior. */
white-space: pre-wrap;
word-break: normal;
overflow-wrap: break-word;
font-size: var(--vscode-editor-font-size, var(--vscode-font-size, 10px));
font-family: var(--vscode-editor-font-family);
outline: none;
}
.button {
background: transparent;
border: none;
color: var(--vscode-foreground);
cursor: var(--copy-button-cursor, default);
padding: 4px;
margin: 0 0px;
display: flex;
align-items: center;
justify-content: center;
opacity: 0.4;
border-radius: 3px;
pointer-events: var(--copy-button-events, none);
margin-left: 4px;
height: 24px;
width: 24px;
outline: none;
}
.button:hover {
background: var(--vscode-toolbar-hoverBackground);
opacity: 1;
}
.button span {
display: block;
}
.codeBlockButtonWrapper {
position: absolute;
top: 4px;
right: 4px;
height: auto;
z-index: 40;
background: var(--code-block-bg);
overflow: visible;
pointer-events: none;
opacity: var(--copy-button-opacity, 0);
padding: 4px 6px;
border-radius: 3px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.codeBlockButtonWrapper:hover {
opacity: 1 !important;
}
.codeBlockButtonWrapper .button {
position: relative;
top: 0;
right: 0;
}

View file

@ -0,0 +1,735 @@
import { memo, useEffect, useRef, useCallback, useState } from "react";
import {
getHighlighter,
isLanguageLoaded,
normalizeLanguage,
type ExtendedLanguage,
} from "../../utils/highlighter";
import { useCopyToClipboard } from "../../utils/clipboard";
import type { ShikiTransformer } from "shiki";
import { toJsxRuntime } from "hast-util-to-jsx-runtime";
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
import styles from "./codeBlock.module.css";
// Configuration constants
export const WINDOW_SHADE_SETTINGS = {
transitionDelayS: 0.2,
collapsedHeight: 500, // Default collapsed height in pixels
};
// Tolerance in pixels for determining when a container is considered "at the bottom"
export const SCROLL_SNAP_TOLERANCE = 20;
interface CodeBlockProps {
source?: string;
rawSource?: string; // Add rawSource prop for copying raw text
language: string;
preStyle?: React.CSSProperties;
initialWordWrap?: boolean;
collapsedHeight?: number;
// initialWindowShade?: boolean;
}
const CodeBlock = memo(
({
source,
rawSource,
language,
preStyle,
initialWordWrap = true,
// initialWindowShade = true,
collapsedHeight,
}: CodeBlockProps) => {
// const [wordWrap, _setWordWrap] = useState(initialWordWrap);
// const [windowShade, setWindowShade] = useState(initialWindowShade);
const [currentLanguage, setCurrentLanguage] = useState<ExtendedLanguage>(
() => normalizeLanguage(language),
);
const userChangedLanguageRef = useRef(false);
const [highlightedCode, setHighlightedCode] =
useState<React.ReactNode>(null);
// const [showCollapseButton, setShowCollapseButton] = useState(true);
const codeBlockRef = useRef<HTMLDivElement>(null);
const preRef = useRef<HTMLDivElement>(null);
const copyButtonWrapperRef = useRef<HTMLDivElement>(null);
const { showCopyFeedback, copyWithFeedback } = useCopyToClipboard();
const isMountedRef = useRef(true);
const buttonPositionTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const collapseTimeout1Ref = useRef<NodeJS.Timeout | null>(null);
const collapseTimeout2Ref = useRef<NodeJS.Timeout | null>(null);
// Update current language when prop changes, but only if user hasn't
// made a selection.
useEffect(() => {
const normalizedLang = normalizeLanguage(language);
if (
normalizedLang !== currentLanguage &&
!userChangedLanguageRef.current
) {
setCurrentLanguage(normalizedLang);
}
}, [language, currentLanguage]);
// Syntax highlighting with cached Shiki instance and mounted state management
useEffect(() => {
// Set mounted state at the beginning of this effect
isMountedRef.current = true;
// Create a safe fallback using React elements instead of HTML string
const fallback = (
<pre style={{ padding: 0, margin: 0 }}>
<code className={`hljs language-${currentLanguage || "txt"}`}>
{source || ""}
</code>
</pre>
);
const highlight = async () => {
// Show plain text if language needs to be loaded.
if (currentLanguage && !isLanguageLoaded(currentLanguage)) {
if (isMountedRef.current) {
setHighlightedCode(fallback);
}
}
const highlighter = await getHighlighter(currentLanguage);
if (!isMountedRef.current) {
return;
}
const hast = await highlighter.codeToHast(source || "", {
lang: currentLanguage || "txt",
theme: document.body.className.toLowerCase().includes("light")
? "github-light"
: "github-dark",
transformers: [
{
pre(node) {
node.properties.style = "padding: 0; margin: 0;";
return node;
},
code(node) {
// Add hljs classes for consistent styling
node.properties.class = `hljs language-${currentLanguage}`;
return node;
},
line(node) {
// Preserve existing line handling
node.properties.class = node.properties.class || "";
return node;
},
},
] as ShikiTransformer[],
});
if (!isMountedRef.current) {
return;
}
// Convert HAST to React elements using hast-util-to-jsx-runtime
// This approach eliminates XSS vulnerabilities by avoiding dangerouslySetInnerHTML
// while maintaining the exact same visual output and syntax highlighting
try {
const reactElement = toJsxRuntime(hast, {
Fragment,
jsx,
jsxs,
// Don't override components - let them render as-is to maintain exact output
});
if (isMountedRef.current) {
setHighlightedCode(reactElement);
}
} catch (error) {
console.error("[CodeBlock] Error converting HAST to JSX:", error);
if (isMountedRef.current) {
setHighlightedCode(fallback);
}
}
};
highlight().catch((e) => {
console.error(
"[CodeBlock] Syntax highlighting error:",
e,
"\nStack trace:",
e.stack,
);
if (isMountedRef.current) {
setHighlightedCode(fallback);
}
});
// Cleanup function - manage mounted state and clear all timeouts
return () => {
isMountedRef.current = false;
if (buttonPositionTimeoutRef.current) {
clearTimeout(buttonPositionTimeoutRef.current);
buttonPositionTimeoutRef.current = null;
}
if (collapseTimeout1Ref.current) {
clearTimeout(collapseTimeout1Ref.current);
collapseTimeout1Ref.current = null;
}
if (collapseTimeout2Ref.current) {
clearTimeout(collapseTimeout2Ref.current);
collapseTimeout2Ref.current = null;
}
};
}, [source, currentLanguage, collapsedHeight]);
// Check if content height exceeds collapsed height whenever content changes
// useEffect(() => {
// const codeBlock = codeBlockRef.current;
// if (codeBlock) {
// const actualHeight = codeBlock.scrollHeight;
// setShowCollapseButton(
// actualHeight >= WINDOW_SHADE_SETTINGS.collapsedHeight,
// );
// }
// }, [highlightedCode]);
// Ref to track if user was scrolled up *before* the source update
// potentially changes scrollHeight
const wasScrolledUpRef = useRef(false);
// Ref to track if outer container was near bottom
const outerContainerNearBottomRef = useRef(false);
// Effect to listen to scroll events and update the ref
useEffect(() => {
const preElement = preRef.current;
if (!preElement) {
return;
}
const handleScroll = () => {
const isAtBottom =
Math.abs(
preElement.scrollHeight -
preElement.scrollTop -
preElement.clientHeight,
) < SCROLL_SNAP_TOLERANCE;
wasScrolledUpRef.current = !isAtBottom;
};
preElement.addEventListener("scroll", handleScroll, { passive: true });
// Initial check in case it starts scrolled up
handleScroll();
return () => {
preElement.removeEventListener("scroll", handleScroll);
};
}, []); // Empty dependency array: runs once on mount
// Effect to track outer container scroll position
useEffect(() => {
const scrollContainer = document.querySelector(
'[data-virtuoso-scroller="true"]',
);
if (!scrollContainer) {
return;
}
const handleOuterScroll = () => {
const isAtBottom =
Math.abs(
scrollContainer.scrollHeight -
scrollContainer.scrollTop -
scrollContainer.clientHeight,
) < SCROLL_SNAP_TOLERANCE;
outerContainerNearBottomRef.current = isAtBottom;
};
scrollContainer.addEventListener("scroll", handleOuterScroll, {
passive: true,
});
// Initial check
handleOuterScroll();
return () => {
scrollContainer.removeEventListener("scroll", handleOuterScroll);
};
}, []);
// Store whether we should scroll after highlighting completes
const shouldScrollAfterHighlightRef = useRef(false);
// Check if we should scroll when source changes
useEffect(() => {
// Only set the flag if we're at the bottom when source changes
if (preRef.current && source && !wasScrolledUpRef.current) {
shouldScrollAfterHighlightRef.current = true;
} else {
shouldScrollAfterHighlightRef.current = false;
}
}, [source]);
const updateCodeBlockButtonPosition = useCallback((forceHide = false) => {
const codeBlock = codeBlockRef.current;
const copyWrapper = copyButtonWrapperRef.current;
if (!codeBlock) {
return;
}
const rectCodeBlock = codeBlock.getBoundingClientRect();
const scrollContainer = document.querySelector(
'[data-virtuoso-scroller="true"]',
);
if (!scrollContainer) {
return;
}
// Get wrapper height dynamically
let wrapperHeight;
if (copyWrapper) {
const copyRect = copyWrapper.getBoundingClientRect();
// If height is 0 due to styling, estimate from children
if (copyRect.height > 0) {
wrapperHeight = copyRect.height;
} else if (copyWrapper.children.length > 0) {
// Try to get height from the button inside
const firstChild = copyWrapper.children[0];
if (!firstChild) {
return;
}
const buttonRect = firstChild.getBoundingClientRect();
const buttonStyle = window.getComputedStyle(firstChild);
const buttonPadding =
parseInt(buttonStyle.getPropertyValue("padding-top") || "0", 10) +
parseInt(buttonStyle.getPropertyValue("padding-bottom") || "0", 10);
wrapperHeight = buttonRect.height + buttonPadding;
}
}
// If we still don't have a height, calculate from font size
if (!wrapperHeight) {
const fontSize = parseInt(
window.getComputedStyle(document.body).getPropertyValue("font-size"),
10,
);
wrapperHeight = fontSize * 2.5; // Approximate button height based on font size
}
const scrollRect = scrollContainer.getBoundingClientRect();
const copyButtonEdge = 48;
const isPartiallyVisible =
rectCodeBlock.top < scrollRect.bottom - copyButtonEdge &&
rectCodeBlock.bottom >= scrollRect.top + copyButtonEdge;
// Calculate margin from existing padding in the component
const computedStyle = window.getComputedStyle(codeBlock);
const paddingValue = parseInt(
computedStyle.getPropertyValue("padding") || "0",
10,
);
const margin =
paddingValue > 0
? paddingValue
: parseInt(computedStyle.getPropertyValue("padding-top") || "0", 10);
// Update visibility state and button interactivity
const isVisible = !forceHide && isPartiallyVisible;
codeBlock.setAttribute(
"data-partially-visible",
isPartiallyVisible ? "true" : "false",
);
codeBlock.style.setProperty(
"--copy-button-cursor",
isVisible ? "pointer" : "default",
);
codeBlock.style.setProperty(
"--copy-button-events",
isVisible ? "all" : "none",
);
codeBlock.style.setProperty(
"--copy-button-opacity",
isVisible ? "1" : "0",
);
if (isPartiallyVisible) {
// Keep button within code block bounds using dynamic measurements
const topPosition = Math.max(
scrollRect.top + margin,
Math.min(
rectCodeBlock.bottom - wrapperHeight - margin,
rectCodeBlock.top + margin,
),
);
const rightPosition = Math.max(
margin,
scrollRect.right - rectCodeBlock.right + margin,
);
codeBlock.style.setProperty("--copy-button-top", `${topPosition}px`);
codeBlock.style.setProperty(
"--copy-button-right",
`${rightPosition}px`,
);
}
}, []);
useEffect(() => {
const handleScroll = () => updateCodeBlockButtonPosition();
const handleResize = () => updateCodeBlockButtonPosition();
const scrollContainer = document.querySelector(
'[data-virtuoso-scroller="true"]',
);
if (scrollContainer) {
scrollContainer.addEventListener("scroll", handleScroll);
window.addEventListener("resize", handleResize);
updateCodeBlockButtonPosition();
}
return () => {
if (scrollContainer) {
scrollContainer.removeEventListener("scroll", handleScroll);
window.removeEventListener("resize", handleResize);
}
};
}, [updateCodeBlockButtonPosition]);
// Update button position and scroll when highlightedCode changes
useEffect(() => {
if (highlightedCode) {
// Clear any existing timeout before setting a new one
if (buttonPositionTimeoutRef.current) {
clearTimeout(buttonPositionTimeoutRef.current);
}
// Update button position
buttonPositionTimeoutRef.current = setTimeout(() => {
updateCodeBlockButtonPosition();
buttonPositionTimeoutRef.current = null; // Optional: Clear ref after execution
}, 0);
// Scroll to bottom if needed (immediately after Shiki updates)
if (shouldScrollAfterHighlightRef.current) {
// Scroll inner container
if (preRef.current) {
preRef.current.scrollTop = preRef.current.scrollHeight;
wasScrolledUpRef.current = false;
}
// Also scroll outer container if it was near bottom
if (outerContainerNearBottomRef.current) {
const scrollContainer = document.querySelector(
'[data-virtuoso-scroller="true"]',
);
if (scrollContainer) {
scrollContainer.scrollTop = scrollContainer.scrollHeight;
outerContainerNearBottomRef.current = true;
}
}
// Reset the flag
shouldScrollAfterHighlightRef.current = false;
}
}
// Cleanup function for this effect
return () => {
if (buttonPositionTimeoutRef.current) {
clearTimeout(buttonPositionTimeoutRef.current);
}
};
}, [highlightedCode, updateCodeBlockButtonPosition]);
// Advanced inertial scroll chaining
// This effect handles the transition between scrolling the code block and the outer container.
// When a user scrolls to the boundary of a code block (top or bottom), this implementation:
// 1. Detects the boundary condition
// 2. Applies inertial scrolling to the outer container for a smooth transition
// 3. Adds physics-based momentum for natural deceleration
// This creates a seamless experience where scrolling flows naturally between nested scrollable areas
useEffect(() => {
if (!preRef.current) {
return;
}
// Find the outer scrollable container
const getScrollContainer = () => {
return document.querySelector(
'[data-virtuoso-scroller="true"]',
) as HTMLElement;
};
// Inertial scrolling implementation
let velocity = 0;
let animationFrameId: number | null = null;
const FRICTION = 0.85; // Friction coefficient (lower = more friction)
const MIN_VELOCITY = 0.5; // Minimum velocity before stopping
// Animation function for inertial scrolling
const animate = () => {
const scrollContainer = getScrollContainer();
if (!scrollContainer) {
return;
}
// Apply current velocity
if (Math.abs(velocity) > MIN_VELOCITY) {
scrollContainer.scrollBy(0, velocity);
velocity *= FRICTION; // Apply friction
animationFrameId = requestAnimationFrame(animate);
} else {
velocity = 0;
animationFrameId = null;
}
};
// Wheel event handler with inertial scrolling
const handleWheel = (e: WheelEvent) => {
// If shift is pressed, let the browser handle default horizontal scrolling
if (e.shiftKey) {
return;
}
if (!preRef.current) {
return;
}
// Only handle wheel events if the inner container has a scrollbar,
// otherwise let the browser handle the default scrolling
const hasScrollbar =
preRef.current.scrollHeight > preRef.current.clientHeight;
// Pass through events if we don't need special handling
if (!hasScrollbar) {
return;
}
const scrollContainer = getScrollContainer();
if (!scrollContainer) {
return;
}
// Check if we're at the top or bottom of the inner container
const isAtVeryTop = preRef.current.scrollTop === 0;
const isAtVeryBottom =
Math.abs(
preRef.current.scrollHeight -
preRef.current.scrollTop -
preRef.current.clientHeight,
) < 1;
// Handle scrolling at container boundaries
if ((e.deltaY < 0 && isAtVeryTop) || (e.deltaY > 0 && isAtVeryBottom)) {
// Prevent default to stop inner container from handling
e.preventDefault();
const boost = 0.15;
velocity += e.deltaY * boost;
// Start animation if not already running
if (!animationFrameId) {
animationFrameId = requestAnimationFrame(animate);
}
}
};
// Add wheel event listener to inner container
const preElement = preRef.current;
preElement.addEventListener("wheel", handleWheel, { passive: false });
// Clean up
return () => {
preElement.removeEventListener("wheel", handleWheel);
// Cancel any ongoing animation
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
}
};
}, []);
// Track text selection state
const [isSelecting, setIsSelecting] = useState(false);
useEffect(() => {
if (!preRef.current) {
return;
}
const handleMouseDown = (e: MouseEvent) => {
// Only trigger if clicking the pre element directly
if (e.currentTarget === preRef.current) {
setIsSelecting(true);
}
};
const handleMouseUp = () => {
setIsSelecting(false);
};
const preElement = preRef.current;
preElement.addEventListener("mousedown", handleMouseDown);
document.addEventListener("mouseup", handleMouseUp);
return () => {
preElement.removeEventListener("mousedown", handleMouseDown);
document.removeEventListener("mouseup", handleMouseUp);
};
}, []);
const handleCopy = useCallback(
(e: React.MouseEvent) => {
e.stopPropagation();
// Check if code block is partially visible before allowing copy
const codeBlock = codeBlockRef.current;
if (
!codeBlock ||
codeBlock.getAttribute("data-partially-visible") !== "true"
) {
return;
}
const textToCopy = rawSource !== undefined ? rawSource : source || "";
if (textToCopy) {
copyWithFeedback(textToCopy, e);
}
},
[source, rawSource, copyWithFeedback],
);
if (source?.length === 0) {
return null;
}
return (
<div className={styles.codeBlockContainer} ref={codeBlockRef}>
<MemoizedStyledPre
preRef={preRef as React.RefObject<HTMLDivElement>}
preStyle={preStyle}
wordWrap={initialWordWrap}
// windowShade={windowShade}
// collapsedHeight={collapsedHeight}
highlightedCode={highlightedCode}
updateCodeBlockButtonPosition={updateCodeBlockButtonPosition}
/>
{!isSelecting && (
<div
className={styles.codeBlockButtonWrapper}
ref={copyButtonWrapperRef}
onMouseOver={() => updateCodeBlockButtonPosition()}
style={{ gap: 0 }}
>
{/* {showCollapseButton && (
<button
className={styles.button}
onClick={() => {
// Get the current code block element
const codeBlock = codeBlockRef.current; // Capture ref early
// Toggle window shade state
setWindowShade(!windowShade);
// Clear any previous timeouts
if (collapseTimeout1Ref.current)
clearTimeout(collapseTimeout1Ref.current);
if (collapseTimeout2Ref.current)
clearTimeout(collapseTimeout2Ref.current);
// After UI updates, ensure code block is visible and update button position
collapseTimeout1Ref.current = setTimeout(
() => {
if (codeBlock) {
// Check if codeBlock element still exists
codeBlock.scrollIntoView({
behavior: "smooth",
block: "nearest",
});
// Wait for scroll to complete before updating button position
collapseTimeout2Ref.current = setTimeout(() => {
// updateCodeBlockButtonPosition itself should also check for refs if needed
updateCodeBlockButtonPosition();
collapseTimeout2Ref.current = null;
}, 50);
}
collapseTimeout1Ref.current = null;
},
WINDOW_SHADE_SETTINGS.transitionDelayS * 1000 + 50,
);
}}
>
{windowShade ? (
<span className="codicon codicon-chevron-down"></span>
) : (
<span className="codicon codicon-chevron-up"></span>
)}
</button>
)} */}
{/* <button
className={styles.button}
onClick={() => setWordWrap(!wordWrap)}
>
{wordWrap ? (
<span className="codicon codicon-word-wrap"></span>
) : (
<span className="codicon codicon-layout-panel-justify"></span>
)}
</button> */}
<button className={styles.button} onClick={handleCopy}>
{showCopyFeedback ? (
<span className="codicon codicon-check"></span>
) : (
<span className="codicon codicon-copy"></span>
)}
</button>
</div>
)}
</div>
);
},
);
// Memoized content component to prevent unnecessary re-renders of highlighted code
const MemoizedCodeContent = memo(
({ children }: { children: React.ReactNode }) => <>{children}</>,
);
// Memoized StyledPre component
const MemoizedStyledPre = memo(
({
preRef,
preStyle,
wordWrap,
// windowShade,
// collapsedHeight,
highlightedCode,
updateCodeBlockButtonPosition,
}: {
preRef: React.RefObject<HTMLDivElement>;
preStyle: React.CSSProperties | undefined;
wordWrap: boolean;
// windowShade: boolean;
// collapsedHeight: number | undefined;
highlightedCode: React.ReactNode;
updateCodeBlockButtonPosition: (forceHide?: boolean) => void;
}) => (
<div
ref={preRef}
className={styles.preWrapper}
style={{
...preStyle,
whiteSpace: !wordWrap ? "pre" : "pre-wrap",
wordBreak: !wordWrap ? "normal" : "normal",
overflowWrap: !wordWrap ? "normal" : "break-word",
// maxHeight: windowShade
// ? `${collapsedHeight || WINDOW_SHADE_SETTINGS.collapsedHeight}px`
// : "none",
}}
onMouseDown={() => updateCodeBlockButtonPosition(true)}
onMouseUp={() => updateCodeBlockButtonPosition(false)}
>
<MemoizedCodeContent>{highlightedCode}</MemoizedCodeContent>
</div>
),
);
export default CodeBlock;

View file

@ -0,0 +1,57 @@
.container {
border: 1px solid var(--vscode-editorGroup-border);
border-radius: 4px;
overflow: hidden;
margin: 0.5rem 0;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
background-color: var(--vscode-editor-background);
color: var(--vscode-editor-foreground);
width: 100%;
padding: 6px 8px;
border: none;
cursor: pointer;
font-size: 13px;
text-align: left;
outline: none;
}
.fileName {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.icon {
transition: transform 0.25s ease;
}
.open {
transform: rotate(90deg);
}
.contentWrapper {
max-height: 0;
overflow: hidden;
transition: max-height 0.1s ease;
}
.contentWrapper div {
border: none !important;
}
.expanded {
max-height: 100%;
}
.content {
padding: 0.4rem 0.2rem 0 0.2rem;
}
.contentWrapper:not(.expanded) {
display: none;
}

View file

@ -0,0 +1,46 @@
import { useRef, useState } from "react";
import styles from "./collapsibleFile.module.css";
import Chip from "../chip/chip";
type CollapsibleFileProps = {
fileName: string | undefined;
children: React.ReactNode;
};
export default function CollapsibleFile({
fileName,
children,
}: CollapsibleFileProps) {
const [isOpen, setIsOpen] = useState(false);
const childRef = useRef(children);
return (
<div className={styles.container}>
<button
className={styles.header}
onClick={(e) => {
e.preventDefault();
setIsOpen((prev) => !prev);
}}
>
{fileName && (
<Chip>
<span className="codicon codicon-python"></span>
<span>{fileName}</span>
</Chip>
)}
<span
className={`codicon codicon-chevron-right ${styles.icon} ${
isOpen ? styles.open : ""
}`}
/>
</button>
<div
className={`${styles.contentWrapper} ${isOpen ? styles.expanded : ""}`}
>
<div className={styles.content}>{childRef.current}</div>
</div>
</div>
);
}

View file

@ -74,7 +74,9 @@ const CurrentFileFunctions = () => {
className={styles.sectionHeader}
onClick={(e) => {
e.stopPropagation();
if (functions.length === 0) {return;}
if (functions.length === 0) {
return;
}
setExpanded((prev) => !prev);
}}
>

View file

@ -2,8 +2,8 @@ const CodeflashLogo = ({
width,
height,
}: {
width: number;
height: number;
width: string;
height: string;
}) => {
return (
<svg
@ -51,7 +51,7 @@ const CodeflashLogo = ({
/>
<path
d="M25.0166 51.8125L0.136719 51.8388L27.276 9.06742H52.1822L25.0166 51.8125Z"
fill="#FFC143"
fill="var(--vscode-foreground)" // this should be yellow part
/>
<path
d="M88.4645 21.4679H53.2595L61.1412 9.06735H96.3462L88.4645 21.4679Z"
@ -67,7 +67,7 @@ const CodeflashLogo = ({
/>
<path
d="M25.9884 89.4869H1.10853L36.2609 34.1311H61.1408L25.9884 89.4869Z"
fill="#FFC143"
fill="var(--vscode-foreground)" // this should be yellow part
/>
</svg>
);

View file

@ -0,0 +1,131 @@
.markdown code:not(pre > code) {
font-family: var(--vscode-editor-font-family, monospace);
filter: saturation(110%) brightness(95%);
color: var(--vscode-textPreformat-foreground) !important;
background-color: transparent;
padding: 0px 2px;
white-space: pre-line;
word-break: break-word;
overflow-wrap: anywhere;
}
/* Target only Dark High Contrast theme using the data attribute VS Code adds to the body */
body[data-vscode-theme-kind="vscode-high-contrast"] & code:not(pre > code) {
color: var(
--vscode-editorInlayHint-foreground,
var(
--vscode-symbolIcon-stringForeground,
var(--vscode-charts-orange, #e9a700)
)
);
}
/* KaTeX styling */
.markdown .katex {
font-size: 1.1em;
color: var(--vscode-editor-foreground);
font-family: KaTeX_Main, "Times New Roman", serif;
line-height: 1.2;
white-space: normal;
text-indent: 0;
}
.markdown .katex-display {
display: block;
/* margin: 1em 0; */
text-align: center;
padding: 0.5em;
overflow-x: auto;
overflow-y: hidden;
background-color: var(--vscode-textCodeBlock-background);
border-radius: 3px;
}
.markdown .katex-error {
color: var(--vscode-errorForeground);
}
.markdown {
font-family:
var(--vscode-font-family),
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
Oxygen,
Ubuntu,
Cantarell,
"Open Sans",
"Helvetica Neue",
sans-serif;
font-size: var(--vscode-font-size, 13px);
}
.markdown p,
.markdown li,
.markdown ol,
.markdown ul {
line-height: 2 !important;
}
.markdown ol,
.markdown ul {
padding-left: 2.5em;
margin-left: 0;
}
.markdown ol {
list-style-type: decimal;
}
.markdown ul {
list-style-type: disc;
}
/* Nested list styles */
.markdown ul ul {
list-style-type: circle;
}
.markdown ul ul ul {
list-style-type: square;
}
.markdown ol ol {
list-style-type: lower-alpha;
}
.markdown ol ol ol {
list-style-type: lower-roman;
}
.markdown p {
white-space: pre-wrap;
margin: 0.5em 0;
}
/* Prevent layout shifts during streaming */
.markdown pre {
min-height: 3em;
transition: height 0.2s ease-out;
}
/* Code block container styling */
.markdown div:has(> pre) {
position: relative;
contain: layout style;
}
.markdown a {
color: var(--vscode-textLink-foreground);
text-decoration-line: underline;
text-decoration-style: dotted;
text-decoration-color: var(--vscode-textLink-foreground);
&:hover {
color: var(--vscode-textLink-activeForeground);
text-decoration-style: solid;
text-decoration-color: var(--vscode-textLink-activeForeground);
}
}

View file

@ -0,0 +1,134 @@
import React, { memo, useMemo } from "react";
import ReactMarkdown from "react-markdown";
import styles from "./markdown.module.css";
import CodeBlock from "../codeBlock/codeBlock";
import remarkGfm from "remark-gfm";
interface MarkdownBlockProps {
markdown?: string;
}
const MarkdownBlock = memo(({ markdown }: MarkdownBlockProps) => {
const components = useMemo(
() => ({
table: ({ children, ...props }: any) => {
return (
<div className="table-wrapper">
<table {...props}>{children}</table>
</div>
);
},
a: ({ href, children, ...props }: any) => {
const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
// Only process file:// protocol or local file paths
const isLocalPath =
href?.startsWith("file://") ||
href?.startsWith("/") ||
!href?.includes("://");
if (!isLocalPath) {
return;
}
e.preventDefault();
// Handle absolute vs project-relative paths
let filePath = href.replace("file://", "");
// Extract line number if present
// const match = filePath.match(/(.*):(\d+)(-\d+)?$/)
// let values = undefined
// if (match) {
// filePath = match[1]
// values = { line: parseInt(match[2]) }
// }
// Add ./ prefix if needed
if (!filePath.startsWith("/") && !filePath.startsWith("./")) {
filePath = "./" + filePath;
}
// vscode.postMessage({
// type: "openFile",
// text: filePath,
// values,
// })
};
return (
<a {...props} href={href} onClick={handleClick}>
{children}
</a>
);
},
pre: ({ children }: any) => {
// The structure from react-markdown v9 is: pre > code > text
const codeEl = children as React.ReactElement;
if (!codeEl || !codeEl.props) {
return <pre>{children}</pre>;
}
const { className = "", children: codeChildren } = codeEl.props as {
className?: string;
children?: React.ReactNode;
};
// Get the actual code text
let codeString = "";
if (typeof codeChildren === "string") {
codeString = codeChildren;
} else if (Array.isArray(codeChildren)) {
codeString = codeChildren
.filter((child) => typeof child === "string")
.join("");
}
// Extract language from className
const match = /language-(\w+)/.exec(className);
const language = match ? match[1] : "text";
// Wrap CodeBlock in a div to ensure proper separation
return (
<div style={{ margin: "1em 0" }}>
<CodeBlock source={codeString} language={language!} />
</div>
);
},
code: ({ children, className, ...props }: any) => {
// This handles inline code
return (
<code className={className} {...props}>
{children}
</code>
);
},
}),
[],
);
return (
<div className={styles.markdown}>
<ReactMarkdown
remarkPlugins={[
remarkGfm,
// () => {
// return (tree: any) => {
// visit(tree, "code", (node: any) => {
// if (!node.lang) {
// node.lang = "text"
// } else if (node.lang.includes(".")) {
// node.lang = node.lang.split(".").slice(-1)[0]
// }
// })
// }
// },
]}
components={components}
>
{markdown || ""}
</ReactMarkdown>
</div>
);
});
export default MarkdownBlock;

View file

@ -7,24 +7,24 @@ import { vscode } from "../utils/vscode";
import { useEffect, useMemo, useRef, useState } from "react";
import styles from "../styles/optimizationQueue.module.css";
import { canDeleteTaskInQueue, canRetryTaskInQueue } from "@codeflash/shared";
import { useStore } from "../store/root";
const OptimizationQueue = ({
queueTasks,
}: {
queueTasks: TQueueTaskItem[];
}) => {
const functions = useStore((state) => state.functionsInCurrentFile);
// const functions = useStore((state) => state.functionsInCurrentFile);
const [optionsOpen, setOptionsOpen] = useState(false);
const optionsButtonRef = useRef<HTMLButtonElement | null>(null);
const dropdownRef = useRef<HTMLDivElement | null>(null);
const sortedTasks = useMemo(() => {
const statusPriority: Record<QueueTaskItemStatus, number> = {
optimizing: 1,
completed: 2,
queued: 3,
failed: 4,
skipped: 5,
initializing: 1,
optimizing: 2,
completed: 3,
queued: 4,
failed: 5,
skipped: 6,
};
return queueTasks.slice().sort((a, b) => {
@ -111,10 +111,8 @@ const OptimizationQueue = ({
<span className="codicon codicon-rocket"></span>
<span>No optimizations yet</span>
<small className={styles.queueEmptyHint}>
Add functions to the queue by clicking the <b>optimize</b>{" "}
CodeLens above a function{" "}
{functions.length > 0 &&
"or by selecting it from the list below."}
Add functions to the queue by clicking the small{" "}
<b>optimize</b> button above a function
</small>
</div>
</li>
@ -187,8 +185,9 @@ const TaskStatus = ({ task }: { task: TQueueTaskItem }) => {
const statusName = alternativeStatusNames[task.status] || task.status;
switch (task.status) {
case "optimizing":
case "initializing":
return (
<span className={styles.queueTaskStatus + " " + styles.optimizing}>
<span className={styles.queueTaskStatus + " " + styles[task.status]}>
<span className="codicon codicon-sync spin" />
<span>{statusName}</span>
</span>

View file

@ -0,0 +1,31 @@
import styles from "../styles/tabs.module.css";
import { useStore } from "../store/root";
const Tabs = () => {
const setActiveTab = useStore((state) => state.setActiveTab);
const activeTab = useStore((state) => state.activeTab);
const tasksCount = useStore((state) => state.queueTasks.length);
return (
<div className={styles.tabs}>
<button
className={`${styles.tab} ${activeTab === "optimization" ? styles.active : ""}`}
onClick={() => setActiveTab("optimization")}
>
Optimization
</button>
<span className={styles.splitter}></span>
<button
className={`${styles.tab} ${activeTab === "tasks" ? styles.active : ""}`}
onClick={() => setActiveTab("tasks")}
>
<span className={styles.tabText}>
Tasks
{tasksCount > 0 && <span className={styles.badge}>{tasksCount}</span>}
</span>
</button>
</div>
);
};
export default Tabs;

View file

@ -0,0 +1,147 @@
.messagesContainer {
display: flex;
flex-grow: 1;
flex-direction: column;
gap: 0.25rem;
padding: 1rem 0; /* remove the horizontal padding so that it doesn't affect the scrollbar, and add the padding to each child instead*/
height: 100%;
overflow-y: auto;
scroll-behavior: smooth;
}
.logEntry {
display: flex;
align-items: center;
gap: 0.5rem;
opacity: 1;
transform: translateY(0);
transition: all 0.3s ease-out;
animation: fadeIn 0.5s forwards;
padding: 0 0.5rem;
}
@keyframes fadeIn {
0% {
opacity: 0;
}
50% {
opacity: 0.7;
}
100% {
opacity: 1;
}
}
.logContent {
flex: 1 !important;
width: 100%;
border-radius: 12px;
position: relative;
transition: all 0.2s ease;
}
.logContent:hover {
border-color: var(--vscode-focusBorder, rgba(0, 122, 255, 0.4));
}
.logContent :global(p) {
margin: 0.5rem 0;
font-size: 0.875rem;
line-height: 1.5;
}
.textShimmer {
position: relative;
display: inline-block;
color: transparent !important;
background-clip: text;
-webkit-background-clip: text;
/* gradient setup */
background-size:
250% 100%,
auto;
background-repeat: no-repeat, padding-box;
--base-color: var(--vscode-editor-foreground);
--base-gradient-color: color-mix(
in srgb,
var(--vscode-editor-background),
var(--vscode-editor-foreground) 40%
);
background-image: linear-gradient(
90deg,
var(--base-color) 0%,
var(--base-gradient-color) 30%,
var(--base-color) 60%,
var(--base-gradient-color) 90%,
var(--base-color) 100%
);
animation: shimmer 4s linear infinite;
}
/* shimmer animation */
@keyframes shimmer {
from {
background-position: 200% center;
}
to {
background-position: -200% center;
}
}
.errorView {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.5rem 0.3rem;
background: var(--vscode-editorWidget-background);
margin: 0 auto;
border-radius: 12px;
}
.errorView span {
color: var(--vscode-descriptionForeground);
}
.queueTaskPatch {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 12px;
font-weight: 500;
padding: 4px 10px;
border: 0;
border-radius: 6px;
background: var(--vscode-editorWidget-background);
color: var(--vscode-editorWidget-foreground);
cursor: pointer;
transition:
background 0.15s ease,
color 0.15s ease,
border 0.15s ease;
}
.ghostActionButton {
display: flex;
align-items: center;
gap: 0.2rem;
font-size: 12px;
font-weight: bold;
border-radius: 6px;
border: 1px solid var(--vscode-input-border);
padding: 0.2rem 0.4rem;
background: transparent;
color: var(--vscode-foreground);
cursor: pointer;
transition:
background 0.15s ease,
color 0.15s ease,
border-color 0.15s ease;
}
.ghostActionButton:hover {
background: var(--vscode-list-hoverBackground);
color: var(--vscode-foreground);
}

View file

@ -0,0 +1,222 @@
import type {
LogEntry,
QueueTaskItem,
ViewPatchMessage,
} from "@codeflash/types";
import styles from "./taskLogging.module.css";
import MarkdownBlock from "./markdown/markdown";
import CollapsibleFile from "./collapsible/collapsibleFile";
import CodeBlock from "./codeBlock/codeBlock";
import { memo, useEffect, useRef } from "react";
import { vscode } from "../utils/vscode";
import {
Virtuoso,
type FollowOutputScalarType,
type VirtuosoHandle,
} from "react-virtuoso";
import { useMemoFunc } from "../utils/useMemoFunc";
import { isTaskRunning } from "@codeflash/shared";
const MemoMarkdownBlock = memo(MarkdownBlock);
const MemoCollapsibleFile = memo(CollapsibleFile);
const footerContainerStyles = {
paddingBottom: "1rem",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
padding: "0 1rem",
margin: "1rem 0",
background: "var(--vscode-editor-background)",
color: "var(--vscode-editor-foreground)",
borderRadius: "7px",
};
const LogItem = memo(
({
log,
isLast,
isStillRunning,
functionName,
filePath,
}: {
log: LogEntry;
isLast: boolean;
isStillRunning: boolean;
functionName: string;
filePath: string | undefined;
}) => {
const isLoading = isStillRunning && log.takes_time && isLast;
const longTaskFinished =
(log.takes_time && !isLast) || (log.takes_time && !isStillRunning);
const contentClassName =
styles.logContent + " " + (isLoading ? styles.textShimmer : "");
switch (log.type) {
case "text":
return (
<div className={styles.logEntry}>
{longTaskFinished && (
<span className="codicon codicon-pass-filled" />
)}
<div className={contentClassName}>
<MemoMarkdownBlock markdown={log.text!} />
</div>
</div>
);
case "code":
return (
<div className={styles.logEntry}>
<div className={contentClassName}>
{log.collapsed ? (
<MemoCollapsibleFile fileName={log.file_name}>
<CodeBlock source={log.code!} language="python" />
</MemoCollapsibleFile>
) : (
<CodeBlock source={log.code!} language="python" />
)}
</div>
</div>
);
case "markdown":
return (
<div className={styles.logEntry}>
{longTaskFinished && (
<span className="codicon codicon-pass-filled" />
)}
<div className={contentClassName}>
<MemoMarkdownBlock markdown={log.markdown!} />
</div>
</div>
);
case "error":
return (
<div className={styles.logEntry}>
<div className={contentClassName} style={footerContainerStyles}>
<MemoMarkdownBlock
markdown={`**No optimizations found.**\n${log.text!}`}
/>
<button
className={styles.ghostActionButton}
title="Retry"
onClick={() => {
vscode.postMessage({
type: "optimizeFunction",
payload: {
functionName,
filePath,
},
});
}}
>
<span>Retry optimization</span>
<span className="codicon codicon-refresh"></span>
</button>
</div>
</div>
);
default:
return (
<div className={styles.logEntry}>
<div className={contentClassName}>
<MemoMarkdownBlock
markdown={`| Not supported log type: ${log.type}`}
/>
</div>
</div>
);
}
},
);
const TaskLogging = ({ task }: { task: QueueTaskItem }) => {
const listRef = useRef<VirtuosoHandle>(null);
const scrollerRef = useRef<HTMLElement>(null);
const followOutput = useMemoFunc(
(isAtBottom: boolean): FollowOutputScalarType => {
if (isAtBottom) {
setTimeout(() => {
listRef.current?.autoscrollToBottom();
}, 20);
}
return isAtBottom ? "smooth" : false;
},
);
useEffect(() => {
listRef.current?.scrollToIndex({ index: task.logs.length - 1 });
}, []);
return (
<div className={styles.messagesContainer}>
<Virtuoso
ref={listRef}
style={{ height: "100%" }}
data={task.logs}
computeItemKey={(_, log) => `${log.id}-${log.collapsed ? "c" : "o"}`}
itemContent={(index) => (
<LogItem
log={task.logs[index]!}
isLast={index === task.logs.length - 1}
isStillRunning={isTaskRunning(task)}
functionName={task.functionName}
filePath={task.filepath}
/>
)}
scrollerRef={(ref) => (scrollerRef.current = ref as HTMLElement)}
// initialTopMostItemIndex={task.logs.length - 1}
totalCount={task.logs.length}
overscan={{
main: 1000,
reverse: 1000,
}}
// atBottomStateChange={atBottomStateChange} // TODO: show scroll to bottom button
followOutput={followOutput}
defaultItemHeight={300}
atTopThreshold={100}
atBottomThreshold={40}
useWindowScroll={false}
/>
{task.status === "completed" && (
<div className={styles.logEntry}>
<div className={styles.logContent}>
<div
style={{
...footerContainerStyles,
color: "var(--vscode-foreground)",
background: "rgba(34, 134, 58, 0.15)",
}}
>
<MemoMarkdownBlock
markdown={`✅ Optimization completed successfully.`}
/>
<button
className={styles.queueTaskPatch}
onClick={() => {
const msg: ViewPatchMessage = {
type: "viewPatch",
payload: {
id: task.id,
functionName: task.functionName,
patchFile: task.patch_file!,
patchId: task.patch_id!,
explanation: task.explanation || "",
speedupStr: task.speedupStr || "",
},
};
vscode.postMessage(msg);
}}
>
<span className="codicon codicon-eye"></span>
<span>View Optimization</span>
</button>
</div>
</div>
</div>
)}
</div>
);
};
export default TaskLogging;

View file

@ -0,0 +1,24 @@
import type { LogEntry } from "@codeflash/types";
import type { State } from "../root";
import { isTaskRunning } from "@codeflash/shared";
// adds a new log entry to the active task
export const handleAddLog = (state: State, log: LogEntry): State => {
const activeTask = state.queueTasks.find(isTaskRunning);
if (!activeTask) {
return state;
}
return {
...state,
queueTasks: state.queueTasks.map((t) => {
if (t.id === activeTask.id) {
return {
...t,
logs: [...t.logs, log],
};
}
return t;
}),
};
};

View file

@ -13,8 +13,13 @@ export const handleRestoreStateFromCache = (
// currentStatusMessage,
// currentFunctionName,
// steps,
focusedTaskId,
queueTasks,
} = payload as { running?: boolean; queueTasks?: QueueTaskItem[] };
} = payload as {
running?: boolean;
queueTasks?: QueueTaskItem[];
focusedTaskId: string;
};
console.log(
`received restore state from cache message: ${message.type}, ${JSON.stringify(payload)}`,
);
@ -22,5 +27,6 @@ export const handleRestoreStateFromCache = (
...oldState,
optimizationRunning: running ?? false,
queueTasks: queueTasks ?? [],
focusedTaskId,
};
};

View file

@ -1,11 +1,25 @@
import type { UpdateStateMessage } from "@codeflash/types";
import type { State } from "../root";
const LEGACY_STATUS: UpdateStateMessage["payload"]["status"][] = [
"connecting",
"idle",
"noPythonFile",
"success",
"error",
"serverError",
"empty",
];
export const handleUpdateState = (
oldState: State,
message: UpdateStateMessage,
): State => {
const payload = message.payload;
if (LEGACY_STATUS.includes(payload.status)) {
return oldState;
}
console.log(
`received update state message: ${message.type}, ${JSON.stringify(payload)}`,
);

View file

@ -1,7 +1,10 @@
import type {
FileInWorkspace,
LogEntry,
QueueTaskItem,
RestoreStateMessage,
SidebarStatus,
SidebarTab,
UpdateQueueTasksMessage,
UpdateStateMessage,
} from "@codeflash/types";
@ -9,13 +12,19 @@ import { create } from "zustand";
import { handleUpdateState } from "./actions/updateSidebarState";
import { handleUpdateQueueTasks } from "./actions/updateQueueTasks";
import { handleRestoreStateFromCache } from "./actions/restoreStateFromMemory";
import { handleAddLog } from "./actions/addLog";
export type State = {
status: SidebarStatus;
activeTab: SidebarTab;
optimizationRunning: boolean;
queueTasks: QueueTaskItem[];
functionsInCurrentFile: string[];
focusedTaskId: string;
currentFilePath: string;
filesInWorkspace: FileInWorkspace[];
functionsInCurrentContextFile: { file: string; functions: string[] };
isValidatingApiKey: boolean;
};
export type Action = {
@ -24,18 +33,34 @@ export type Action = {
updateQueueTasks: (msg: UpdateQueueTasksMessage) => void;
restoreStateFromCache: (msg: RestoreStateMessage) => void;
apiKeyEnterValid: () => void;
updateFilesInWorkspace: (files: FileInWorkspace[]) => void;
updateFunctionsInCurrentContextFile: (
file: string,
functions: string[],
) => void;
changeTaskFocus: (id: string) => void;
setActiveTab: (tab: SidebarTab) => void;
setApiKeyLoadingState: (loading: boolean) => void;
addLog: (log: LogEntry) => void;
};
const initialState: State = {
status: "idle",
activeTab: "optimization",
optimizationRunning: false,
queueTasks: [],
functionsInCurrentFile: [],
focusedTaskId: "",
currentFilePath: "",
filesInWorkspace: [],
functionsInCurrentContextFile: { file: "", functions: [] },
isValidatingApiKey: false,
};
export const useStore = create<State & Action>((_set) => ({
...initialState,
setActiveTab: (tab: SidebarTab) =>
_set((oldState) => ({ ...oldState, activeTab: tab })),
updateSidebarState: (message: UpdateStateMessage) =>
_set((oldState) => handleUpdateState(oldState, message)),
updateQueueTasks: (message: UpdateQueueTasksMessage) =>
@ -43,4 +68,19 @@ export const useStore = create<State & Action>((_set) => ({
restoreStateFromCache: (message: RestoreStateMessage) =>
_set((oldState) => handleRestoreStateFromCache(oldState, message)),
apiKeyEnterValid: () => _set((oldState) => ({ ...oldState, status: "idle" })),
updateFilesInWorkspace: (files: FileInWorkspace[]) =>
_set((oldState) => ({ ...oldState, filesInWorkspace: files })),
updateFunctionsInCurrentContextFile: (file: string, functions: string[]) =>
_set((oldState) => ({
...oldState,
functionsInCurrentContextFile: {
file,
functions,
},
})),
changeTaskFocus: (id: string) =>
_set((oldState) => ({ ...oldState, focusedTaskId: id })),
addLog: (log: LogEntry) => _set((oldState) => handleAddLog(oldState, log)),
setApiKeyLoadingState: (loading: boolean) =>
_set((oldState) => ({ ...oldState, isValidatingApiKey: loading })),
}));

View file

@ -0,0 +1,72 @@
.chatViewBanner {
height: 100%;
display: flex;
justify-content: center;
padding: 2.5rem 2rem;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
.logoWrapper {
text-align: center;
max-width: 600px;
width: 100%;
padding: 1.5rem 0;
animation: fadeIn 0.6s cubic-bezier(0.16, 1, 0.3, 1);
}
.logoContainer {
margin: 0 auto 1.25rem;
max-width: 280px;
}
.description {
margin: 1.75rem 0 0;
font-size: 1rem;
font-weight: 400;
color: var(--vscode-descriptionForeground, #8b949e);
line-height: 1.7;
opacity: 0.95;
margin-left: auto;
margin-right: auto;
transition: color 0.2s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(12px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Responsive adjustments */
@media (max-width: 768px) {
.chatViewBanner {
margin: 0.75rem;
padding: 2rem 1.25rem;
border-radius: 10px;
}
.logoWrapper {
padding: 1rem 0;
}
.logoContainer {
max-width: 240px;
}
}

View file

@ -1,8 +1,8 @@
/* === Wrapper === */
.sectionTitleIcon {
color: var(--vscode-icon-foreground);
font-size: 16px;
margin-right: 8px;
color: var(--vscode-icon-foreground);
font-size: 16px;
margin-right: 8px;
}
.badge {
display: inline-flex;
@ -20,7 +20,6 @@
user-select: none;
}
.functionsWrapper {
margin: 8px;
border-radius: 4px;
@ -37,7 +36,8 @@
justify-content: space-between;
padding: 8px 12px;
background: var(--vscode-editor-background); /* matches VS Code panels */
border-bottom: 1px solid var(--vscode-panel-border, var(--vscode-editorGroup-border));
border-bottom: 1px solid
var(--vscode-panel-border, var(--vscode-editorGroup-border));
cursor: pointer;
transition: background-color 0.2s ease;
}
@ -61,230 +61,243 @@
opacity: 0.8;
}
.functionsWrapperDisabled {
opacity: 0.7;
pointer-events: none;
opacity: 0.7;
pointer-events: none;
}
/* === Function list === */
.functionList {
list-style: none;
transition: max-height 0.3s ease, opacity 0.2s ease;
overflow: hidden;
}
.functionList.expanded {
max-height: 300px;
opacity: 1;
overflow-y: auto;
margin: 8px 0;
padding: 0 16px;
}
.functionList.collapsed {
max-height: 0;
opacity: 0;
overflow: hidden;
padding: 0;
margin: 0;
}
/* === Class Section === */
.classSection {
margin-bottom: 10px;
border-radius: 6px;
overflow: hidden;
transition: all 0.2s ease;
}
.classHeader {
display: flex;
align-items: center;
padding: 8px 12px;
border-radius: 6px;
background-color: color-mix(in srgb, var(--vscode-list-inactiveSelectionBackground) 60%, transparent);
cursor: pointer;
transition: all 0.25s ease;
gap: 8px;
position: relative;
}
.classHeader:hover {
background-color: var(--vscode-list-hoverBackground);
}
.classToggle {
transition: transform 0.3s ease;
}
.expanded .classToggle {
transform: rotate(90deg);
}
.classIcon {
color: var(--vscode-descriptionForeground);
opacity: 0.5;
}
.className {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-weight: 600;
}
.methodCount {
font-size: 0.8em;
}
/* === Methods List === */
.methodsList {
list-style: none;
padding: 0 0 0 32px;
margin: 0;
overflow: hidden;
transition: all 0.3s ease;
position: relative;
}
.methodsList::before {
content: "";
position: absolute;
top: 0;
bottom: 0;
left: 12px;
width: 1px;
background-color: var(--vscode-editorIndentGuide-background);
}
/* === Function Item === */
.functionItem {
display: flex;
align-items: center;
justify-content: space-between;
padding: 4px 8px;
margin: 3px 0;
border-radius: 4px;
background-color: var(--vscode-editor-background);
border: 1px solid transparent;
transition: all 0.25s ease;
position: relative;
overflow: hidden;
}
.functionItem:hover {
background-color: var(--vscode-list-hoverBackground);
}
.functionItemHighlighted {
background-color: var(--vscode-list-activeSelectionBackground);
color: var(--vscode-list-activeSelectionForeground);
border-color: var(--vscode-focusBorder, var(--vscode-list-activeSelectionBackground));
}
.functionItemContent {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
overflow: hidden;
cursor: pointer;
min-width: 0;
border-radius: 4px;
padding: 2px;
transition: all 0.2s ease;
}
.functionIcon {
flex-shrink: 0;
opacity: 0.4;
font-size: 14px;
transition: opacity 0.2s ease;
}
.functionItem:hover .functionIcon {
opacity: 0.6;
}
.methodItem .functionIcon,
.standaloneFunctionItem .functionIcon {
color: var(--vscode-descriptionForeground);
}
.functionName {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex-grow: 1;
min-width: 50px;
position: relative;
}
/* === Status States === */
.functionItemOptimizing {
opacity: 0.8;
background-color: var(--vscode-editor-inactiveSelectionBackground, rgba(128, 128, 128, 0.1));
}
.functionItemOptimizationHighlight {
background-color: var(--vscode-activityBarBadge-background);
color: var(--vscode-activityBarBadge-foreground);
border: 2px solid var(--vscode-activityBarBadge-background);
/* === Function list === */
.functionList {
list-style: none;
transition:
max-height 0.3s ease,
opacity 0.2s ease;
overflow: hidden;
}
.functionList.expanded {
max-height: 300px;
opacity: 1;
overflow-y: auto;
margin: 8px 0;
padding: 0 16px;
}
.functionList.collapsed {
max-height: 0;
opacity: 0;
overflow: hidden;
padding: 0;
margin: 0;
}
/* === Class Section === */
.classSection {
margin-bottom: 10px;
border-radius: 6px;
overflow: hidden;
transition: all 0.2s ease;
}
.classHeader {
display: flex;
align-items: center;
padding: 8px 12px;
border-radius: 6px;
background-color: color-mix(
in srgb,
var(--vscode-list-inactiveSelectionBackground) 60%,
transparent
);
cursor: pointer;
transition: all 0.25s ease;
gap: 8px;
position: relative;
}
.classHeader:hover {
background-color: var(--vscode-list-hoverBackground);
}
.classToggle {
transition: transform 0.3s ease;
}
.expanded .classToggle {
transform: rotate(90deg);
}
.classIcon {
color: var(--vscode-descriptionForeground);
opacity: 0.5;
}
.className {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-weight: 600;
}
.methodCount {
font-size: 0.8em;
}
/* === Methods List === */
.methodsList {
list-style: none;
padding: 0 0 0 32px;
margin: 0;
overflow: hidden;
transition: all 0.3s ease;
position: relative;
}
.methodsList::before {
content: "";
position: absolute;
top: 0;
bottom: 0;
left: 12px;
width: 1px;
background-color: var(--vscode-editorIndentGuide-background);
}
/* === Function Item === */
.functionItem {
display: flex;
align-items: center;
justify-content: space-between;
padding: 4px 8px;
margin: 3px 0;
border-radius: 4px;
background-color: var(--vscode-editor-background);
border: 1px solid transparent;
transition: all 0.25s ease;
position: relative;
overflow: hidden;
}
.functionItem:hover {
background-color: var(--vscode-list-hoverBackground);
}
.functionItemHighlighted {
background-color: var(--vscode-list-activeSelectionBackground);
color: var(--vscode-list-activeSelectionForeground);
border-color: var(
--vscode-focusBorder,
var(--vscode-list-activeSelectionBackground)
);
}
.functionItemContent {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
overflow: hidden;
cursor: pointer;
min-width: 0;
border-radius: 4px;
padding: 2px;
transition: all 0.2s ease;
}
.functionIcon {
flex-shrink: 0;
opacity: 0.4;
font-size: 14px;
transition: opacity 0.2s ease;
}
.functionItem:hover .functionIcon {
opacity: 0.6;
}
.methodItem .functionIcon,
.standaloneFunctionItem .functionIcon {
color: var(--vscode-descriptionForeground);
}
.functionName {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex-grow: 1;
min-width: 50px;
position: relative;
}
/* === Status States === */
.functionItemOptimizing {
opacity: 0.8;
background-color: var(
--vscode-editor-inactiveSelectionBackground,
rgba(128, 128, 128, 0.1)
);
}
.functionItemOptimizationHighlight {
background-color: var(--vscode-activityBarBadge-background);
color: var(--vscode-activityBarBadge-foreground);
border: 2px solid var(--vscode-activityBarBadge-background);
box-shadow: 0 0 8px rgba(0, 123, 255, 0.3);
animation: optimizationGlow 2s ease-in-out infinite;
}
.functionItemOptimizationHighlight .optimizeButton {
background-color: var(--vscode-activityBarBadge-foreground);
color: var(--vscode-activityBarBadge-background);
font-weight: 600;
}
@keyframes optimizationGlow {
0%,
100% {
box-shadow: 0 0 8px rgba(0, 123, 255, 0.3);
animation: optimizationGlow 2s ease-in-out infinite;
}
.functionItemOptimizationHighlight .optimizeButton {
background-color: var(--vscode-activityBarBadge-foreground);
color: var(--vscode-activityBarBadge-background);
font-weight: 600;
50% {
box-shadow: 0 0 16px rgba(0, 123, 255, 0.6);
}
@keyframes optimizationGlow {
0%, 100% {
box-shadow: 0 0 8px rgba(0, 123, 255, 0.3);
}
50% {
box-shadow: 0 0 16px rgba(0, 123, 255, 0.6);
}
}
/* === Optimize Button === */
.optimizeButton {
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border: 1px solid transparent;
border-radius: 4px;
padding: 3px 10px;
font-size: 0.8em;
cursor: pointer;
transition: all 0.2s ease;
min-width: 60px;
margin-left: 8px;
flex-shrink: 0;
opacity: 0.7;
font-weight: 400;
display: flex;
align-items: center;
gap: 8px;
}
.optimizeButton:hover:not(:disabled) {
border-color: var(--vscode-badge-background);
background: var(--vscode-badge-background);
color: var(--vscode-badge-foreground);
opacity: 0.9;
}
.optimizeButton:active:not(:disabled) {
border-color: var(--vscode-focusBorder);
opacity: 1;
}
.optimizeButton:disabled {
background: color-mix(in srgb, var(--vscode-badge-background) 40%, transparent);
color: var(--vscode-badge-foreground);
cursor: not-allowed;
}
}
/* === Optimize Button === */
.optimizeButton {
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border: 1px solid transparent;
border-radius: 4px;
padding: 3px 10px;
font-size: 0.8em;
cursor: pointer;
transition: all 0.2s ease;
min-width: 60px;
margin-left: 8px;
flex-shrink: 0;
opacity: 0.7;
font-weight: 400;
display: flex;
align-items: center;
gap: 8px;
}
.optimizeButton:hover:not(:disabled) {
border-color: var(--vscode-badge-background);
background: var(--vscode-badge-background);
color: var(--vscode-badge-foreground);
opacity: 0.9;
}
.optimizeButton:active:not(:disabled) {
border-color: var(--vscode-focusBorder);
opacity: 1;
}
.optimizeButton:disabled {
background: color-mix(
in srgb,
var(--vscode-badge-background) 40%,
transparent
);
color: var(--vscode-badge-foreground);
cursor: not-allowed;
}

View file

@ -1,14 +1,12 @@
@import "./codicons.min.css";
.methods-label {
font-size: 0.85em;
color: var(--vscode-descriptionForeground);
margin: 6px 0 4px 20px;
display: block;
}
.expanded .methods-label {
display: block;
:root {
--code-block-bg: color-mix(
in srgb,
var(--vscode-editor-background) 95%,
black 5%
);
/* --code-block-bg: var(--vscode-editor-background, --vscode-sideBar-background, rgb(30 30 30))cc; */
}
body {
@ -18,10 +16,8 @@ body {
font-weight: var(--vscode-font-weight);
font-size: var(--vscode-font-size);
color: var(--vscode-editor-foreground);
background-color: var(--vscode-sideBar-background);
}
/* Scrollbar styling */
* {
scrollbar-width: thin; /* Firefox */
@ -46,477 +42,6 @@ body {
background: var(--vscode-scrollbarSlider-hoverBackground);
}
.app-container {
display: flex;
flex-direction: column;
justify-content: space-between;
height: 100vh;
overflow: hidden;
position: relative;
}
.header-card,
.content-card {
margin: 8px;
border-radius: 4px;
background-color: var(--vscode-editor-background);
border: 1px solid var(--vscode-panel-border, transparent);
overflow: auto;
}
.header-card {
padding: 12px 16px;
margin-bottom: 4px;
}
.status-container {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.status-badge {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 10px;
border-radius: 16px;
background-color: var(--vscode-editor-background);
border: 1px solid var(--vscode-editorGroupHeader-tabsBorder);
transition: all 0.3s ease;
}
.status-badge .codicon {
font-size: 14px;
}
.status-badge #status-text {
font-size: 0.9em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 240px;
}
.status-running,
.status-connecting {
border-color: var(--vscode-activityBarBadge-background);
background-color: rgba(20, 110, 230, 0.1);
}
.center {
display: flex;
justify-content: center;
align-items: center;
}
.status-success,
.status-empty {
border-color: var(--vscode-testing-iconPassed);
background-color: rgba(48, 180, 120, 0.1);
}
.status-error,
.status-serverError {
border-color: var(--vscode-testing-iconFailed);
background-color: rgba(220, 70, 60, 0.1);
}
.status-noPythonFile,
.status-idle {
border-color: var(--vscode-editorInfo-foreground);
background-color: rgba(70, 150, 220, 0.1);
}
.action-button {
background-color: transparent;
color: var(--vscode-descriptionForeground);
border: 1px solid transparent;
border-radius: 4px;
padding: 6px;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
position: relative;
opacity: 0.6;
}
.action-button:hover:not(:disabled) {
border-color: var(--vscode-input-border);
color: var(--vscode-icon-foreground);
opacity: 1;
}
.action-button:disabled {
opacity: 0.3;
cursor: not-allowed;
}
.action-button .codicon {
font-size: 16px;
}
.file-context {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 8px;
border-radius: 6px;
background-color: var(--vscode-editor-lineHighlightBackground,
rgba(128, 128, 128, 0.1));
}
.file-label {
font-size: 0.9em;
color: var(--vscode-descriptionForeground);
}
.file-name {
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
position: relative;
padding-left: 2px;
}
.hidden-element {
display: none;
visibility: hidden;
position: absolute;
width: 0;
height: 0;
overflow: hidden;
}
.content-card {
flex: 1;
display: flex;
flex-direction: column;
overflow: auto;
padding: 0;
}
.section-header {
padding: 12px 16px;
border-bottom: 1px solid var(--vscode-panel-border);
}
.section-title {
display: flex;
align-items: center;
gap: 10px;
font-weight: 600;
letter-spacing: 0.02em;
}
.section-title .codicon {
color: var(--vscode-descriptionForeground);
opacity: 0.7;
}
.badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 16px;
height: 16px;
padding: 0 6px;
font-size: 0.75em;
font-weight: 500;
border-radius: 8px;
background-color: var(--vscode-badge-background);
color: var(--vscode-badge-foreground);
opacity: 0.9;
}
.functions-wrapper {
flex: 1;
overflow: auto;
padding: 0 0 8px 0;
transition: opacity 0.3s ease;
}
.functions-wrapper.disabled {
opacity: 0.7;
pointer-events: none;
}
.function-list {
list-style: none;
padding: 0 16px;
margin: 8px 0;
}
.class-section {
margin-bottom: 10px;
border-radius: 6px;
overflow: hidden;
transition: all 0.2s ease;
}
.class-header {
display: flex;
align-items: center;
padding: 8px 12px;
border-radius: 6px;
background-color: var(--vscode-list-inactiveSelectionBackground,
rgba(128, 128, 128, 0.1));
cursor: pointer;
transition: all 0.25s ease;
gap: 8px;
position: relative;
}
.class-header:hover {
background-color: var(--vscode-list-hoverBackground);
}
.class-header.highlighted {
background-color: var(--vscode-list-activeSelectionBackground);
color: var(--vscode-list-activeSelectionForeground);
}
.class-toggle {
transition: transform 0.3s ease;
}
.expanded .class-toggle.codicon-chevron-right {
transform: rotate(90deg);
}
.class-icon {
color: var(--vscode-descriptionForeground);
opacity: 0.5;
}
.class-name {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-weight: 600;
}
.method-count {
font-size: 0.8em;
}
.methods-list {
list-style: none;
padding: 0 0 0 32px;
margin: 0;
overflow: hidden;
transition: all 0.3s ease;
position: relative;
}
.methods-list::before {
content: "";
position: absolute;
top: 0;
bottom: 0;
left: 12px;
width: 1px;
background-color: var(--vscode-editorIndentGuide-background);
}
.function-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 4px 8px;
margin: 3px 0;
border-radius: 4px;
background-color: var(--vscode-editor-background);
border: 1px solid transparent;
transition: all 0.25s ease;
position: relative;
overflow: hidden;
}
.function-item:hover {
background-color: var(--vscode-list-hoverBackground);
}
.function-item.highlighted {
background-color: var(--vscode-list-activeSelectionBackground);
color: var(--vscode-list-activeSelectionForeground);
border-color: var(--vscode-focusBorder,
var(--vscode-list-activeSelectionBackground));
}
.function-item-content {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
overflow: hidden;
cursor: pointer;
min-width: 0;
border-radius: 4px;
padding: 2px;
transition: all 0.2s ease;
}
.function-icon {
flex-shrink: 0;
opacity: 0.4;
font-size: 14px;
transition: opacity 0.2s ease;
}
.function-item:hover .function-icon {
opacity: 0.6;
}
.method-item .function-icon {
color: var(--vscode-descriptionForeground);
}
.standalone-function-item .function-icon {
color: var(--vscode-descriptionForeground);
}
.function-name {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex-grow: 1;
min-width: 50px;
position: relative;
}
.function-item.optimizing {
opacity: 0.8;
background-color: var(--vscode-editor-inactiveSelectionBackground,
rgba(128, 128, 128, 0.1));
}
.function-item.optimization-highlight {
background-color: var(--vscode-activityBarBadge-background);
color: var(--vscode-activityBarBadge-foreground);
border: 2px solid var(--vscode-activityBarBadge-background);
box-shadow: 0 0 8px rgba(0, 123, 255, 0.3);
animation: optimizationGlow 2s ease-in-out infinite;
}
.function-item.optimization-highlight .optimize-button {
background-color: var(--vscode-activityBarBadge-foreground);
color: var(--vscode-activityBarBadge-background);
font-weight: 600;
}
@keyframes optimizationGlow {
0%,
100% {
box-shadow: 0 0 8px rgba(0, 123, 255, 0.3);
}
50% {
box-shadow: 0 0 16px rgba(0, 123, 255, 0.6);
}
}
.optimize-button {
background: transparent;
color: var(--vscode-badge-foreground);
border: 1px solid transparent;
border-radius: 4px;
padding: 3px 10px;
font-size: 0.8em;
cursor: pointer;
transition: all 0.2s ease;
min-width: 60px;
margin-left: 8px;
flex-shrink: 0;
opacity: 0.7;
font-weight: 400;
display: flex;
align-items: center;
gap: 8px;
}
.optimize-button:hover:not(:disabled) {
border-color: var(--vscode-badge-background);
background: var(--vscode-badge-background);
color: var(--vscode-badge-foreground);
opacity: 0.9;
}
.optimize-button:active:not(:disabled) {
border-color: var(--vscode-focusBorder);
opacity: 1;
}
.optimize-button:disabled {
opacity: 0.3;
cursor: not-allowed;
color: var(--vscode-disabledForeground);
}
.list-separator {
height: 1px;
background: linear-gradient(to right,
transparent,
var(--vscode-panel-border),
transparent);
margin: 10px 0;
list-style: none;
}
.placeholder-card {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
padding: 20px;
margin: 12px;
border-radius: 4px;
background-color: var(--vscode-editor-background);
border: 1px solid var(--vscode-panel-border);
text-align: center;
}
.placeholder-card .codicon {
font-size: 24px;
opacity: 0.7;
}
.placeholder-card p {
margin: 0;
color: var(--vscode-descriptionForeground);
font-size: 0.9em;
}
.placeholder-card.status-running,
.placeholder-card.status-connecting {
border-color: var(--vscode-activityBarBadge-background);
background-color: rgba(20, 110, 230, 0.05);
}
.placeholder-card.status-success,
.placeholder-card.status-empty {
border-color: var(--vscode-testing-iconPassed);
background-color: rgba(48, 180, 120, 0.05);
}
.placeholder-card.status-error,
.placeholder-card.status-serverError {
border-color: var(--vscode-testing-iconFailed);
background-color: rgba(220, 70, 60, 0.05);
}
.placeholder-card.status-noPythonFile,
.placeholder-card.status-idle {
border-color: var(--vscode-editorInfo-foreground);
background-color: rgba(70, 150, 220, 0.05);
}
@keyframes spin {
0% {
transform: rotate(0deg);
@ -531,333 +56,16 @@ body {
animation: spin 1s linear infinite;
}
.section-title .codicon-lightbulb {
color: var(--vscode-textLink-foreground);
}
/* Clean optimization banner design */
.optimization-banner {
margin: 8px;
border-radius: 6px;
background-color: var(--vscode-editor-background);
border: 1px solid var(--vscode-activityBarBadge-background);
overflow: hidden;
animation: slideDown 0.3s ease-out;
}
.optimization-banner.hidden {
display: none;
}
.optimization-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background-color: var(--vscode-activityBarBadge-background);
color: var(--vscode-activityBarBadge-foreground);
}
.optimize-current-diff {
margin-bottom: 8px;
}
.optimization-title-section {
display: flex;
align-items: center;
gap: 10px;
}
.optimization-icon {
font-size: 16px;
}
.optimization-icon.loading {
animation: spin 2s linear infinite;
}
.optimization-title-text {
.app-container {
display: flex;
flex-direction: column;
gap: 2px;
height: 100vh;
position: relative;
}
.optimization-action {
font-size: 0.85em;
opacity: 0.9;
}
.optimization-function {
font-weight: 600;
font-size: 0.95em;
}
.cancel-btn {
background: transparent;
border: none;
color: var(--vscode-activityBarBadge-foreground);
cursor: pointer;
padding: 4px;
border-radius: 3px;
opacity: 0.8;
transition: opacity 0.2s ease;
}
.cancel-btn:hover {
opacity: 1;
background-color: rgba(255, 255, 255, 0.1);
}
.optimization-progress-section {
padding: 0;
}
.optimization-steps-container {
display: flex;
flex-direction: column;
}
.optimization-step-item {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 12px 16px;
border-bottom: 1px solid var(--vscode-panel-border);
transition: all 0.2s ease;
}
.optimization-step-item:last-child {
border-bottom: none;
}
.optimization-step-item.queued {
opacity: 0.6;
}
.optimization-step-item.active {
background-color: var(--vscode-editor-lineHighlightBackground);
border-left: 3px solid var(--vscode-activityBarBadge-background);
}
.optimization-step-item.completed {
background-color: var(--vscode-diffEditor-insertedTextBackground);
border-left: 3px solid var(--vscode-testing-iconPassed);
}
.optimization-step-item.failed {
background-color: var(--vscode-diffEditor-removedTextBackground);
border-left: 3px solid var(--vscode-testing-iconFailed);
}
.step-indicator {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 50%;
background-color: var(--vscode-editor-background);
border: 2px solid var(--vscode-panel-border);
flex-shrink: 0;
margin-top: 2px;
}
.optimization-step-item.active .step-indicator {
border-color: var(--vscode-activityBarBadge-background);
background-color: var(--vscode-activityBarBadge-background);
color: var(--vscode-activityBarBadge-foreground);
animation: pulse 1.5s ease-in-out infinite;
}
.optimization-step-item.completed .step-indicator {
border-color: var(--vscode-testing-iconPassed);
background-color: var(--vscode-testing-iconPassed);
color: white;
}
.optimization-step-item.failed .step-indicator {
border-color: var(--vscode-testing-iconFailed);
background-color: var(--vscode-testing-iconFailed);
color: white;
}
.step-indicator .codicon {
font-size: 12px;
}
.step-content {
.content {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
}
.step-label {
font-weight: 500;
color: var(--vscode-editor-foreground);
}
.step-result {
font-size: 0.85em;
color: var(--vscode-descriptionForeground);
font-weight: 600;
}
.optimization-step-item.completed .step-result {
color: var(--vscode-testing-iconPassed);
}
.optimization-step-item.failed .step-result {
color: var(--vscode-testing-iconFailed);
}
.optimization-results {
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 12px;
background-color: var(--vscode-editor-background);
border-top: 1px solid var(--vscode-panel-border);
z-index: 1001;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}
@keyframes pulse {
0% {
background-color: var(--vscode-editor-background);
}
50% {
background-color: var(--vscode-editor-lineHighlightBackground);
}
100% {
background-color: var(--vscode-editor-background);
}
}
.view-functions-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
min-width: 200px;
background-color: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border: none;
border-radius: 4px;
padding: 10px 16px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
}
.view-functions-btn:hover {
background-color: var(--vscode-button-hoverBackground);
transform: translateY(-1px);
}
.view-functions-btn:active {
transform: translateY(1px);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.view-functions-btn:focus {
outline: 2px solid var(--vscode-focusBorder);
outline-offset: 2px;
}
.view-functions-btn .codicon {
font-size: 16px;
}
.success .view-functions-btn {
background-color: var(--vscode-button-secondaryBackground);
color: var(--vscode-button-secondaryForeground);
}
.success .view-functions-btn:hover {
background-color: var(--vscode-button-secondaryHoverBackground);
}
.failure .view-functions-btn {
background-color: var(--vscode-editorError-foreground);
color: white;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideInUp {
from {
transform: translateY(30px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
@keyframes slideDown {
from {
transform: translateY(-20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
@keyframes pulse {
0%,
100% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.1);
opacity: 0.8;
}
}
/* Refresh button spacing in section title */
.refresh-inline {
margin-left: auto;
}
/* Ensure all components respect user's theme preferences */
@media (prefers-color-scheme: dark) {
.header-card,
.content-card {
background-color: var(--vscode-editor-background);
}
}
/* Clean, minimal design for focused work */
.function-item {
border-radius: 3px;
overflow: hidden;
}
/* api key container */
@ -908,5 +116,125 @@ body {
.api-key-container a:hover {
text-decoration: underline;
}
/* end of api key container */
/* glowing animation */
:root {
--duration: 5s;
--scale: 0.6;
--glow-size: 100%;
}
/* core glow element (empty element positioned over the container) */
.glowEffect {
pointer-events: none;
position: absolute;
inset: 0;
z-index: -1;
transform: translateZ(0); /* GPU hint */
will-change: transform, opacity, filter, background-position;
}
/* The visual gradient sits on a pseudo element so we can transform it independently */
.glowEffect::before {
content: "";
position: absolute;
left: 50%;
top: 50%;
width: var(--glow-size);
height: var(--glow-size);
transform: translate(-50%, -50%) scale(var(--scale));
border-radius: 50%;
filter: blur(25px) brightness(1.2);
mix-blend-mode: screen; /* nicer glow when over dark bg; change to normal if undesired */
transition:
opacity 200ms linear,
transform 200ms linear;
pointer-events: none;
z-index: -1;
}
/* color variables (override inline or via stylesheet) */
.glowEffect {
--g1: #ffc043; /* main golden orange */
--g2: #f57627; /* warm tangerine */
--g3: #f53b38; /* coral red for contrast */
--g4: #7363ed; /* vibrant violet for balance */
}
.modePulse::before {
background:
radial-gradient(
circle at 30% 30%,
color-mix(in srgb, var(--g1) 85%, transparent) 0%,
transparent 40%
),
radial-gradient(
circle at 70% 70%,
color-mix(in srgb, var(--g2) 85%, transparent) 0%,
transparent 40%
),
radial-gradient(
circle at 50% 50%,
color-mix(in srgb, var(--g3) 80%, transparent) 0%,
transparent 45%
);
animation: glowPulse var(--duration) ease-in-out infinite;
border-radius: 50%;
}
@keyframes glowPulse {
0% {
transform: translate(-50%, -50%) scale(calc(var(--scale) * 1));
opacity: 0.45;
}
50% {
transform: translate(-50%, -50%) scale(calc(var(--scale) * 1.12));
opacity: 0.85;
}
100% {
transform: translate(-50%, -50%) scale(calc(var(--scale) * 1));
opacity: 0.45;
}
}
.table-wrapper {
overflow-x: auto;
margin: 1rem 0;
/* border-radius: 12px; */
background-color: var(--vscode-editor-background);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.table-wrapper table {
width: 100%;
border-collapse: collapse; /* merges shared borders */
color: var(--vscode-editor-foreground);
}
.table-wrapper th,
.table-wrapper td {
padding: 0.6rem 0.8rem;
text-align: center !important;
border: 1px solid var(--vscode-editorGroup-border); /* only cell borders */
}
.table-wrapper th:first-child,
.table-wrapper td:first-child {
text-align: left !important;
}
.table-wrapper th {
background-color: var(--vscode-sideBarSectionHeader-background);
color: var(--vscode-sideBarSectionHeader-foreground);
font-weight: 600;
position: sticky;
top: 0;
z-index: 1;
}
.table-wrapper tr:nth-child(even) td {
background-color: var(--vscode-editor-inactiveSelectionBackground);
}
.table-wrapper tr:hover td {
background-color: var(--vscode-list-hoverBackground);
}

View file

@ -5,6 +5,9 @@
display: flex;
flex-direction: column;
flex: 1 1 auto;
height: 100%;
overflow-y: auto;
margin: 1rem 0;
min-height: 0; /* critical for scrolling */
}
.queueTasksList {
@ -30,56 +33,66 @@
}
/* Optimizing → bluish gradient */
.queueTaskItem.optimizing {
background: linear-gradient(
to right,
rgba(10, 102, 194, .1) 0%,
rgba(10, 102, 194, .05) 50%,
rgba(10, 102, 194, 0) 100%
), var(--vscode-editorWidget-background);
.queueTaskItem.optimizing,
.queueTaskItem.initializing {
background:
linear-gradient(
to right,
rgba(10, 102, 194, 0.1) 0%,
rgba(10, 102, 194, 0.05) 50%,
rgba(10, 102, 194, 0) 100%
),
var(--vscode-editorWidget-background);
}
/* Queued → yellowish gradient */
.queueTaskItem.queued {
background: linear-gradient(
to right,
rgba(181, 137, 0, 0.1) 0%,
rgba(181, 137, 0, 0.05) 50%,
rgba(181, 137, 0, 0) 100%
), var(--vscode-editorWidget-background);
background:
linear-gradient(
to right,
rgba(181, 137, 0, 0.1) 0%,
rgba(181, 137, 0, 0.05) 50%,
rgba(181, 137, 0, 0) 100%
),
var(--vscode-editorWidget-background);
}
/* Completed → greenish gradient */
.queueTaskItem.completed {
background: linear-gradient(
to right,
rgba(34, 134, 58, 0.1) 0%,
rgba(34, 134, 58, 0.05) 50%,
rgba(34, 134, 58, 0) 100%
), var(--vscode-editorWidget-background);
background:
linear-gradient(
to right,
rgba(34, 134, 58, 0.1) 0%,
rgba(34, 134, 58, 0.05) 50%,
rgba(34, 134, 58, 0) 100%
),
var(--vscode-editorWidget-background);
}
/* Failed → reddish gradient */
.queueTaskItem.failed {
background: linear-gradient(
to right,
rgba(215, 58, 73, 0.2) 0%,
rgba(215, 58, 73, 0.1) 50%,
rgba(215, 58, 73, 0) 100%
), var(--vscode-editorWidget-background);
background:
linear-gradient(
to right,
rgba(215, 58, 73, 0.2) 0%,
rgba(215, 58, 73, 0.1) 50%,
rgba(215, 58, 73, 0) 100%
),
var(--vscode-editorWidget-background);
}
/* Skipped → neutral gray gradient */
.queueTaskItem.skipped {
background: linear-gradient(
to right,
rgba(106, 115, 125, 0.2) 0%,
rgba(106, 115, 125, 0.1) 50%,
rgba(106, 115, 125, 0) 100%
), var(--vscode-editorWidget-background);
background:
linear-gradient(
to right,
rgba(106, 115, 125, 0.2) 0%,
rgba(106, 115, 125, 0.1) 50%,
rgba(106, 115, 125, 0) 100%
),
var(--vscode-editorWidget-background);
}
/* === Header: title + status === */
.taskItemLabel {
display: flex;
@ -102,7 +115,6 @@
text-decoration: underline;
}
.queueTaskName .filePath {
color: var(--vscode-descriptionForeground);
font-size: 0.85em;
@ -120,7 +132,8 @@
}
/* Optimizing → bluish */
.queueTaskStatus.optimizing {
.queueTaskStatus.optimizing,
.queueTaskStatus.initializing {
color: #1d79d4; /* readable blue */
background: rgba(10, 102, 194, 0.15);
}
@ -149,7 +162,6 @@
background: rgba(106, 115, 125, 0.15);
}
.taskItemHeader {
display: flex;
justify-content: space-between;
@ -157,7 +169,6 @@
margin-bottom: 8px;
}
/* === Description text === */
.queueTaskDescription {
font-size: 13px;
@ -208,9 +219,12 @@
background: transparent;
color: var(--vscode-foreground);
cursor: pointer;
transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
transition:
background 0.15s ease,
color 0.15s ease,
border-color 0.15s ease;
}
.queueTaskDelete:hover{
.queueTaskDelete:hover {
background: var(--vscode-list-hoverBackground);
border-color: var(--vscode-focusBorder);
}
@ -227,7 +241,10 @@
color: #3fc05d; /* soft green */
background: rgba(34, 134, 58, 0.15);
cursor: pointer;
transition: background 0.15s ease, color 0.15s ease, border 0.15s ease;
transition:
background 0.15s ease,
color 0.15s ease,
border 0.15s ease;
}
.queueTaskPatch:hover {
@ -244,10 +261,13 @@
background: transparent;
color: var(--vscode-foreground);
cursor: pointer;
transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
transition:
background 0.15s ease,
color 0.15s ease,
border-color 0.15s ease;
}
.ghostActionButton:hover{
.ghostActionButton:hover {
background: var(--vscode-list-hoverBackground);
color: var(--vscode-foreground);
}
@ -256,7 +276,6 @@
flex-shrink: 0;
}
/* === Empty state === */
.queueEmptyState {
display: flex;
@ -291,8 +310,6 @@
opacity: 0.8;
}
.queueTasksHeader {
position: relative;
display: flex;
@ -303,7 +320,9 @@
border-radius: 10px;
background: var(--vscode-editorWidget-background);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
transition: background-color 0.2s ease, transform 0.1s ease;
transition:
background-color 0.2s ease,
transform 0.1s ease;
}
.queueTasksTitle {
@ -329,7 +348,6 @@
transform: rotate(180deg);
}
.options {
position: absolute;
top: 100%; /* right below the header */
@ -339,7 +357,7 @@
background: var(--vscode-editorWidget-background);
border: 1px solid var(--vscode-input-border);
border-radius: 6px;
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
padding: 6px 3px;
z-index: 1000;
}
@ -354,7 +372,7 @@
}
.options span:hover {
background: rgba(255,255,255,0.1);
background: rgba(255, 255, 255, 0.1);
color: #fff;
}
/* Minimal "logs" button */
@ -369,7 +387,9 @@
align-items: center;
gap: 4px;
cursor: pointer;
transition: background 0.15s ease, border-color 0.15s ease;
transition:
background 0.15s ease,
border-color 0.15s ease;
outline: none;
}
@ -378,10 +398,8 @@
border-color: var(--vscode-focusBorder);
}
.queueTasksContent {
flex: 1 1 auto;
overflow-y: auto;
min-height: 0;
}

View file

@ -0,0 +1,68 @@
.tabs {
display: flex;
align-items: center;
justify-content: center;
padding: 0.2rem 0.5rem;
border-radius: 6px;
background-color: var(--vscode-sideBar-background);
}
.tab {
flex: 1;
background: transparent;
border: none;
color: var(--vscode-foreground);
font-size: 13px;
font-weight: 500;
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
transition:
background-color 0.2s ease,
color 0.2s ease;
}
.tab:hover {
background-color: var(--vscode-list-hoverBackground);
color: var(--vscode-list-hoverForeground, var(--vscode-foreground));
}
.tab:focus {
outline: 1px solid var(--vscode-focusBorder);
outline-offset: 1px;
}
.active {
background-color: var(--vscode-list-activeSelectionBackground);
color: var(--vscode-list-activeSelectionForeground);
}
.tabText {
position: relative;
display: inline-flex;
align-items: center;
gap: 6px;
}
.splitter {
width: 1px;
height: 16px;
background-color: var(--vscode-badge-foreground);
margin: 0 6px;
}
.badge {
display: inline-flex;
align-items: center;
justify-content: center;
background-color: var(--vscode-badge-background);
color: var(--vscode-badge-foreground);
font-size: 11px;
font-weight: 600;
min-width: 16px;
height: 16px;
border-radius: 8px;
line-height: 1;
padding: 2px 4px;
z-index: 1;
}

View file

@ -0,0 +1,79 @@
import { useState, useCallback, useEffect, useRef } from "react";
/**
* Options for copying text to clipboard
*/
interface CopyOptions {
/** Duration in ms to show success feedback (default: 2000) */
feedbackDuration?: number;
/** Optional callback when copy succeeds */
onSuccess?: () => void;
/** Optional callback when copy fails */
onError?: (error: Error) => void;
}
/**
* Copy text to clipboard with error handling
*/
export const copyToClipboard = async (
text: string,
options?: CopyOptions,
): Promise<boolean> => {
try {
await navigator.clipboard.writeText(text);
options?.onSuccess?.();
return true;
} catch (error) {
const err =
error instanceof Error ? error : new Error("Failed to copy to clipboard");
options?.onError?.(err);
console.error("Failed to copy to clipboard:", err);
return false;
}
};
/**
* React hook for managing clipboard copy state with feedback
*/
export const useCopyToClipboard = (feedbackDuration = 2000) => {
const [showCopyFeedback, setShowCopyFeedback] = useState(false);
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
const copyWithFeedback = useCallback(
async (text: string, e?: React.MouseEvent) => {
e?.stopPropagation();
// Clear any existing timeout
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
const success = await copyToClipboard(text, {
onSuccess: () => {
setShowCopyFeedback(true);
timeoutRef.current = setTimeout(() => {
setShowCopyFeedback(false);
timeoutRef.current = null;
}, feedbackDuration);
},
});
return success;
},
[feedbackDuration],
);
// Cleanup timeout on unmount
useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, []);
return {
showCopyFeedback,
copyWithFeedback,
};
};

View file

@ -0,0 +1,157 @@
// directly import the theme and language modules, only the ones you imported will be bundled.
import darkTheme from "@shikijs/themes/github-dark";
import lightTheme from "@shikijs/themes/github-light";
// `shiki/core` entry does not include any themes or languages or the wasm binary.
import { createHighlighterCore, type HighlighterCore } from "shiki/core";
import { createOnigurumaEngine } from "shiki/engine/oniguruma";
type BundledLanguage = "python";
// Extend BundledLanguage to include 'txt' because Shiki supports this but it is
// not listed in the bundled languages
export type ExtendedLanguage = BundledLanguage | "txt";
// Map common language aliases to their Shiki BundledLanguage equivalent
const languageAliases: Record<string, ExtendedLanguage> = {
// Python variants
py: "python",
python3: "python",
python: "python",
py3: "python",
};
// Track which languages we've warned about to avoid duplicate warnings
const warnedLanguages = new Set<string>();
// Normalize language to a valid Shiki language
export function normalizeLanguage(
language: string | undefined,
): ExtendedLanguage {
if (language === undefined) {
return "txt";
}
// Convert to lowercase for consistent matching
const normalizedInput = language.toLowerCase();
// Check if it's an alias
if (normalizedInput in languageAliases) {
return languageAliases[normalizedInput]!;
}
// Warn about unrecognized language and default to txt (only once per language)
if (language !== "txt" && !warnedLanguages.has(language)) {
console.warn(
`[Shiki] Unrecognized language '${language}', defaulting to txt.`,
);
warnedLanguages.add(language);
}
return "txt";
}
// Export function to check if a language is loaded
export const isLanguageLoaded = (language: string): boolean => {
return state.loadedLanguages.has(normalizeLanguage(language));
};
// Artificial delay for testing language loading (ms) - for testing
const LANGUAGE_LOAD_DELAY = 0;
// Common languages for first-stage initialization
const initialLanguages: BundledLanguage[] = ["python"];
// Singleton state
const state: {
instance: HighlighterCore | null;
instanceInitPromise: Promise<HighlighterCore> | null;
loadedLanguages: Set<ExtendedLanguage>;
pendingLanguageLoads: Map<ExtendedLanguage, Promise<void>>;
} = {
instance: null,
instanceInitPromise: null,
loadedLanguages: new Set<ExtendedLanguage>(["txt"]),
pendingLanguageLoads: new Map(),
};
export const getHighlighter = async (
language?: string,
): Promise<HighlighterCore> => {
try {
const shikilang = normalizeLanguage(language);
// Initialize highlighter if needed
if (!state.instanceInitPromise) {
state.instanceInitPromise = (async () => {
// const startTime = performance.now()
// console.debug("[Shiki] Initialization started...")
const instance = await createHighlighterCore({
themes: [darkTheme, lightTheme],
langs: [import("@shikijs/langs/python")],
engine: createOnigurumaEngine(import("shiki/wasm")),
});
// const elapsed = Math.round(performance.now() - startTime)
// console.debug(`[Shiki] Initialization complete (${elapsed}ms)`)
state.instance = instance;
// Track initially loaded languages
initialLanguages.forEach((lang) => state.loadedLanguages.add(lang));
return instance;
})();
}
// Wait for initialization to complete
const instance = await state.instanceInitPromise;
// Load requested language if needed (txt is already in loadedLanguages)
if (!state.loadedLanguages.has(shikilang)) {
// Check for existing pending load
let loadingPromise = state.pendingLanguageLoads.get(shikilang);
if (!loadingPromise) {
// const loadStart = performance.now()
// Create new loading promise
loadingPromise = (async () => {
try {
// Add artificial delay for testing if nonzero
if (LANGUAGE_LOAD_DELAY > 0) {
await new Promise((resolve) =>
setTimeout(resolve, LANGUAGE_LOAD_DELAY),
);
}
// await instance.loadLanguage(shikilang);
state.loadedLanguages.add(shikilang);
// const loadTime = Math.round(performance.now() - loadStart)
// console.debug(`[Shiki] Loaded language ${shikilang} (${loadTime}ms)`)
} catch (error) {
console.error(
`[Shiki] Failed to load language ${shikilang}:`,
error,
);
throw error;
} finally {
// Clean up pending promise after completion
state.pendingLanguageLoads.delete(shikilang);
}
})();
// Store the promise
state.pendingLanguageLoads.set(shikilang, loadingPromise);
}
await loadingPromise;
}
return instance;
} catch (error) {
console.error("[Shiki] Error in getHighlighter:", error);
throw error;
}
};

View file

@ -0,0 +1,31 @@
// https://github.com/msgbyte/tailchat/blob/master/client/shared/hooks/useMemoizedFn.ts
import { useMemo, useRef } from "react";
type Noop = (this: any, ...args: any[]) => any;
type PickFunction<T extends Noop> = (
this: ThisParameterType<T>,
...args: Parameters<T>
) => ReturnType<T>;
export function useMemoFunc<T extends Noop>(fn: T) {
if (process.env.NODE_ENV === "development") {
if (typeof fn !== "function") {
console.error(
`useMemoFunc expected parameter is a function, got ${typeof fn}`,
);
}
}
const fnRef = useRef<T>(fn);
// why not write `fnRef.current = fn`?
// https://github.com/alibaba/hooks/issues/728
fnRef.current = useMemo(() => fn, [fn]);
const memoizedFn = useRef<PickFunction<T>>(function (this, ...args) {
return fnRef.current.apply(this, args);
});
return memoizedFn.current as T;
}

View file

@ -0,0 +1,16 @@
import { useEffect, useRef } from "react";
export default function useUpdateEffect(
callback: React.EffectCallback,
dependencies: any[],
) {
const firstRenderRef = useRef(true);
useEffect(() => {
if (firstRenderRef.current) {
firstRenderRef.current = false;
return;
}
return callback();
}, dependencies);
}

View file

@ -1,5 +1,11 @@
import type {
ChangeTaskFocusFromBackendMessage,
NewLogEntryMessage,
RecievedFilesInWorkspace,
RecievedFunctionsMessage,
RestoreStateMessage,
SetActiveSidebarTabMessage,
SetApiKeyLoadingStateMessage,
UpdateQueueTasksMessage,
UpdateStateMessage,
WebviewMessage,
@ -12,9 +18,6 @@ export const messageHandler = (event: MessageEvent, store: State & Action) => {
return;
}
const webviewMessage = message as WebviewMessage;
console.log(
`received message from the sidebar provider ${JSON.stringify(message)}`,
);
switch (webviewMessage.type) {
case "updateState":
@ -24,11 +27,38 @@ export const messageHandler = (event: MessageEvent, store: State & Action) => {
store.updateQueueTasks(webviewMessage as UpdateQueueTasksMessage);
break;
case "restoreStateFromCache":
console.log({ webviewMessage });
store.restoreStateFromCache(webviewMessage as RestoreStateMessage);
break;
case "apiKeyEnterValid":
store.apiKeyEnterValid();
break;
case "recievedFilesInWorkspace":
const files = (webviewMessage as RecievedFilesInWorkspace).payload.files;
store.updateFilesInWorkspace(files);
break;
case "recievedFunctions":
const { filePath, functions } = (
webviewMessage as RecievedFunctionsMessage
).payload;
store.updateFunctionsInCurrentContextFile(filePath, functions);
break;
case "changeTaskFocusFromBackend":
const { id } = (webviewMessage as ChangeTaskFocusFromBackendMessage)
.payload;
store.changeTaskFocus(id);
break;
case "newLogEntry":
const { log } = (webviewMessage as NewLogEntryMessage).payload;
store.addLog(log);
break;
case "setActiveSidebarTab":
const { tab } = (webviewMessage as SetActiveSidebarTabMessage).payload;
store.setActiveTab(tab);
break;
case "setApiKeyLoadingState":
const { loading } = (webviewMessage as SetApiKeyLoadingStateMessage)
.payload;
store.setApiKeyLoadingState(loading);
break;
}
};

View file

@ -1,4 +1,5 @@
export type MessageType =
| "showMessage"
| "updateState"
| "restoreStateFromCache"
| "updateQueueTasks"
@ -12,17 +13,29 @@ export type MessageType =
| "apiKeyEnter"
| "viewDiff"
| "viewPatch"
| "changeTaskFocus"
| "deleteTask"
| "viewLogs"
| "optimizeCurrentDiff";
| "optimizeCurrentDiff"
| "newLogEntry"
| "filesInWorkspace"
| "requestFunctions"
| "recievedFunctions"
| "recievedFilesInWorkspace"
| "changeTaskFocusFromBackend"
| "setActiveSidebarTab"
| "setApiKeyLoadingState";
export type QueueTaskItemStatus =
| "queued"
| "initializing"
| "optimizing"
| "completed"
| "failed"
| "skipped";
export type SidebarTab = "optimization" | "tasks";
export type QueueTaskItem = {
id: string;
status: QueueTaskItemStatus;
@ -34,6 +47,7 @@ export type QueueTaskItem = {
patch_id?: string;
explanation?: string;
speedupStr?: string;
logs: LogEntry[];
};
export type SidebarStatus =
@ -52,6 +66,14 @@ export interface WebviewMessage {
payload?: unknown;
}
export interface ShowMessageMessage extends WebviewMessage {
type: "showMessage";
payload: {
type: "info" | "error" | "warning";
message: string;
};
}
export interface UpdateStateMessage extends WebviewMessage {
type: "updateState";
payload: {
@ -74,6 +96,13 @@ export interface UpdateQueueTasksMessage extends WebviewMessage {
};
}
export interface SetApiKeyLoadingStateMessage extends WebviewMessage {
type: "setApiKeyLoadingState";
payload: {
loading: boolean;
};
}
export interface ApiKeyEnterInvalidMessage extends WebviewMessage {
type: "apiKeyEnterInvalid";
}
@ -82,6 +111,13 @@ export interface ApiKeyEnterValidMessage extends WebviewMessage {
type: "apiKeyEnterValid";
}
export interface NewLogEntryMessage extends WebviewMessage {
type: "newLogEntry";
payload: {
log: LogEntry;
};
}
export interface RequestAnalysisMessage extends WebviewMessage {
type: "requestAnalysis";
}
@ -110,6 +146,53 @@ export interface CreateSampleFileMessage extends WebviewMessage {
type: "createSampleFile";
}
export interface ChangeTaskFocusMessage extends WebviewMessage {
type: "changeTaskFocus";
payload: {
id: string;
};
}
export interface FilesInWorkspace extends WebviewMessage {
type: "filesInWorkspace";
}
export interface RecievedFilesInWorkspace extends WebviewMessage {
type: "recievedFilesInWorkspace";
payload: {
files: FileInWorkspace[];
};
}
export interface RequestFunctionsMessage extends WebviewMessage {
type: "requestFunctions";
payload: {
filePath: string;
};
}
export interface RecievedFunctionsMessage extends WebviewMessage {
type: "recievedFunctions";
payload: {
filePath: string;
functions: string[];
};
}
export interface ChangeTaskFocusFromBackendMessage extends WebviewMessage {
type: "changeTaskFocusFromBackend";
payload: {
id: string;
};
}
export interface SetActiveSidebarTabMessage extends WebviewMessage {
type: "setActiveSidebarTab";
payload: {
tab: SidebarTab;
};
}
export interface ApiKeyEnterMessage extends WebviewMessage {
type: "apiKeyEnter";
payload: {
@ -150,3 +233,42 @@ export interface ViewLogsMessage extends WebviewMessage {
export interface OptimizeCurrentDiffMessage extends WebviewMessage {
type: "optimizeCurrentDiff";
}
export type FileInWorkspace = { abs: string; rel: string };
/** Status of an execution step */
export type ExecutionStatus =
| "running"
| "success"
| "error"
| "warning"
| "info";
/** Type of content in a log entry */
export type LogContentType = "text" | "code" | "markdown" | "stats" | "error";
/** A single log entry in the execution log */
export interface LogEntry {
id: string;
/** Type of content this entry contains */
type: LogContentType;
takes_time?: boolean;
// for text messages
text?: string;
// for code messages
code?: string;
collapsed?: boolean;
function_name?: string;
file_name?: string;
// for markdown messages
markdown?: string;
}
export const LSPMessageType = {
CODE: "code",
TEXT: "text",
MARKDOWN: "markdown",
STATS: "stats",
};

View file

@ -18,7 +18,6 @@ export const LSP_COMMANDS = {
"getOptimizableFunctionsInCurrentDiff",
GET_OPTIMIZABLE_FUNCTIONS_IN_COMMIT: "getOptimizableFunctionsInCommit",
INITIALIZE_FUNCTION_OPTIMIZATION: "initializeFunctionOptimization",
DISCOVER_FUNCTION_TESTS: "discoverFunctionTests",
PERFORM_FUNCTION_OPTIMIZATION: "performFunctionOptimization",
VALIDATE_PROJECT: "validateProject",
ON_PATCH_APPLIED: "onPatchApplied",

View file

@ -13,11 +13,14 @@ import { LSP_COMMANDS, SIDEBAR_VIEW_ID } from "./constants";
import { Logger } from "./utils";
import type { LanguageClient } from "vscode-languageclient/node";
import type { ErrorWebviewConfig, OptimizationResponse } from "./types";
import { viewState } from "./webview/state";
import { optimizationEventEmitter, viewState } from "./webview/state";
import { ErrorWebview } from "./utils/ErrorWebview";
import { isTaskRunning } from "@codeflash/shared";
let logger: Logger = new Logger("Codeflash Extension");
let optimizationEventListener: vscode.Disposable | null = null;
const isGitUsedInRepo = async (): Promise<boolean> => {
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
if (!workspaceFolder) {
@ -93,46 +96,15 @@ const initializeLspClients = async (
// create another client for optimization as we are using a single pipe per client, so using a single client would be
// mean waiting for the optimization to be finished before calling any other lsp features
let optimizationClient: LanguageClient | undefined;
try {
await Promise.all([
(async () => {
client = await lspService.start(pythonPath);
})(),
(async () => {
optimizationClient = await optimizationLspService.start(pythonPath);
})(),
]);
logger.info("LSP client is ready and running.");
} catch (lspError) {
const errorMsg =
lspError instanceof Error ? lspError.message : String(lspError);
logger.error(
"LSP startup failed",
lspError instanceof Error ? lspError : undefined,
);
// Show detailed error with troubleshooting steps
const action = await vscode.window.showErrorMessage(
`Codeflash Language Server failed to start: ${errorMsg}`,
"Show Troubleshooting",
"View Logs",
"Retry",
);
if (action === "Show Troubleshooting") {
vscode.env.openExternal(
vscode.Uri.parse(
"https://github.com/codeflash-ai/codeflash#troubleshooting",
),
);
} else if (action === "View Logs") {
vscode.commands.executeCommand("workbench.action.openWindowLog");
} else if (action === "Retry") {
vscode.commands.executeCommand("workbench.action.reloadWindow");
}
throw new Error(`LSP initialization failed: ${errorMsg}`);
}
await Promise.all([
(async () => {
client = await lspService.start(pythonPath);
})(),
(async () => {
optimizationClient = await optimizationLspService.start(pythonPath);
})(),
]);
logger.info("LSP client is ready and running.");
if (!client || !optimizationClient) {
throw new Error("LSP client initialization failed");
@ -187,6 +159,7 @@ const initializeServicesAndProviders = async (
analysisService,
navigationService,
);
codeLensProvider.refresh(500); // refresh to get the code lenses of the current file
var endTime = performance.now();
// Register CodeLens provider for Python files
@ -204,16 +177,6 @@ const initializeServicesAndProviders = async (
},
);
const refreshAnalysisCommand = vscode.commands.registerCommand(
"codeflash.refreshAnalysis",
async () => {
logger?.info("Refreshing analysis from command");
codeLensProvider?.refresh();
// Also trigger sidebar refresh if visible
await sidebarProvider?.refreshAnalysis?.();
},
);
const optimizeAllCommand = vscode.commands.registerCommand(
"codeflash.optimizeAll",
async () => {
@ -227,7 +190,6 @@ const initializeServicesAndProviders = async (
// new GitWatcherService(sidebarProvider, lspClient),
codeLensDisposable,
optimizeFunctionCommand,
refreshAnalysisCommand,
optimizeAllCommand,
logger,
optimizationService,
@ -299,9 +261,10 @@ export async function doActivate(
pythonPath,
);
const { status, message } = await lspClient.sendRequest<{
const { status, message, moduleRoot } = await lspClient.sendRequest<{
status: string;
message?: string;
moduleRoot?: string;
}>(LSP_COMMANDS.VALIDATE_PROJECT, {});
logger.info(
`project validation result: ${JSON.stringify({ status, message })}`,
@ -326,6 +289,7 @@ export async function doActivate(
);
return;
}
viewState.moduleRoot = moduleRoot || "";
await Promise.all([
checkUserApiKey(lspClient),
@ -335,79 +299,34 @@ export async function doActivate(
logger.info(
`✅ Codeflash extension activation complete, took ${totalEndTime - totalStartTime} ms.`,
);
optimizationEventListener = optimizationEventEmitter.event((message) => {
const currentRunningTask = viewState.queueTasks.find(isTaskRunning);
if (!currentRunningTask) {
return;
}
if (!isTaskRunning(currentRunningTask)) {
logger.debug(
`Task ${currentRunningTask.id} is not running, skipping log entry with message: ${message}.`,
);
return;
}
currentRunningTask.logs.push(message);
});
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
logger?.error(
"❌ Codeflash activation failed",
error instanceof Error ? error : undefined,
context.subscriptions.push(
vscode.window.registerWebviewViewProvider(
SIDEBAR_VIEW_ID,
new ErrorWebview(
{
icon: "error",
title: "Codeflash Activation Failed",
details: errorMessage,
},
context.extensionUri,
),
),
);
// Determine error category for better user guidance
let category = "Unknown";
let troubleshootingHint = "";
if (
errorMessage.includes("Python") ||
errorMessage.includes("interpreter")
) {
category = "Python Environment";
troubleshootingHint =
"Ensure Python 3.8+ is installed and in your PATH. Try: python --version";
} else if (
errorMessage.includes("LSP") ||
errorMessage.includes("Language Server")
) {
category = "Language Server";
troubleshootingHint =
"The Codeflash LSP server failed to start. Check if required dependencies are installed.";
} else if (errorMessage.includes("timeout")) {
category = "Startup Timeout";
troubleshootingHint =
"Startup took too long. This might be due to slow system or missing dependencies.";
}
// Show categorized error with helpful actions
const action = await vscode.window.showErrorMessage(
`Codeflash failed to activate (${category}): ${errorMessage}`,
"Show Details",
"Open Logs",
"Disable Extension",
"Report Issue",
);
if (action === "Show Details") {
const detailsMessage = `
Codeflash Activation Failure
Category: ${category}
Error: ${errorMessage}
Hint: ${troubleshootingHint}
Troubleshooting Steps:
1. Check Python installation: python --version
2. Verify VS Code Python extension is installed
3. Check Output channels: Codeflash, CF-LSP
4. Review VS Code logs: Help > Toggle Developer Tools > Console
For support, visit: https://github.com/your-repo/codeflash/issues
`.trim();
vscode.window.showInformationMessage(detailsMessage, { modal: true });
} else if (action === "Open Logs") {
vscode.commands.executeCommand("workbench.action.openWindowLog");
} else if (action === "Disable Extension") {
vscode.commands.executeCommand(
"workbench.extensions.action.disableWorkspace",
"codeflash",
);
} else if (action === "Report Issue") {
const issueUrl = `https://github.com/your-repo/codeflash/issues/new?template=bug_report.md&title=Activation%20Failure%20(${category})&body=${encodeURIComponent(
`Error: ${errorMessage}\nCategory: ${category}\nHint: ${troubleshootingHint}`,
)}`;
vscode.env.openExternal(vscode.Uri.parse(issueUrl));
}
throw error;
}
}
@ -453,7 +372,7 @@ async function handleOptimizeAll(
for (let i = 0; i < analysisResult.functions.length; i++) {
const functionName = analysisResult.functions[i];
await sidebarProvider.addFunctionToQueue(
sidebarProvider.addFunctionToQueue(
functionName!,
activeEditor.document.uri,
);
@ -486,5 +405,6 @@ async function checkApiKey(
}
export function deactivate(context: vscode.ExtensionContext) {
optimizationEventListener?.dispose();
context.subscriptions.forEach((subscription) => subscription.dispose());
}

View file

@ -2,7 +2,7 @@ import * as vscode from "vscode";
import type { LanguageClient } from "vscode-languageclient/node";
import { State as LanguageClientState } from "vscode-languageclient/node";
import { Logger } from "../utils";
import { debounce } from "../utils/debounce";
import debounce from "debounce";
import type { AnalysisService, NavigationService } from "../services";
interface OptimizationSuggestion {
@ -37,8 +37,10 @@ export class CodeflashCodeLensProvider implements vscode.CodeLensProvider {
// Refresh CodeLenses when LSP state changes
this._disposables.push(
this._lspClient.onDidChangeState(() => {
this._onDidChangeCodeLenses.fire();
this._lspClient.onDidChangeState((e) => {
if (e.newState === LanguageClientState.Running) {
this._onDidChangeCodeLenses.fire();
}
}),
vscode.workspace.onDidChangeTextDocument((e) => {
if (e.document.languageId === "python") {
@ -94,6 +96,9 @@ export class CodeflashCodeLensProvider implements vscode.CodeLensProvider {
!result.functions ||
result.functions.length === 0
) {
this._logger.warn(
`No optimizable functions found for ${document.uri.fsPath} : ${result.message}`,
);
return [];
}
@ -195,9 +200,11 @@ export class CodeflashCodeLensProvider implements vscode.CodeLensProvider {
return this._cachedSuggestions.get(uri.toString()) || [];
}
public refresh(): void {
public refresh(delay: number = 0): void {
this._cachedSuggestions.clear();
this._onDidChangeCodeLenses.fire();
debounce(() => this._onDidChangeCodeLenses.fire(), delay, {
immediate: delay === 0,
});
}
public dispose(): void {

View file

@ -12,17 +12,22 @@ import type {
NavigationService,
} from "../services";
import { Logger, generateWebviewHtml } from "../utils";
import { LSP_COMMANDS, SIDEBAR_BUNDLE_DIR } from "../constants";
import {
LSP_COMMANDS,
SIDEBAR_BUNDLE_DIR,
SIDEBAR_VIEW_ID,
} from "../constants";
import { optimizationEventEmitter, viewState } from "../webview/state";
import { randomUUID } from "crypto";
import { GitPatchProvider } from "./GitPatchProvider";
import type {
LogEntry,
QueueTaskItem,
RestoreStateMessage,
SidebarStatus,
UpdateStateMessage,
} from "@codeflash/types";
import { canRetryTaskInQueue } from "@codeflash/shared";
import { isTaskRunning, canRetryTaskInQueue } from "@codeflash/shared";
export class SidebarProvider implements vscode.WebviewViewProvider {
private _view?: vscode.WebviewView;
@ -30,7 +35,6 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
private readonly _logger: Logger;
private _currentFocusUri: vscode.Uri | null = null;
private _currentFunctionCount = 0;
private currentTaskId: string | null = null;
private initializedOnce = false;
private openedPathches = new Map<string, GitPatchProvider>();
@ -69,17 +73,8 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
);
webviewView.webview.onDidReceiveMessage(this.handleWebviewMessage);
// register a listener for the optimization event emitter
const optimizationEventListener = optimizationEventEmitter.event(
(message) => {
if (!this.currentTaskId || message.trim() === "") {
return;
}
this.updateUIQueueTasks("update", {
id: this.currentTaskId,
description: message,
});
},
this.addLogEntryToUI.bind(this),
);
this._disposables.push(
@ -94,6 +89,15 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
})();
}
private addLogEntryToUI(log: LogEntry) {
this.sendMessage({
type: "newLogEntry",
payload: {
log,
},
});
}
sendWebviewApiKeyError() {
this.updateWebviewState("apiKeyError", "Invalid API key.", null, null);
}
@ -302,7 +306,7 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
}
// send message to the lsp to cleanup the patch metadata
await this._optimizationService.sendPatchCleanupMessage(patchId);
await this.updateUIQueueTasks("remove", { id });
this.updateUIQueueTasks("remove", { id });
};
if (this.openedPathches.has(data.payload.patchFile)) {
this.openedPathches.get(data.payload.patchFile)?.showPatch();
@ -326,9 +330,88 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
case "viewLogs":
this.handleViewLogs();
break;
case "filesInWorkspace":
this.requestFilesInWorkspace();
break;
case "requestFunctions":
const filePath = data.payload.filePath;
this.requestFunctionsInFile(filePath);
break;
case "changeTaskFocus":
viewState.focusedTaskId = data.payload.id;
break;
case "showMessage":
const { type, message } = data.payload;
if (type === "info") {
vscode.window.showInformationMessage(message);
} else if (type === "error") {
vscode.window.showErrorMessage(message);
} else if (type === "warning") {
vscode.window.showWarningMessage(message);
}
break;
}
};
private async requestFunctionsInFile(
filePath: string,
functions?: string[],
): Promise<void> {
try {
const finalFunctions =
functions ||
(
await this._analysisService.getOptimizableFunctions(
vscode.Uri.file(filePath),
)
).functions;
this.sendMessage({
type: "recievedFunctions",
payload: {
filePath,
functions: finalFunctions,
},
});
} catch (error) {
this._logger.debug(
"Failed to get optimizable functions :" + (error as Error)?.message ||
"",
);
}
}
private async requestFilesInWorkspace(): Promise<void> {
let pathPrefix = "";
if (viewState.moduleRoot) {
const relativeModuleRoot = vscode.workspace.asRelativePath(
vscode.Uri.file(viewState.moduleRoot),
);
if (relativeModuleRoot !== viewState.moduleRoot) {
pathPrefix = relativeModuleRoot + "/";
}
}
this._logger.info(
`requesting files in workspace with prefix: ${pathPrefix}`,
);
const files = await vscode.workspace.findFiles(
pathPrefix + "**/*.py",
"**/site-packages/**",
);
const filesPayload = files.map((f) => {
return {
abs: f.fsPath,
rel: vscode.workspace.asRelativePath(f),
};
});
this.sendMessage({
type: "recievedFilesInWorkspace",
payload: {
files: filesPayload,
},
});
}
private async handleOptimizeCurrentDiff(): Promise<void> {
this._logger.info("Optimizing current diff");
const result = await this._lspClient.sendRequest<OptimizationResponse>(
@ -366,23 +449,38 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
if (!apiKey?.trim()) {
return;
}
const result = await this._lspClient.sendRequest<OptimizationResponse>(
LSP_COMMANDS.PROVIDE_API_KEY,
{ api_key: apiKey },
);
if (result.status === "success") {
this._logger.info("API key is valid.");
viewState.user_id = result.user_id as string;
// lazy option to restart the extension host, uncomment if we have issues with initializing the state
// await vscode.commands.executeCommand("workbench.action.restartExtensionHost");
this.handleInitialState();
this.sendMessage({
type: "setApiKeyLoadingState",
payload: {
loading: true,
},
});
try {
const result = await this._lspClient.sendRequest<OptimizationResponse>(
LSP_COMMANDS.PROVIDE_API_KEY,
{ api_key: apiKey },
);
if (result.status === "success") {
this._logger.info("API key is valid.");
viewState.user_id = result.user_id as string;
// lazy option to restart the extension host, uncomment if we have issues with initializing the state
// await vscode.commands.executeCommand("workbench.action.restartExtensionHost");
this.handleInitialState();
this.sendMessage({
type: "apiKeyEnterValid",
});
} else {
vscode.window.showErrorMessage("Invalid API key.");
this.sendMessage({
type: "apiKeyEnterInvalid",
});
}
} finally {
this.sendMessage({
type: "apiKeyEnterValid",
});
} else {
vscode.window.showErrorMessage("Invalid API key.");
this.sendMessage({
type: "apiKeyEnterInvalid",
type: "setApiKeyLoadingState",
payload: {
loading: false,
},
});
}
}
@ -471,6 +569,15 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
outputChannel.show(true); // true = preserve focus on current editor
}
changeTaskFocus(taskId: string): void {
viewState.focusedTaskId = taskId;
this.sendMessage({
type: "changeTaskFocusFromBackend",
payload: {
id: taskId,
},
});
}
addFunctionToQueue(
functionName: string,
uri?: vscode.Uri | string,
@ -510,7 +617,21 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
}
if (focusOnView) {
this._view?.show?.();
vscode.commands
.executeCommand("workbench.view.extension." + SIDEBAR_VIEW_ID)
.then(() => {
this._view?.show?.();
const runningTask = viewState.queueTasks.find(isTaskRunning);
// set the active tab to the main optimization tab
this.sendMessage({
type: "setActiveSidebarTab",
payload: {
// if there is an already running task show the queued new task in the tasks tab, if not the newly added task will start right away so show the main tab
tab: runningTask ? "tasks" : "optimization",
},
});
});
}
const taskId = randomUUID().toString();
@ -525,7 +646,25 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
this._optimizationService.addOptimizationTask(taskId, functionName, {
abortController,
onTaskStart: async (id): Promise<boolean> => {
this.currentTaskId = id;
this.updateUIQueueTasks("update", {
id,
status: "initializing",
});
const task = viewState.queueTasks.find((t) => t.id === id);
if (!task) {
return false;
}
this.changeTaskFocus(id);
const initLog: LogEntry = {
id: randomUUID(),
type: "text",
text: "initializing codeflash optimization process",
takes_time: true,
};
task.logs.push(initLog);
this.addLogEntryToUI(initLog);
this._logger.debug(
`Starting optimization process for function '${functionName}' via sidebar`,
);
@ -551,8 +690,6 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
}
viewState.running = true;
viewState.currentFunctionName = functionName;
// this.sendOptimizationStarted(functionName);
this.updateUIQueueTasks("update", {
id,
status: "optimizing",
@ -576,43 +713,43 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
onTaskError: (error, id) => {
viewState.running = false;
const msg = error instanceof Error ? error.message : String(error);
let displayedErrorMsg = msg;
if (msg.includes("NO OPTIMIZATIONS GENERATED")) {
// no optimizations are generated from the aiservice
this.updateUIQueueTasks("update", {
id,
status: "skipped",
description: "No optimizations are generated",
});
displayedErrorMsg = "No optimizations are generated";
} else if (msg.includes("No best optimizations found for")) {
// optimizations are generated but none are good enough, didn't pass the threshold
this.updateUIQueueTasks("update", {
id,
status: "skipped",
description: "No best optimization found",
});
} else if (
msg.includes("The threshold for test coverage was not met")
) {
this.updateUIQueueTasks("update", {
id,
status: "skipped",
description: msg,
});
} else if (msg.includes("NO TESTS GENERATED")) {
this.updateUIQueueTasks("update", {
id,
status: "skipped",
description: "No tests generated",
});
} else {
this.updateUIQueueTasks("update", {
id,
// status: "failed",
status: "skipped",
description: msg,
});
displayedErrorMsg = "No best optimization found";
}
// else if (
// msg.includes("The threshold for test coverage was not met")
// ) {
// }
else if (msg.includes("NO TESTS GENERATED")) {
displayedErrorMsg = "No tests generated";
}
const task = viewState.queueTasks.find((task) => task.id === id);
// At this point the task is technically running, so we can safely add a new log entry to the task
const logErrorMessage: LogEntry = {
id: randomUUID(),
text: displayedErrorMsg,
type: "error",
};
task?.logs.push(logErrorMessage);
this.sendMessage({
type: "newLogEntry",
payload: {
log: logErrorMessage,
},
});
this.updateUIQueueTasks("update", {
id,
status: "skipped",
description: displayedErrorMsg,
});
},
updateQueueTask: (payload) => {
this.updateUIQueueTasks("update", { id: taskId, ...payload });
@ -630,6 +767,10 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
`queue state changed (${mode}) : ${JSON.stringify(task)}`,
);
if (mode === "add") {
if (!task.id) {
return;
}
task.logs = [];
viewState.queueTasks.push(task as QueueTaskItem);
} else if (mode === "update") {
const index = viewState.queueTasks.findIndex((t) => t.id === task.id);
@ -673,9 +814,6 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
`Editor changed. New URI: ${editor?.document.uri.toString() ?? "None"}`,
);
if (!viewState.user_id) {
return;
}
const uri = this.getPythonFileUri(editor);
if (!uri) {
return;
@ -770,6 +908,7 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
this._logger.debug(
`Analysis result for ${uri.fsPath}: Status=${result.status}, Functions=${result.functions.length}, Message=${result.message}`,
);
this.requestFunctionsInFile(uri.fsPath, result.functions);
const stillFocused = this._currentFocusUri?.toString() === uri.toString();
if (this._view.visible && stillFocused) {

View file

@ -7,7 +7,6 @@ import {
import type { OptimizableFunctionsResponse } from "../types";
import { LSP_COMMANDS } from "../constants";
import { getFileNameFromUri, getFileNameFromUriString, Logger } from "../utils";
import { viewState } from "../webview/state";
import type { SidebarStatus } from "@codeflash/types";
export class AnalysisService {
@ -25,14 +24,6 @@ export class AnalysisService {
const uriString = uri.toString();
const fileName = getFileNameFromUri(uri) || "Unknown";
if (!viewState.user_id) {
return {
status: "serverError",
message: "Cannot analyze: User ID not found.",
functions: [],
};
}
if (this.client.state !== LanguageClientState.Running) {
this.logger.error(
`LSP client not running for analysis of ${fileName}. State: ${this.client.state}`,

View file

@ -1,4 +1,3 @@
import * as vscode from "vscode";
import type {
LanguageClientOptions,
ServerOptions,
@ -13,7 +12,6 @@ import {
} from "../constants";
import { Logger } from "../utils";
import { createInterceptingOutputChannel } from "../utils/outputChannelInterceptor";
import { optimizationEventEmitter } from "../webview/state";
export class LspService {
private client: LanguageClient | undefined;
@ -64,11 +62,11 @@ export class LspService {
synchronize: {},
outputChannel: createInterceptingOutputChannel(
this.logger.getOutputChannel(),
optimizationEventEmitter,
true,
),
traceOutputChannel: createInterceptingOutputChannel(
this.logger.getOutputChannel(),
optimizationEventEmitter,
true,
),
};
@ -123,10 +121,9 @@ export class LspService {
"Failed to start CF-LSP client",
error instanceof Error ? error : undefined,
);
vscode.window.showErrorMessage(
"Failed to start the Codeflash Language Server. See Output channels (CF-LSP, Log (Extension Host)) for details.",
throw new Error(
`**Failed to start Codeflash LSP server**\n\nError: \`${errorMessage}\` \nPython path: \`${pythonPath}\``,
);
throw new Error(`Failed to start LSP client: ${errorMessage}`);
}
}

View file

@ -93,20 +93,8 @@ export class OptimizationService {
);
try {
updateTask({ description: "Discovering function tests" });
// Step 2: Discover function tests
const testResult = await this.discoverFunctionTests(functionName);
if (testResult.status !== "success") {
const error = testResult.message || "Test discovery failed";
return {
success: false,
error: error,
};
}
updateTask({ description: "Performing function optimization" });
// Step 3: Perform function optimization
// Step 2: Perform function optimization
const optimizationResult =
await this.performFunctionOptimization(functionName);
@ -197,23 +185,6 @@ export class OptimizationService {
return result;
}
async discoverFunctionTests(
functionName: string,
): Promise<OptimizationResponse> {
this.logger.info(`Discovering tests for function ${functionName}`);
const result =
await this.optimizationClient.sendRequest<OptimizationResponse>(
LSP_COMMANDS.DISCOVER_FUNCTION_TESTS,
{ functionName },
);
this.logger.info(
`Test discovery successful for ${functionName}. Response: ${JSON.stringify(result)}`,
);
return result;
}
async performFunctionOptimization(
functionName: string,
): Promise<OptimizationResponse> {

View file

@ -3,10 +3,9 @@ import type {
PythonExtension,
ResolvedEnvironment,
} from "@vscode/python-extension";
import { LSP_MODULE_NAME, PYTHON_EXTENSION_ID } from "../constants";
import { PYTHON_EXTENSION_ID } from "../constants";
import type { ErrorWebviewConfig } from "../types";
import { Logger } from "../utils";
import { execSync } from "child_process";
interface ExtensionDependencies {
pythonPath?: string;
@ -58,50 +57,24 @@ export class PythonService {
this.logger.info(`Codeflash uses Python in the environment: ${pythonPath}`);
// check for codeflash lsp module
try {
execSync(`${pythonPath} -c "import ${LSP_MODULE_NAME}"`, {
env: { CODEFLASH_LSP: "true" },
});
} catch (error) {
const msg = error instanceof Error ? error.message : String(error);
const match = msg.match(/ModuleNotFoundError:.*\n?/);
const errorMsg = match ? match[0].trim() : msg.trim();
// try {
// execSync(`${pythonPath} -c "import ${LSP_MODULE_NAME}"`, {
// env: { CODEFLASH_LSP: "true" },
// });
// } catch (error) {
// const msg = error instanceof Error ? error.message : String(error);
// const match = msg.match(/ModuleNotFoundError:.*\n?/);
// const errorMsg = match ? match[0].trim() : msg.trim();
return {
errorConfig: {
icon: "warning",
title: "Codeflash LSP Module Not Found",
details: `${errorMsg}<br/><br/> Active Python: ${pythonPath}`,
terminalCommand: `pip install --upgrade codeflash`,
},
};
}
// check for pyproject.toml file
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
if (!workspaceFolder) {
return {
errorConfig: {
icon: "warning",
title: "No Workspace Folder Detected",
details: "Please open a folder or workspace to use Codeflash.",
},
};
}
try {
await vscode.workspace.fs.stat(
vscode.Uri.file(workspaceFolder?.uri.path + "/pyproject.toml"),
);
} catch (error) {
this.logger.error(
"Error checking for pyproject.toml file",
error instanceof Error ? error : undefined,
);
return {
errorConfig: codeflashInitErrorConfig,
};
}
// return {
// errorConfig: {
// icon: "warning",
// title: "Codeflash LSP Module Not Found",
// details: `${errorMsg}<br/><br/> Active Python: ${pythonPath}`,
// terminalCommand: `pip install --upgrade codeflash`,
// },
// };
// }
return {
pythonPath,

View file

@ -2,13 +2,23 @@ import type {
ApiKeyEnterInvalidMessage,
ApiKeyEnterMessage,
ApiKeyEnterValidMessage,
ChangeTaskFocusFromBackendMessage,
ChangeTaskFocusMessage,
CreateSampleFileMessage,
DeleteTaskMessage,
FilesInWorkspace,
NavigateToFunctionMessage,
NewLogEntryMessage,
OptimizeCurrentDiffMessage,
OptimizeFunctionMessage,
RecievedFilesInWorkspace,
RecievedFunctionsMessage,
RequestAnalysisMessage,
RequestFunctionsMessage,
RestoreStateMessage,
SetActiveSidebarTabMessage,
SetApiKeyLoadingStateMessage,
ShowMessageMessage,
UpdateQueueTasksMessage,
UpdateStateMessage,
ViewDiffMessage,
@ -25,6 +35,7 @@ export interface ErrorWebviewConfig {
}
export type IncomingWebviewMessage =
| ShowMessageMessage
| RequestAnalysisMessage
| NavigateToFunctionMessage
| OptimizeFunctionMessage
@ -35,11 +46,20 @@ export type IncomingWebviewMessage =
| ViewPatchMessage
| DeleteTaskMessage
| ViewLogsMessage
| OptimizeCurrentDiffMessage;
| OptimizeCurrentDiffMessage
| FilesInWorkspace
| RequestFunctionsMessage
| ChangeTaskFocusMessage;
export type OutgoingWebviewMessage =
| UpdateStateMessage
| RestoreStateMessage
| UpdateQueueTasksMessage
| ApiKeyEnterInvalidMessage
| ApiKeyEnterValidMessage;
| ApiKeyEnterValidMessage
| NewLogEntryMessage
| RecievedFilesInWorkspace
| RecievedFunctionsMessage
| ChangeTaskFocusFromBackendMessage
| SetActiveSidebarTabMessage
| SetApiKeyLoadingStateMessage;

View file

@ -8,6 +8,7 @@ import type { ErrorWebviewConfig } from "../types";
import * as vscode from "vscode";
import { SIDEBAR_BUNDLE_DIR } from "../constants";
import { generateNonce, getStaticMeta } from "./staticUris";
import { marked } from "marked";
export class ErrorWebview implements WebviewViewProvider {
constructor(
@ -45,6 +46,7 @@ export class ErrorWebview implements WebviewViewProvider {
}
private getHtmlContent(metaTags: string, nonce: string): string {
const detailsMarkdown = marked(this.config.details);
const terminalCommandSection = this.config.terminalCommand
? `
<div class="terminal-section">
@ -203,7 +205,7 @@ export class ErrorWebview implements WebviewViewProvider {
<span class="codicon codicon-${this.config.icon}"></span>
</p>
<h2 class="title">${this.config.title}</h2>
<p>${this.config.details}</p>
<p>${detailsMarkdown}</p>
${terminalCommandSection}
<button id="restart-extension-btn" class="execute-btn">Restart Extension Host</button>

View file

@ -1,15 +0,0 @@
export function debounce<F extends (...args: any[]) => any>(
func: F,
waitFor: number,
) {
let timeout: NodeJS.Timeout | null = null;
const debounced = (...args: Parameters<F>) => {
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(() => func(...args), waitFor);
};
return debounced as (...args: Parameters<F>) => void;
}

View file

@ -1,58 +1,59 @@
import type * as vscode from "vscode";
import { optimizationEventEmitter } from "../webview/state";
import type { LogEntry } from "@codeflash/types";
import { LSPMessageType } from "@codeflash/types";
import { Logger } from "./logger";
import PQueue from "p-queue";
import { randomUUID } from "crypto";
const whitelistMessages = [
"Generating optimized candidates",
"Generated ",
// "Did not find any pre-existing tests",
"Determining best optimization",
"Optimization candidate",
"Test results matched",
"Refining",
"Generating explanation",
"Best candidate",
];
function extractMessageFromLspLog(line: string): string {
try {
const idx = line.indexOf("::::");
if (idx === -1) {
return "";
}
const message = line.slice(idx + 4).trim();
for (const prefix of whitelistMessages) {
if (message.startsWith(prefix)) {
if (prefix === "Best candidate") {
return "Found best candidate";
}
return message;
}
}
return "";
} catch {
return "";
}
}
const delimiter = "\u241F";
const queue = new PQueue({ concurrency: 1 });
export function createInterceptingOutputChannel(
innerChannel: vscode.OutputChannel,
eventEmitter?: vscode.EventEmitter<string>,
emitEvents: boolean = false,
): vscode.OutputChannel {
class WrappedChannel implements vscode.OutputChannel {
constructor(
private readonly inner: vscode.OutputChannel,
private readonly eventEmitter?: vscode.EventEmitter<string>,
) {}
constructor(private readonly inner: vscode.OutputChannel) {
this.emitEvents = emitEvents;
this.logger = new Logger("LSP-MESSAGE-INTERCEPTOR");
}
name: string = innerChannel.name;
private emitEvents: boolean;
private logger: Logger;
private extractMessageFromLspLog(line: string): LogEntry[] {
const messages = line.split(delimiter);
if (messages.length <= 1) {
return [];
}
messages.pop();
const parsedMessages = [];
for (const msg of messages) {
try {
const parsed = JSON.parse(msg.trim());
if (Object.values(LSPMessageType).includes(parsed?.type)) {
parsedMessages.push(parsed);
}
} catch (err) {
this.logger.info(
"Failed to parse LSP message " + msg + " with error " + err,
);
}
}
return parsedMessages;
}
fireEvent(line: string): void {
if (!!this.eventEmitter) {
const msg = extractMessageFromLspLog(line);
if (msg !== "") {
this.eventEmitter.fire(msg);
if (this.emitEvents) {
const parsedMessage = this.extractMessageFromLspLog(line);
for (const msg of parsedMessage) {
const uuid = randomUUID();
this.logger.debug("sending LSP message " + msg + " with id " + uuid);
optimizationEventEmitter.fire({
...msg,
id: uuid,
});
}
}
}
@ -64,7 +65,7 @@ export function createInterceptingOutputChannel(
*/
append(value: string): void {
this.inner.append(value);
this.fireEvent(value);
queue.add(() => this.fireEvent(value));
}
/**
@ -75,7 +76,7 @@ export function createInterceptingOutputChannel(
*/
appendLine(value: string): void {
this.inner.appendLine(value);
this.fireEvent(value);
queue.add(() => this.fireEvent(value));
}
/**
@ -106,7 +107,10 @@ export function createInterceptingOutputChannel(
columnOrPreserveFocus?: vscode.ViewColumn | boolean,
preserveFocus?: boolean,
): void {
this.inner.show(columnOrPreserveFocus as any, preserveFocus);
this.inner.show(
columnOrPreserveFocus as vscode.ViewColumn,
preserveFocus,
);
}
/**
@ -120,9 +124,10 @@ export function createInterceptingOutputChannel(
* Dispose and free associated resources.
*/
dispose(): void {
queue.clear();
this.inner.dispose();
}
}
return new WrappedChannel(innerChannel, eventEmitter);
return new WrappedChannel(innerChannel);
}

View file

@ -30,7 +30,7 @@ export const getStaticMeta = (
default-src 'none';
style-src ${webview.cspSource} 'unsafe-inline';
font-src ${webview.cspSource};
script-src ${webview.cspSource} 'nonce-${nonce}';
script-src ${webview.cspSource} 'nonce-${nonce}' 'unsafe-eval';
img-src ${webview.cspSource} https:;
"
/>

View file

@ -1,7 +1,7 @@
import { proxy } from "valtio/vanilla";
import { proxyMap } from "valtio/vanilla/utils";
import * as vscode from "vscode";
import type { QueueTaskItem, SidebarStatus } from "@codeflash/types";
import type { LogEntry, QueueTaskItem, SidebarStatus } from "@codeflash/types";
export const viewState = proxy<{
running: boolean;
@ -9,32 +9,22 @@ export const viewState = proxy<{
currentStatus?: SidebarStatus;
existingOptimizationFetched: boolean;
currentStatusMessage?: string;
currentFunctionName: string;
queueTasks: QueueTaskItem[];
focusedTaskId?: string;
steps: Map<
number,
{
status: string;
details?: any;
details?: unknown;
}
>;
moduleRoot: string;
}>({
running: false,
currentFunctionName: "",
existingOptimizationFetched: false,
queueTasks: [
// {
// id: "db77f475-2ff0-4c4f-9739-cbf316bb1539",
// status: "completed",
// functionName: "sorter",
// filepath:"/home/codeflash-sample/sorter.py",
// description: "Optimization completed successfully",
// patch_file: "/home/mohammed/.config/codeflash/patches/webhooks-testing-20250814-184153.sorter_new_3.patch",
// explanation: "\nThe optimization replaces a manual bubble sort implementation with Python's built-in `arr.sort()` method, delivering a **28,746% speedup** (from 841ms to 2.91ms).\n\n**Key optimizations:**\n1. **Algorithm change**: Replaced O(n²) bubble sort with Python's Timsort (O(n log n) average case)\n2. **Native implementation**: `arr.sort()` uses highly optimized C code instead of interpreted Python loops\n3. **Eliminated redundant operations**: Removed ~11.5 million loop iterations and array access operations\n\n**Performance impact by test type:**\n- **Large datasets** (1000+ elements): Massive speedup due to algorithmic improvement from quadratic to linearithmic complexity\n- **Already sorted data**: Timsort's adaptive nature makes it nearly O(n) for sorted inputs, while bubble sort still requires O(n²) comparisons\n- **Small lists**: Still faster due to native C implementation avoiding Python loop overhead\n- **All data types**: Works identically for integers, floats, strings, and mixed comparable types\n\nThe line profiler shows the original code spent 83.1% of time in the inner loop operations (comparisons and swaps), while the optimized version completes the entire sort in just 19.6% of the total runtime. The optimization maintains identical behavior including in-place mutation and all print statements.\n",
// }
],
queueTasks: [],
steps: proxyMap(),
moduleRoot: "",
});
export const optimizationEventEmitter = new vscode.EventEmitter<string>(); // for emitting events from the lsp server logs to the sidebar, for tracking the running optimization progress
export const optimizationEventEmitter = new vscode.EventEmitter<LogEntry>(); // for emitting events from the lsp server logs to the sidebar, for tracking the running optimization progress

View file

@ -1,7 +1,7 @@
{
"compilerOptions": {
"module": "commonjs", // or keep nodenext if needed
"moduleResolution": "node", // 👈 classic behavior
"module": "es2022", // or keep nodenext if needed
"moduleResolution": "bundler", // 👈 classic behavior
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"sourceMap": true,
@ -22,7 +22,7 @@
"skipLibCheck": true,
"declaration": true,
"declarationMap": true,
"jsx": "react-jsx",
"jsx": "react-jsx"
},
"include": [
"src/**/*",