Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
405 changes: 401 additions & 4 deletions mpp-vscode/src/providers/chat-view.ts

Large diffs are not rendered by default.

125 changes: 122 additions & 3 deletions mpp-vscode/webview/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import React, { useState, useEffect, useCallback } from 'react';
import { Timeline } from './components/Timeline';
import { ChatInput } from './components/ChatInput';
import { ModelConfig } from './components/ModelSelector';
import { SelectedFile } from './components/FileChip';
import { CompletionItem } from './components/CompletionPopup';
import { useVSCode, ExtensionMessage } from './hooks/useVSCode';
import type { AgentState, ToolCallInfo, TerminalOutput, ToolCallTimelineItem } from './types/timeline';
import './App.css';
Expand All @@ -18,6 +20,12 @@ interface ConfigState {
currentConfigName: string | null;
}

interface CompletionResult {
newText: string;
newCursorPosition: number;
shouldTriggerNextCompletion: boolean;
}

const App: React.FC = () => {
// Agent state - mirrors ComposeRenderer's state
const [agentState, setAgentState] = useState<AgentState>({
Expand All @@ -35,6 +43,16 @@ const App: React.FC = () => {
currentConfigName: null
});

// Token usage state
const [totalTokens, setTotalTokens] = useState<number | null>(null);

// Active file state (for auto-add current file feature)
const [activeFile, setActiveFile] = useState<SelectedFile | null>(null);

// Completion state - from mpp-core
const [completionItems, setCompletionItems] = useState<CompletionItem[]>([]);
const [completionResult, setCompletionResult] = useState<CompletionResult | null>(null);

const { postMessage, onMessage, isVSCode } = useVSCode();

// Handle messages from extension
Expand Down Expand Up @@ -198,6 +216,43 @@ const App: React.FC = () => {
});
}
break;

// Token usage update
case 'tokenUpdate':
if (msg.data?.totalTokens != null) {
setTotalTokens(msg.data.totalTokens as number);
}
break;

// Active file changed (for auto-add current file)
case 'activeFileChanged':
if (msg.data) {
setActiveFile({
path: msg.data.path as string,
name: msg.data.name as string,
relativePath: msg.data.path as string,
isDirectory: msg.data.isDirectory as boolean || false
});
}
break;

// Completion results from mpp-core
case 'completionsResult':
if (msg.data?.items) {
setCompletionItems(msg.data.items as CompletionItem[]);
}
break;

// Completion applied result
case 'completionApplied':
if (msg.data) {
setCompletionResult({
newText: msg.data.newText as string,
newCursorPosition: msg.data.newCursorPosition as number,
shouldTriggerNextCompletion: msg.data.shouldTriggerNextCompletion as boolean
});
}
break;
Comment on lines +220 to +255
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Strengthen type safety in message handling.

The message handlers use unchecked type assertions (as string, as number, as CompletionItem[]) without validating the data structure. If msg.data doesn't match the expected shape, runtime errors could occur.

Consider adding runtime validation or using type guards to verify the data before casting, especially for complex types like CompletionItem[].

Example validation pattern:

 case 'completionsResult':
   if (msg.data?.items) {
-    setCompletionItems(msg.data.items as CompletionItem[]);
+    const items = msg.data.items;
+    if (Array.isArray(items)) {
+      setCompletionItems(items as CompletionItem[]);
+    }
   }
   break;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Token usage update
