[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:
parent
5d71db60e6
commit
25c6da57a3
54 changed files with 5525 additions and 1529 deletions
|
|
@ -43,9 +43,9 @@ export default [
|
|||
{
|
||||
prefer: "type-imports",
|
||||
disallowTypeAnnotations: false,
|
||||
fixStyle: "separate-type-imports"
|
||||
}
|
||||
]
|
||||
fixStyle: "separate-type-imports",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
];
|
||||
|
|
|
|||
1772
js/VSC-Extension/package-lock.json
generated
1772
js/VSC-Extension/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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": [
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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{" "}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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)}`,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 })),
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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}`,
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:;
|
||||
"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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/**/*",
|
||||
|
|
|
|||
Loading…
Reference in a new issue