diff --git a/vscode-extension/package-lock.json b/vscode-extension/package-lock.json index 94f3270..3cf39af 100644 --- a/vscode-extension/package-lock.json +++ b/vscode-extension/package-lock.json @@ -13,6 +13,7 @@ "diff": "^8.0.0-beta", "express": "^4.19.2", "openai": "^4.53.2", + "react-inspector": "^9.0.0", "ws": "^8.18.0" }, "devDependencies": { @@ -2618,6 +2619,25 @@ "node": ">= 0.8" } }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-inspector": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/react-inspector/-/react-inspector-9.0.0.tgz", + "integrity": "sha512-w/VJucSeHxlwRa2nfM2k7YhpT1r5EtlDOClSR+L7DyQP91QMdfFEDXDs9bPYN4kzP7umFtom7L0b2GGjph4Kow==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0" + } + }, "node_modules/readable-stream": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", @@ -5143,6 +5163,18 @@ "unpipe": "1.0.0" } }, + "react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "peer": true + }, + "react-inspector": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/react-inspector/-/react-inspector-9.0.0.tgz", + "integrity": "sha512-w/VJucSeHxlwRa2nfM2k7YhpT1r5EtlDOClSR+L7DyQP91QMdfFEDXDs9bPYN4kzP7umFtom7L0b2GGjph4Kow==", + "requires": {} + }, "readable-stream": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", diff --git a/vscode-extension/package.json b/vscode-extension/package.json index 254a7db..aef845d 100644 --- a/vscode-extension/package.json +++ b/vscode-extension/package.json @@ -10,6 +10,7 @@ }, "main": "./out/extension.js", "contributes": { + "viewsContainers": { "activitybar": [ { @@ -111,6 +112,7 @@ "diff": "^8.0.0-beta", "express": "^4.19.2", "openai": "^4.53.2", + "react-inspector": "^9.0.0", "ws": "^8.18.0" }, "extensionDependencies": [ diff --git a/vscode-extension/src/panels/LMApiHandler.ts b/vscode-extension/src/panels/LMApiHandler.ts index 29c04c8..0c93a7b 100644 --- a/vscode-extension/src/panels/LMApiHandler.ts +++ b/vscode-extension/src/panels/LMApiHandler.ts @@ -28,7 +28,17 @@ export class LMApiHandler { console.log(`Selecting model with options:`, modelOptions); //console.log('HERE', await vscode.lm.selectChatModels()); // gpt-5-mini gpt-4o-mini copilot-fast gpt-4o claude-haiku-4.5 gpt-4.1 oswe-vscode-prime auto - const [model] = await vscode.lm.selectChatModels(modelOptions); + const allModels = await vscode.lm.selectChatModels({}); + console.log('ALL AVAILABLE MODELS:', JSON.stringify(allModels.map(m => ({ + name: m.name, + vendor: m.vendor, + family: m.family, + version: m.version, + id: m.id + })), null, 2)); + + const [model] = await vscode.lm.selectChatModels(modelOptions); + console.log('SELECTED MODEL:', model ? `${model.vendor}/${model.family}` : 'NONE'); if (!model) { this.sendErrorResponse(message, JSON.stringify({ @@ -52,9 +62,19 @@ export class LMApiHandler { const clientWantsStreaming = message.data.options?.stream === true; console.log(`Client wants streaming: ${clientWantsStreaming}`); + const formattedMessages = message.data.messages.map((msg: any) => { + if (msg.role === 'user') { + return vscode.LanguageModelChatMessage.User(msg.content); + } else if (msg.role === 'assistant') { + return vscode.LanguageModelChatMessage.Assistant(msg.content); + } else { + // system messages get treated as user messages in VSCode LM API + return vscode.LanguageModelChatMessage.User(msg.content); + } + }); // Send the request - all requests return a stream in newer VSCode API const response = await model.sendRequest( - message.data.messages, + formattedMessages, message.data.options || {}, cancelTokenSource.token ); diff --git a/vscode-extension/webview-ui/package-lock.json b/vscode-extension/webview-ui/package-lock.json index cbc7025..0ff42b4 100644 --- a/vscode-extension/webview-ui/package-lock.json +++ b/vscode-extension/webview-ui/package-lock.json @@ -4632,16 +4632,17 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, "node_modules/typescript": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.4.tgz", - "integrity": "sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" } }, "node_modules/uc.micro": { @@ -9305,9 +9306,9 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, "typescript": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.4.tgz", - "integrity": "sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true }, "uc.micro": { @@ -11262,9 +11263,9 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, "typescript": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.4.tgz", - "integrity": "sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true }, "uc.micro": { diff --git a/vscode-extension/webview-ui/src/App.css b/vscode-extension/webview-ui/src/App.css index 67d90e7..5c2a157 100644 --- a/vscode-extension/webview-ui/src/App.css +++ b/vscode-extension/webview-ui/src/App.css @@ -31,6 +31,16 @@ main { max-width: -webkit-fill-available !important; } +.annotation-tile img { + max-width: 100% !important; + height: auto !important; +} + +.annotation-tile pre { + overflow-x: auto; + max-width: 100% !important; +} + .annotation-tile.selected { background-color: #e0e0e0; /* Slightly darker background when selected */ } diff --git a/vscode-extension/webview-ui/src/Generator.tsx b/vscode-extension/webview-ui/src/Generator.tsx new file mode 100644 index 0000000..b39e781 --- /dev/null +++ b/vscode-extension/webview-ui/src/Generator.tsx @@ -0,0 +1,490 @@ +import { AnnotationEditorProps } from "./App"; +import React, { useEffect, useState, useRef } from "react"; +import e from "cors"; +import "./tools.css"; +import { ChatMessage, lmApi } from "./lm-api-client"; + + +const Generator: React.FC = (props) => { + // State management + const [userInput, setUserInput] = useState(props.value.metadata.userInput || ''); + const [draftText, setDraftText] = useState(props.value.metadata.annotationDraftText || ''); + const [isGenerating, setIsGenerating] = useState(false); + const [error, setError] = useState(null); + const [hasGenerated, setHasGenerated] = useState(!!(props.value.metadata.annotationDraftText)); + const [isFinalized, setIsFinalized] = useState(props.value.metadata.isFinalized || false); + const [useMock, setUseMock] = useState(false); + const annotationTitle = props.value.metadata.annotationTitle || userInput || ''; + + const dynamicActions = [ + { + id: 'explain', + label: 'Explain Highlighted Code', + pattern: /\b(explain|what does this do|what does this code do|how does this work)\b/i, + systemPrompt: `You write concise annotation text that explains highlighted code for learners. +Return markdown only. No title. Prefer short paragraphs and section headers like ### Purpose or ### Behavior. Use numbered lists for step-by-step logic. Preserve inline code with backticks.`, + userSuffix: 'Explain the highlighted code clearly.', + }, + { + id: 'summarize', + label: 'Summarize', + pattern: /\b(summari[sz]e|summary|overview|brief|tldr|tl;dr)\b/i, + systemPrompt: `You write a concise summary of highlighted code. +Return markdown only. No title. Keep it to 2-4 sentences max. Focus on what the code accomplishes at a high level, not implementation details.`, + userSuffix: 'Summarize the highlighted code in a few sentences.', + }, + { + id: 'step-by-step', + label: 'Step-by-Step Walkthrough', + pattern: /\b(step[- ]by[- ]step|walkthrough|trace|line[- ]by[- ]line|walk through)\b/i, + systemPrompt: `You write a step-by-step walkthrough of highlighted code. +Return markdown only. No title. Use a numbered list where each step explains what one logical chunk of the code does, in execution order. Keep each step to one sentence.`, + userSuffix: 'Walk through the highlighted code step by step.', + }, + { + id: 'context', + label: 'How Does This Fit In?', + pattern: /\b(context|fit|role|purpose|why is this|relationship|how does this fit|within the)\b/i, + systemPrompt: `You explain how a piece of highlighted code fits within its surrounding class, module, or file. +Return markdown only. No title. Focus on the role this code plays in the larger structure: what calls it, what it enables, why it exists here rather than elsewhere.`, + userSuffix: 'Explain how the highlighted code fits within the overall class or module.', + }, + ]; + + const draftIsEmpty = !draftText.trim(); + const visibleActions = draftIsEmpty + ? dynamicActions + : dynamicActions.filter(a => a.pattern.test(userInput || annotationTitle)); + + const isMounted = useRef(true); + + // Check API availability on mount + useEffect(() => { + const lmApiAvailable = typeof lmApi !== 'undefined' && lmApi !== null && typeof lmApi.chat === 'function'; + if (!lmApiAvailable) { + setUseMock(true); + } + return () => { isMounted.current = false; }; + }, []); + + // Save state to metadata + useEffect(() => { + props.utils.setMetadata({ + userInput, + annotationDraftText: draftText, + annotationTitle: userInput || annotationTitle, + isFinalized, + }); + }, [userInput, draftText, isFinalized]); + + // Get document context + const anchorText = props.utils.getText(); + const documentText = props.value.document || ''; + const startPos = props.value.start; + const endPos = props.value.end; + + const createFormattedDocument = () => { + if (!documentText) return ''; + const before = documentText.substring(0, startPos); + const highlighted = documentText.substring(startPos, endPos); + const after = documentText.substring(endPos); + return `${before}<<${highlighted}>>${after}`; + }; + + const generateAnnotation = async () => { + if (!userInput.trim()) { + setError('Please describe what you want the annotation to do'); + return; + } + + setIsGenerating(true); + setError(null); + + try { + const formattedDocument = createFormattedDocument(); + + const prompt: ChatMessage[] = [ + { + role: "system", + content: `You write concise annotation text that fulfills the user's requested annotation. + +Return markdown only for the annotation body. +Do not include a title. +Do not default to generic "explain highlighted code" wording unless the user explicitly asked for an explanation. +Match the user's request closely. +Prefer short paragraphs and section headers only when they improve readability. +Preserve inline code formatting with backticks where useful. +Keep it clean enough to display directly inside an annotation.` + }, + { + role: "user", + content: + `User request: ${userInput}\n\n` + + `Highlighted code with context:\n\n${formattedDocument || anchorText}\n\n` + + `Write the annotation text now.` + } + ]; + + let response: string; + if (useMock) { + await new Promise(r => setTimeout(r, 500)); + response = '### Purpose\nThis method compares Song objects to determine their ordering.\n\n### Behavior\n1. **Null Check**: Throws if the other song is null.\n2. **Title**: Compares titles lexicographically.\n3. **Artist**: If titles match, compares artists.\n4. **Duration**: If both match, compares duration numerically.\n5. **Genre**: Final tiebreaker on genre.'; + } else { + response = await lmApi.chat(prompt, { + vendor: 'copilot', + family: 'gpt-4o', + temperature: 0.35 + }); + } + + if (isMounted.current) { + const nextDraft = response.trim(); + setDraftText(nextDraft); + setHasGenerated(true); + setIsFinalized(false); + props.utils.setMetadata({ + annotationDraftText: nextDraft, + annotationTitle: userInput, + }); + } + } catch (err) { + const errorMsg = err instanceof Error ? err.message : 'An error occurred while generating'; + console.error('[Generator] Error:', errorMsg, err); + if (isMounted.current) { + setError(errorMsg); + } + } finally { + if (isMounted.current) { + setIsGenerating(false); + } + } + }; + + const runDynamicAction = async (action: typeof dynamicActions[number]) => { + if (!anchorText.trim()) { + setError('No highlighted code selected'); + return; + } + + setIsGenerating(true); + setError(null); + + try { + const formattedDocument = createFormattedDocument(); + const prompt: ChatMessage[] = [ + { role: "system", content: action.systemPrompt }, + { + role: "user", + content: + `${action.userSuffix}\n\n` + + `Highlighted code with context:\n\n${formattedDocument || anchorText}` + } + ]; + + let response: string; + if (useMock) { + await new Promise(r => setTimeout(r, 500)); + response = `Mock result for "${action.label}".\n\nThis is placeholder text from the mock API.`; + } else { + response = await lmApi.chat(prompt, { + vendor: 'copilot', + family: 'gpt-4o', + temperature: 0.35 + }); + } + + if (isMounted.current) { + const nextDraft = response.trim(); + setDraftText(nextDraft); + setHasGenerated(true); + props.utils.setMetadata({ annotationDraftText: nextDraft }); + } + } catch (err) { + const errorMsg = err instanceof Error ? err.message : `Failed: ${action.label}`; + console.error(`[Generator] ${action.id} error:`, errorMsg, err); + if (isMounted.current) { + setError(errorMsg); + } + } finally { + if (isMounted.current) { + setIsGenerating(false); + } + } + }; + + const resetGenerator = () => { + setDraftText(''); + setError(null); + setHasGenerated(false); + setIsFinalized(false); + props.utils.setMetadata({ + annotationDraftText: '', + annotationTitle: '', + isFinalized: false, + }); + }; + + const finalizeAnnotation = () => { + props.utils.setMetadata({ + annotationDraftText: draftText, + annotationTitle: userInput || annotationTitle, + isFinalized: true, + }); + setIsFinalized(true); + }; + + const editAnnotation = () => { + setIsFinalized(false); + }; + + // Finalized state + if (hasGenerated && isFinalized) { + return ( +
+
+ {userInput || annotationTitle || 'Annotation'} + +
+
+
+ {draftText || "(No generated text yet)"} +
+
+
+ ); + } + + // Active state: editing draft + if (hasGenerated && !isFinalized) { + return ( +
+
+ ✓ Generated annotation active +
+ + +
+
+
+ {visibleActions.length > 0 && ( +
+ {visibleActions.map(action => ( + + ))} +
+ )} +