case 'tokenUpdate':
if (msg.data?.totalTokens != null) {
setTotalTokens(msg.data.totalTokens as number);
}
break;
// Active file changed (for auto-add current file)
case 'activeFileChanged':
if (msg.data) {
setActiveFile({
path: msg.data.path as string,
name: msg.data.name as string,
relativePath: msg.data.path as string,
isDirectory: msg.data.isDirectory as boolean || false
});
}
break;
// Completion results from mpp-core
case 'completionsResult':
if (msg.data?.items) {
setCompletionItems(msg.data.items as CompletionItem[]);
}
break;
// Completion applied result
case 'completionApplied':
if (msg.data) {
setCompletionResult({
newText: msg.data.newText as string,
newCursorPosition: msg.data.newCursorPosition as number,
shouldTriggerNextCompletion: msg.data.shouldTriggerNextCompletion as boolean
});
}
break;
// Token usage update
case 'tokenUpdate':
if (msg.data?.totalTokens != null) {
setTotalTokens(msg.data.totalTokens as number);
}
break;
// Active file changed (for auto-add current file)
case 'activeFileChanged':
if (msg.data) {
setActiveFile({
path: msg.data.path as string,
name: msg.data.name as string,
relativePath: msg.data.path as string,
isDirectory: msg.data.isDirectory as boolean || false
});
}
break;
// Completion results from mpp-core
case 'completionsResult':
if (msg.data?.items) {
const items = msg.data.items;
if (Array.isArray(items)) {
setCompletionItems(items as CompletionItem[]);
}
}
break;
// Completion applied result
case 'completionApplied':
if (msg.data) {
setCompletionResult({
newText: msg.data.newText as string,
newCursorPosition: msg.data.newCursorPosition as number,
shouldTriggerNextCompletion: msg.data.shouldTriggerNextCompletion as boolean
});
}
break;
🤖 Prompt for AI Agents
In mpp-vscode/webview/src/App.tsx around lines 220 to 255, the message handlers
perform unchecked casts (e.g., "as string", "as number", "as CompletionItem[]")
which can cause runtime errors if msg.data is malformed; replace these casts
with runtime validation/type-guards: check msg.data exists, validate primitive
fields with typeof (e.g., typeof msg.data.totalTokens === 'number'), validate
objects have required keys, validate arrays with Array.isArray and per-item
type-guards for CompletionItem, and only call state setters when validation
passes; on validation failure, log an error and avoid updating state (or use
safe defaults), and factor repeated checks into small helper validators to keep
the handler concise.

}
}, []);

Expand All @@ -206,21 +261,35 @@ const App: React.FC = () => {
return onMessage(handleExtensionMessage);
}, [onMessage, handleExtensionMessage]);

// Request config on mount
useEffect(() => {
postMessage({ type: 'requestConfig' });
}, [postMessage]);

// Send message to extension
const handleSend = useCallback((content: string) => {
const handleSend = useCallback((content: string, files?: SelectedFile[]) => {
// Build message with file context (DevIns format)
let fullContent = content;
if (files && files.length > 0) {
const fileCommands = files.map(f =>
f.isDirectory ? `/dir:${f.relativePath}` : `/file:${f.relativePath}`
).join('\n');
fullContent = `${fileCommands}\n\n${content}`;
}

// Immediately show user message in timeline for feedback
setAgentState(prev => ({
...prev,
isProcessing: true,
timeline: [...prev.timeline, {
type: 'message',
timestamp: Date.now(),
message: { role: 'user', content }
message: { role: 'user', content: fullContent }
}]
}));

// Send to extension
postMessage({ type: 'sendMessage', content });
postMessage({ type: 'sendMessage', content: fullContent });
}, [postMessage]);

// Clear history
Expand Down Expand Up @@ -248,6 +317,48 @@ const App: React.FC = () => {
postMessage({ type: 'selectConfig', data: { configName: config.name } });
}, [postMessage]);

// Handle prompt optimization
const handlePromptOptimize = useCallback(async (prompt: string): Promise<string> => {
return new Promise((resolve) => {
// Send optimization request to extension
postMessage({ type: 'action', action: 'optimizePrompt', data: { prompt } });

// Listen for response
const handler = (event: MessageEvent) => {
const msg = event.data;
if (msg.type === 'promptOptimized' && msg.data?.optimizedPrompt) {
window.removeEventListener('message', handler);
resolve(msg.data.optimizedPrompt as string);
} else if (msg.type === 'promptOptimizeFailed') {
window.removeEventListener('message', handler);
resolve(prompt); // Return original on failure
}
};
window.addEventListener('message', handler);

// Timeout after 30 seconds
setTimeout(() => {
window.removeEventListener('message', handler);
resolve(prompt);
}, 30000);
});
}, [postMessage]);
Comment on lines +320 to +345
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix memory leak and use established message handling pattern.

The handlePromptOptimize function adds a window event listener that isn't cleaned up if the component unmounts, creating a memory leak. Additionally, it bypasses the useVSCode hook's onMessage mechanism, which is inconsistent with the rest of the codebase.

Refactor to use the established pattern or manage cleanup properly:

 const handlePromptOptimize = useCallback(async (prompt: string): Promise<string> => {
   return new Promise((resolve) => {
+    let resolved = false;
+    
     postMessage({ type: 'action', action: 'optimizePrompt', data: { prompt } });

     const handler = (event: MessageEvent) => {
       const msg = event.data;
       if (msg.type === 'promptOptimized' && msg.data?.optimizedPrompt) {
+        if (resolved) return;
+        resolved = true;
         window.removeEventListener('message', handler);
         resolve(msg.data.optimizedPrompt as string);
       } else if (msg.type === 'promptOptimizeFailed') {
+        if (resolved) return;
+        resolved = true;
         window.removeEventListener('message', handler);
         resolve(prompt);
       }
     };
     window.addEventListener('message', handler);

     setTimeout(() => {
+      if (resolved) return;
+      resolved = true;
       window.removeEventListener('message', handler);
       resolve(prompt);
     }, 30000);
   });
 }, [postMessage]);

Better yet, consider using a request ID system with the onMessage hook to avoid manual event listener management entirely.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
mpp-vscode/webview/src/App.tsx around lines 320 to 345: the current
handlePromptOptimize creates a window message listener that can leak if the
component unmounts and it also bypasses the project's useVSCode onMessage
pattern; refactor to use the established useVSCode/onMessage mechanism (or pass
postMessage into a request/response helper) and implement a request ID
correlation so responses are matched to the Promise, and ensure any temporary
handlers are registered via the hook and removed on unmount; alternatively, keep
the Promise-based API but register the listener through the hook (not
window.addEventListener), store the timeout ID and request ID, and clear both
the listener and timeout when a response arrives or the component unmounts.


// Handle MCP config click
const handleMcpConfigClick = useCallback(() => {
postMessage({ type: 'action', action: 'openMcpConfig' });
}, [postMessage]);

// Handle get completions from mpp-core
const handleGetCompletions = useCallback((text: string, cursorPosition: number) => {
postMessage({ type: 'getCompletions', data: { text, cursorPosition } });
}, [postMessage]);

// Handle apply completion from mpp-core
const handleApplyCompletion = useCallback((text: string, cursorPosition: number, completionIndex: number) => {
postMessage({ type: 'applyCompletion', data: { text, cursorPosition, completionIndex } });
}, [postMessage]);

// Check if we need to show config prompt
const needsConfig = agentState.timeline.length === 0 &&
agentState.currentStreamingContent.includes('No configuration found') ||
Expand Down Expand Up @@ -310,11 +421,19 @@ const App: React.FC = () => {
onStop={handleStop}
onConfigSelect={handleConfigSelect}
onConfigureClick={handleOpenConfig}
onMcpConfigClick={handleMcpConfigClick}
onPromptOptimize={handlePromptOptimize}
onGetCompletions={handleGetCompletions}
onApplyCompletion={handleApplyCompletion}
completionItems={completionItems}
completionResult={completionResult}
disabled={agentState.isProcessing}
isExecuting={agentState.isProcessing}
placeholder="Ask AutoDev anything... (use / for commands, @ for agents)"
availableConfigs={configState.availableConfigs}
currentConfigName={configState.currentConfigName}
totalTokens={totalTokens}
activeFile={activeFile}
/>
</div>
);
Expand Down
62 changes: 51 additions & 11 deletions mpp-vscode/webview/src/components/ChatInput.css
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
.chat-input-container {
padding: 8px 12px 12px;
border-top: 1px solid var(--panel-border);
position: relative;
border: 1px solid var(--panel-border);
border-radius: 8px;
background: var(--background);
margin: 8px;
}

/* Toolbar */
/* Bottom Toolbar */
.input-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
padding: 0 4px;
padding: 6px 8px;
border-top: 1px solid var(--panel-border);
background: var(--selection-background);
}

.toolbar-left {
Expand Down Expand Up @@ -56,6 +59,13 @@
display: flex;
gap: 8px;
align-items: flex-end;
padding: 8px;
}

.input-with-completion {
position: relative;
flex: 1;
min-width: 0;
}

.chat-textarea {
Expand Down Expand Up @@ -149,17 +159,47 @@
}

.input-hint {
margin-top: 6px;
font-size: 11px;
font-size: 10px;
color: var(--foreground);
opacity: 0.5;
opacity: 0.4;
white-space: nowrap;
}

.input-hint kbd {
background: var(--selection-background);
padding: 2px 5px;
border-radius: 3px;
background: var(--background);
padding: 1px 4px;
border-radius: 2px;
font-family: inherit;
font-size: 9px;
}

/* Token indicator */
.token-indicator {
font-size: 11px;
color: var(--foreground);
opacity: 0.6;
padding: 2px 6px;
background: var(--background);
border-radius: 4px;
}

/* Enhance button */
.enhance-button {
display: flex;
align-items: center;
gap: 4px;
}

.enhance-button.enhancing {
color: var(--vscode-textLink-foreground, #3794ff);
animation: pulse 1.5s ease-in-out infinite;
}

.enhance-button .enhancing-text {
font-size: 10px;
}

@keyframes pulse {
0%, 100% { opacity: 0.6; }
50% { opacity: 1; }
}
Loading
Loading